Skip to content

Commit c250eb3

Browse files
committed
test(e2e): cover preview white-screen monitoring
1 parent 1f1e08f commit c250eb3

1 file changed

Lines changed: 159 additions & 0 deletions

File tree

e2e/ui/app-design-files.test.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,20 @@ import type { Locator, Page, Request } from '@playwright/test';
66
import { automatedUiScenarios } from '@/playwright/resources';
77
import type { UiScenario } from '@/playwright/resources';
88
import { T } from '@/timeouts';
9+
import {
10+
PREVIEW_WHITE_SCREEN_CONFIRMATION_MS,
11+
PREVIEW_WHITE_SCREEN_TIMEOUT_MS,
12+
} from '@open-design/contracts/runtime/preview-observability';
913

1014
const STORAGE_KEY = 'open-design:config';
1115
const TINY_PNG_B64 =
1216
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO5W6McAAAAASUVORK5CYII=';
1317

18+
interface CapturedSafetyEvent {
19+
event?: string;
20+
properties?: Record<string, unknown>;
21+
}
22+
1423
test.describe.configure({ timeout: T.xlong });
1524

1625
test.beforeEach(async ({ page }) => {
@@ -43,6 +52,78 @@ async function routeMockAgents(page: Page) {
4352
]);
4453
}
4554

