@@ -6,11 +6,20 @@ import type { Locator, Page, Request } from '@playwright/test';
66import { automatedUiScenarios } from '@/playwright/resources' ;
77import type { UiScenario } from '@/playwright/resources' ;
88import { 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
1014const STORAGE_KEY = 'open-design:config' ;
1115const TINY_PNG_B64 =
1216 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO5W6McAAAAASUVORK5CYII=' ;
1317
18+ interface CapturedSafetyEvent {
19+ event ?: string ;
20+ properties ?: Record < string , unknown > ;
21+ }
22+
1423test . describe . configure ( { timeout : T . xlong } ) ;
1524
1625test . 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+
46127for ( 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 : / p e r s i s t e n t - b l a n k \. h t m l / 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+
9321091test ( '[P0] @critical HTML file list and previews stay stable across repeated switches' , async ( { page } ) => {
9331092 await routeMockAgents ( page ) ;
9341093
0 commit comments