@@ -263,6 +263,15 @@ async function seed(): Promise<string> {
263263 return encodeURIComponent ( `${ sessionToken } .${ await makeSignature ( sessionToken , authSecret ) } ` )
264264}
265265
266+ /**
267+ * The rendered workspace content shell. While a streamed page is still arriving, React parks the
268+ * next segment's markup, shell included, in a hidden `<div id="S:n">` until it swaps it in, so the
269+ * class alone can match a second, hidden copy.
270+ */
271+ function contentShell ( page : Page ) : Locator {
272+ return page . locator ( '.workspace-content-shell' ) . filter ( { visible : true } )
273+ }
274+
266275async function visit ( page : Page , route : string ) {
267276 const path = `/workspace/${ workspaceId } /${ route } `
268277 const [ response ] = await Promise . all ( [
@@ -280,7 +289,7 @@ async function visit(page: Page, route: string) {
280289 new URL ( page . url ( ) ) . pathname . startsWith ( `/workspace/${ workspaceId } /` ) ,
281290 'Session was rejected'
282291 )
283- await expect ( page . locator ( '.workspace-content-shell' ) ) . toBeVisible ( )
292+ await expect ( contentShell ( page ) ) . toBeVisible ( )
284293 await expect ( page . getByText ( 'Application error' , { exact : false } ) ) . toHaveCount ( 0 )
285294 const contentByRoute : Record < string , Locator > = {
286295 home : page . getByRole ( 'textbox' , { name : 'Message' , exact : true } ) ,
@@ -337,7 +346,7 @@ async function expectNoOverflow(page: Page) {
337346 }
338347 )
339348 . toBeLessThanOrEqual ( 1 )
340- const shell = await page . locator ( '.workspace-content-shell' ) . boundingBox ( )
349+ const shell = await contentShell ( page ) . boundingBox ( )
341350 assert ( shell && shell . width > 0 && shell . x >= - 1 , 'The content must have usable horizontal space' )
342351 assert (
343352 shell . x + shell . width <= ( page . viewportSize ( ) ?. width ?? 0 ) + 1 ,
@@ -717,7 +726,7 @@ async function exerciseViewport(
717726 timeout : 120_000 ,
718727 } )
719728 await expect ( sidebar ) . toBeHidden ( )
720- await expect ( page . locator ( '.workspace-content-shell' ) ) . toBeVisible ( )
729+ await expect ( contentShell ( page ) ) . toBeVisible ( )
721730 await expect (
722731 page . getByRole ( 'button' , { name : `Actions for ${ folderName } ` , exact : true } )
723732 ) . toBeVisible ( { timeout : 120_000 } )
@@ -1100,61 +1109,59 @@ async function exerciseViewport(
11001109 await expectNoOverflow ( page )
11011110 }
11021111 )
1103- if ( viewport . name !== 'tablet' ) {
1104- await check ( `${ prefix } /workflow controls open and return to the canvas` , page , async ( ) => {
1105- await visit ( page , `w/${ workflowId } ` )
1106- await expect ( page . locator ( '.react-flow' ) ) . toBeVisible ( { timeout : 120_000 } )
1107- const panel = page . getByRole ( 'complementary' , { name : 'Workflow panel' , exact : true } )
1108- await expect ( panel ) . toBeHidden ( )
1109- await capture ( page , `${ browserName } -${ viewport . name } -workflow-canvas` )
1110- await page . getByRole ( 'button' , { name : 'Workflow' , exact : true } ) . tap ( )
1111- await expectContained ( page , panel )
1112- await capture ( page , `${ browserName } -${ viewport . name } -workflow-panel` )
1113- await page . getByRole ( 'button' , { name : 'Back to canvas' , exact : true } ) . tap ( )
1114- await expect ( panel ) . toBeHidden ( )
1112+ await check ( `${ prefix } /workflow controls open and return to the canvas` , page , async ( ) => {
1113+ await visit ( page , `w/${ workflowId } ` )
1114+ await expect ( page . locator ( '.react-flow' ) ) . toBeVisible ( { timeout : 120_000 } )
1115+ const panel = page . getByRole ( 'complementary' , { name : 'Workflow panel' , exact : true } )
1116+ await expect ( panel ) . toBeHidden ( )
1117+ await capture ( page , `${ browserName } -${ viewport . name } -workflow-canvas` )
1118+ await page . getByRole ( 'button' , { name : 'Workflow' , exact : true } ) . tap ( )
1119+ await expectContained ( page , panel )
1120+ await capture ( page , `${ browserName } -${ viewport . name } -workflow-panel` )
1121+ await page . getByRole ( 'button' , { name : 'Back to canvas' , exact : true } ) . tap ( )
1122+ await expect ( panel ) . toBeHidden ( )
1123+ await expect
1124+ . poll ( async ( ) => {
1125+ const canvas = await page . locator ( '.react-flow' ) . boundingBox ( )
1126+ const content = await contentShell ( page ) . boundingBox ( )
1127+ return canvas && content ? canvas . width / content . width : 0
1128+ } )
1129+ . toBeGreaterThan ( 0.95 )
1130+ if ( viewport . name === 'phone' ) {
1131+ await sql `update workflow set locked = true where id = ${ workflowId } `
1132+ await withDocumentNavigation ( page , page . url ( ) , ( ) =>
1133+ page . reload ( { waitUntil : 'commit' , timeout : 360_000 } )
1134+ )
1135+ const notifications = page . getByLabel ( 'Notifications' , { exact : true } )
1136+ await expect ( notifications ) . toContainText ( 'This workflow is locked' , {
1137+ timeout : 120_000 ,
1138+ } )
1139+ await expectContained ( page , notifications )
1140+ const unlock = notifications . getByRole ( 'button' , {
1141+ name : 'Unlock Workflow' ,
1142+ exact : true ,
1143+ } )
1144+ await expectContained ( page , unlock )
1145+ await capture ( page , `${ browserName } -phone-workflow-notification` )
1146+ const [ unlocked ] = await Promise . all ( [
1147+ page . waitForResponse (
1148+ ( response ) =>
1149+ response . request ( ) . method ( ) === 'PUT' &&
1150+ new URL ( response . url ( ) ) . pathname === `/api/workflows/${ workflowId } ` ,
1151+ { timeout : 120_000 }
1152+ ) ,
1153+ unlock . tap ( ) ,
1154+ ] )
1155+ assert . equal ( unlocked . status ( ) , 200 , 'Unlocking the workflow must succeed over HTTP' )
11151156 await expect
11161157 . poll ( async ( ) => {
1117- const canvas = await page . locator ( '.react-flow' ) . boundingBox ( )
1118- const content = await page . locator ( '.workspace-content-shell' ) . boundingBox ( )
1119- return canvas && content ? canvas . width / content . width : 0
1120- } )
1121- . toBeGreaterThan ( 0.95 )
1122- if ( viewport . name === 'phone' ) {
1123- await sql `update workflow set locked = true where id = ${ workflowId } `
1124- await withDocumentNavigation ( page , page . url ( ) , ( ) =>
1125- page . reload ( { waitUntil : 'commit' , timeout : 360_000 } )
1126- )
1127- const notifications = page . getByLabel ( 'Notifications' , { exact : true } )
1128- await expect ( notifications ) . toContainText ( 'This workflow is locked' , {
1129- timeout : 120_000 ,
1158+ const rows = await sql `select locked from workflow where id = ${ workflowId } `
1159+ return rows [ 0 ] ?. locked
11301160 } )
1131- await expectContained ( page , notifications )
1132- const unlock = notifications . getByRole ( 'button' , {
1133- name : 'Unlock Workflow' ,
1134- exact : true ,
1135- } )
1136- await expectContained ( page , unlock )
1137- await capture ( page , `${ browserName } -phone-workflow-notification` )
1138- const [ unlocked ] = await Promise . all ( [
1139- page . waitForResponse (
1140- ( response ) =>
1141- response . request ( ) . method ( ) === 'PUT' &&
1142- new URL ( response . url ( ) ) . pathname === `/api/workflows/${ workflowId } ` ,
1143- { timeout : 120_000 }
1144- ) ,
1145- unlock . tap ( ) ,
1146- ] )
1147- assert . equal ( unlocked . status ( ) , 200 , 'Unlocking the workflow must succeed over HTTP' )
1148- await expect
1149- . poll ( async ( ) => {
1150- const rows = await sql `select locked from workflow where id = ${ workflowId } `
1151- return rows [ 0 ] ?. locked
1152- } )
1153- . toBe ( false )
1154- }
1155- await expectNoOverflow ( page )
1156- } )
1157- }
1161+ . toBe ( false )
1162+ }
1163+ await expectNoOverflow ( page )
1164+ } )
11581165 }
11591166 } finally {
11601167 documentNavigations . delete ( page )
0 commit comments