11import { test , expect } from '@playwright/test' ;
22
3- // Helper: wait for the recommendation page to finish loading (spinner gone)
3+ // Helper: wait for the recommendation page to finish loading (spinner gone) and reach a terminal state.
4+ // In CI, hydration and network can be slow, so we wait for form, error, or step-0 UI to appear.
45async function waitForPageReady ( page : any ) : Promise < void > {
56 const spinner = page . locator ( '[role="progressbar"]' ) ;
67 await spinner . waitFor ( { state : 'hidden' , timeout : 30000 } ) . catch ( ( ) => { } ) ;
7- // Extra buffer for client hydration
8- await page . waitForTimeout ( 1000 ) ;
8+ const readyTimeout = process . env . CI ? 20000 : 5000 ;
9+ const deadline = Date . now ( ) + readyTimeout ;
10+ while ( Date . now ( ) < deadline ) {
11+ const hasError = await isErrorState ( page ) ;
12+ if ( hasError ) return ;
13+ const hasForm = await page . locator ( 'form' ) . first ( ) . isVisible ( ) . catch ( ( ) => false ) ;
14+ if ( hasForm ) return ;
15+ const hasStep0 = await page . getByRole ( 'button' , { name : / c o n t i n u e w i t h o u t s a v i n g | l o g i n .* g o o g l e .* d r i v e / i } ) . first ( ) . isVisible ( ) . catch ( ( ) => false ) ;
16+ if ( hasStep0 ) return ;
17+ await page . waitForTimeout ( 400 ) ;
18+ }
919}
1020
1121// Helper: check if page landed in an error/empty state (expected with fake IDs)
@@ -29,11 +39,12 @@ test.describe('Recommendation Creation', () => {
2939 await expect ( page ) . toHaveURL ( / .* r e c o m m e n d a t i o n s .* / ) ;
3040
3141 // With a fake ID the page will either show the form or an error — both are valid.
32- // Just verify something rendered after the spinner disappeared.
33- const hasError = await isErrorState ( page ) ;
34- const hasForm = await page . locator ( 'form' ) . first ( ) . isVisible ( ) . catch ( ( ) => false ) ;
35-
36- expect ( hasError || hasForm ) . toBeTruthy ( ) ;
42+ // Poll for terminal state so CI (slower hydration) doesn't flake.
43+ await expect ( async ( ) => {
44+ const hasError = await isErrorState ( page ) ;
45+ const hasForm = await page . locator ( 'form' ) . first ( ) . isVisible ( ) . catch ( ( ) => false ) ;
46+ expect ( hasError || hasForm ) . toBeTruthy ( ) ;
47+ } ) . toPass ( { timeout : process . env . CI ? 15000 : 5000 } ) ;
3748 } ) ;
3849
3950 test ( 'can navigate through form steps' , async ( { page } ) => {
@@ -63,11 +74,14 @@ test.describe('Recommendation Creation', () => {
6374 const googleDriveButton = page . getByRole ( 'button' , { name : / l o g i n .* g o o g l e .* d r i v e / i } ) ;
6475 const continueWithoutSaving = page . getByRole ( 'button' , { name : / c o n t i n u e w i t h o u t s a v i n g / i } ) ;
6576
66- const hasGoogleButton = await googleDriveButton . isVisible ( { timeout : 5000 } ) . catch ( ( ) => false ) ;
67- const hasContinueButton = await continueWithoutSaving . isVisible ( { timeout : 5000 } ) . catch ( ( ) => false ) ;
68-
69- expect ( hasGoogleButton || hasContinueButton ) . toBeTruthy ( ) ;
77+ // Poll so CI (slower render) doesn't flake
78+ await expect ( async ( ) => {
79+ const hasGoogleButton = await googleDriveButton . isVisible ( { timeout : 2000 } ) . catch ( ( ) => false ) ;
80+ const hasContinueButton = await continueWithoutSaving . isVisible ( { timeout : 2000 } ) . catch ( ( ) => false ) ;
81+ expect ( hasGoogleButton || hasContinueButton ) . toBeTruthy ( ) ;
82+ } ) . toPass ( { timeout : process . env . CI ? 15000 : 5000 } ) ;
7083
84+ const hasContinueButton = await continueWithoutSaving . isVisible ( { timeout : 5000 } ) . catch ( ( ) => false ) ;
7185 if ( hasContinueButton ) {
7286 await continueWithoutSaving . click ( ) ;
7387 await page . waitForTimeout ( 1000 ) ;
0 commit comments