55+
async function captureSafetyTelemetry(page: Page): Promise<CapturedSafetyEvent[]> {
56+
const events: CapturedSafetyEvent[] = [];
57+
await page.unroute('**/api/app-config').catch(() => {});
58+
await page.addInitScript((key) => {
59+
window.localStorage.setItem(
60+
key,
61+
JSON.stringify({
62+
mode: 'daemon',
63+
apiKey: '',
64+
baseUrl: 'https://api.anthropic.com',
65+
model: 'claude-sonnet-4-5',
66+
agentId: 'mock',
67+
skillId: null,
68+
designSystemId: null,
69+
onboardingCompleted: true,
70+
agentModels: {},
71+
privacyDecisionAt: 1,
72+
telemetry: { metrics: true, content: false, artifactManifest: false },
73+
}),
74+
);
75+
}, STORAGE_KEY);
76+
await page.route('**/api/app-config', async (route) => {
77+
if (route.request().method() !== 'GET') {
78+
await route.continue();
79+
return;
80+
}
81+
await route.fulfill({
82+
json: {
83+
config: {
84+
onboardingCompleted: true,
85+
agentId: 'mock',
86+
skillId: null,
87+
designSystemId: null,
88+
agentModels: {},
89+
privacyDecisionAt: 1,
90+
telemetry: { metrics: true, content: false, artifactManifest: false },
91+
},
92+
},
93+
});
94+
});
95+
await page.route('**/api/analytics/config', async (route) => {
96+
await route.fulfill({
97+
json: {
98+
enabled: true,
99+
env: 'e2e',
100+
key: 'phc_e2e',
101+
host: 'https://analytics.open-design.test',
102+
installationId: 'e2e-installation',
103+
},
104+
});
105+
});
106+
await page.route('https://analytics.open-design.test/**', async (route) => {
107+
const body = route.request().postData();
108+
if (body) {
109+
try {
110+
events.push(JSON.parse(body) as CapturedSafetyEvent);
111+
} catch {
112+
// posthog-js can use non-JSON batch encodings; this witness owns only
113+
// the direct JSON safety-telemetry transport.
114+
}
115+
}
116+
await route.fulfill({ status: 200, json: { status: 1 } });
117+
});
118+
return events;
119+
}
120+
121+
function capturedWhiteScreenEvents(
122+
events: CapturedSafetyEvent[],
123+
): CapturedSafetyEvent[] {
124+
return events.filter((event) => event.event === 'client_preview_white_screen');
125+
}
126+
46127
for (const entry of automatedUiScenarios().filter((scenario) => designFileFlows.has(scenario.flow ?? ''))) {
47128
test(`[${designFileScenarioPriority(entry)}]${criticalDesignFileScenarioTag(entry)} ${entry.id}: ${entry.title}`, async ({ page }) => {
48129
await routeMockAgents(page);
@@ -929,6 +1010,84 @@ test('[P0] @critical file workspace restores HTML preview after switching throug
9291010
await expect(page.getByTestId('file-workspace')).toBeVisible();
9301011
});
9311012

1013+
test('[P0] @critical white-screen monitoring recovers layout stalls and confirms persistent blanks', async ({ page }) => {
1014+
const safetyEvents = await captureSafetyTelemetry(page);
1015+
await routeMockAgents(page);
1016+
1017+
const projectId = await createProjectViaApi(page, 'Preview white-screen monitoring');
1018+
await seedHtmlArtifact(
1019+
page,
1020+
projectId,
1021+
'recoverable-blank.html',
1022+
`<!doctype html>
1023+
<html>
1024+
<head><style>main { display: none; }</style></head>
1025+
<body data-monitor-fixture="recoverable" data-monitor-recovered="false">
1026+
<main><h1>Recovered preview paint</h1></main>
1027+
<script>
1028+
window.__monitorFixtureStartedAt = performance.now();
1029+
window.addEventListener('resize', function () {
1030+
if (performance.now() - window.__monitorFixtureStartedAt < 4500) return;
1031+
document.querySelector('main').style.display = 'block';
1032+
document.body.dataset.monitorRecovered = 'true';
1033+
});
1034+
</script>
1035+
</body>
1036+
</html>`,
1037+
);
1038+
await seedHtmlArtifact(
1039+
page,
1040+
projectId,
1041+
'persistent-blank.html',
1042+
`<!doctype html>
1043+
<html>
1044+
<body data-monitor-fixture="persistent">
1045+
<script>window.__monitorFixtureReady = true;</script>
1046+
</body>
1047+
</html>`,
1048+
);
1049+
1050+
await page.goto(`/projects/${projectId}?forceInline=1`, { waitUntil: 'domcontentloaded' });
1051+
await expectWorkspaceReady(page);
1052+
await openDesignFile(page, 'recoverable-blank.html');
1053+
1054+
const activePreview = page.frameLocator('[data-testid="artifact-preview-frame"]');
1055+
const recoverableBody = activePreview.locator('body[data-monitor-fixture="recoverable"]');
1056+
const recoverableMain = recoverableBody.locator('main');
1057+
await expect(recoverableBody).toBeVisible();
1058+
await expect(recoverableBody).toHaveAttribute('data-monitor-recovered', 'true', {
1059+
timeout: PREVIEW_WHITE_SCREEN_TIMEOUT_MS + T.short,
1060+
});
1061+
await expect(recoverableMain).toHaveCSS('display', 'block', {
1062+
timeout: PREVIEW_WHITE_SCREEN_TIMEOUT_MS + T.short,
1063+
});
1064+
await page.waitForTimeout(PREVIEW_WHITE_SCREEN_CONFIRMATION_MS + 100);
1065+
expect(capturedWhiteScreenEvents(safetyEvents)).toEqual([]);
1066+
1067+
await openAllProjectFiles(page);
1068+
const persistentRow = await revealDesignFileRow(page, 'persistent-blank.html');
1069+
await persistentRow.getByRole('button').first().click();
1070+
await expect(page.getByRole('tab', { name: /persistent-blank\.html/i }))
1071+
.toHaveAttribute('aria-selected', 'true');
1072+
const persistentBody = activePreview.locator('body[data-monitor-fixture="persistent"]');
1073+
await expect(persistentBody).toBeAttached();
1074+
1075+
await page.waitForTimeout(PREVIEW_WHITE_SCREEN_TIMEOUT_MS - 500);
1076+
expect(capturedWhiteScreenEvents(safetyEvents)).toEqual([]);
1077+
1078+
await expect.poll(
1079+
() => capturedWhiteScreenEvents(safetyEvents).length,
1080+
{ timeout: PREVIEW_WHITE_SCREEN_CONFIRMATION_MS + T.short },
1081+
).toBe(1);
1082+
const [whiteScreen] = capturedWhiteScreenEvents(safetyEvents);
1083+
expect(whiteScreen?.properties).toMatchObject({
1084+
blank_observation_count: 2,
1085+
sample_interval_ms: PREVIEW_WHITE_SCREEN_CONFIRMATION_MS,
1086+
visible_element_count: 0,
1087+
visibility_state: 'visible',
1088+
});
1089+
});
1090+
9321091
test('[P0] @critical HTML file list and previews stay stable across repeated switches', async ({ page }) => {
9331092
await routeMockAgents(page);
9341093

0 commit comments

Comments
 (0)