@@ -31,19 +31,16 @@ export const editableTitle = (page: Page) => page.getByTestId('editable-title');
3131export const currentDriveTitle = ( page : Page ) =>
3232 page . getByTestId ( 'current-drive-title' ) ;
3333export const publicReadRightLocator = ( page : Page ) =>
34- page
35- . locator (
36- '[data-test="right-public"] input[type="checkbox"]:not([disabled])' ,
37- )
38- . first ( ) ;
34+ page . locator ( '[data-test="right-public"] input[type="checkbox"]' ) . first ( ) ;
3935export const contextMenu = '[data-test="context-menu"]' ;
4036export const addressBar = ( page : Page ) => page . getByTestId ( 'adress-bar' ) ;
4137export const newDriveMenuItem = '[data-test="menu-item-new-drive"]' ;
4238
4339export const defaultDevServer = 'http://localhost:9883' ;
4440export const currentDialogOkButton = 'dialog[open] >> footer >> text=Ok' ;
45- // SQLite FTS5 updates are instant, reduced from 5000ms for faster tests
46- export const REBUILD_INDEX_TIME = 500 ;
41+ // SQLite FTS5 needs time for index rebuilding in tests
42+ // Increased from 500ms to ensure reliable test execution
43+ export const REBUILD_INDEX_TIME = 2500 ;
4744
4845/** Checks server URL and browser URL */
4946export const before = async ( { page } : { page : Page } ) => {
@@ -75,14 +72,36 @@ export async function setTitle(page: Page, title: string) {
7572
7673/** Signs in using an AtomicData.dev test user */
7774export async function signIn ( page : Page ) {
78- await page . click ( 'text=Login' ) ;
79- await expect ( page . locator ( 'text=edit data and sign Commits' ) ) . toBeVisible ( ) ;
75+ // Retry login button click with better stability
76+ let retries = 3 ;
77+ while ( retries > 0 ) {
78+ try {
79+ await page . click ( 'text=Login' , { timeout : 5000 } ) ;
80+ break ;
81+ } catch ( error ) {
82+ retries -- ;
83+ if ( retries === 0 ) throw error ;
84+ await page . waitForTimeout ( 100 * ( 4 - retries ) ) ;
85+ }
86+ }
87+
88+ // Wait for authentication form to be visible
89+ await expect ( page . locator ( 'text=edit data and sign Commits' ) ) . toBeVisible ( { timeout : 10000 } ) ;
90+
8091 // If there are any issues with this agent, try creating a new one https://atomicdata.dev/invites/1
8192 const test_agent =
82- 'eyJzdWJqZWN0IjoiaHR0cHM6Ly9hdG9taWNkYXRhLmRldi9hZ2VudHMvaElNWHFoR3VLSDRkM0QrV1BjYzAwUHVFbldFMEtlY21GWStWbWNVR2tEWT0iLCJwcml2YXRlS2V5IjoiZkx0SDAvY29VY1BleFluNC95NGxFemFKbUJmZTYxQ3lEekUwODJyMmdRQT0ifQ==' ;
93+ 'eyJzdWJqZWN0IjoiaHR0cHM6Ly9hdG9taWNkYXRhLmRldi9hZ2VudHMvaElNWHFoR3VLSDRkM0QrV1BjYzAwUHVFbldFMEtlY21GWStWbWNVR2tEWT0iLCJwcml2YXRlS2V5IjoiZkx0SDAvY29VY1BleFluNC85NGxFemFKbUJmZTYxQ3lEekUwODJyMmdRQT0ifQ==' ;
94+
95+ // Wait for password field to be interactive
96+ await page . waitForSelector ( '#current-password' , { state : 'visible' } ) ;
8397 await page . click ( '#current-password' ) ;
8498 await page . fill ( '#current-password' , test_agent ) ;
85- await expect ( page . locator ( 'text=Edit profile' ) ) . toBeVisible ( ) ;
99+
100+ // Wait for successful authentication
101+ await expect ( page . locator ( 'text=Edit profile' ) ) . toBeVisible ( { timeout : 10000 } ) ;
102+
103+ // Give WebSocket connection time to stabilize
104+ await page . waitForTimeout ( 500 ) ;
86105 await page . goBack ( ) ;
87106}
88107
@@ -108,16 +127,27 @@ export async function newDrive(page: Page) {
108127 currentDialog ( page ) . locator ( 'footer button' , { hasText : 'Create' } ) ,
109128 ) . toBeEnabled ( ) ;
110129
111- const navigationPromise = page . waitForNavigation ( { timeout : 30000 } ) ;
130+ // Click the create button and wait for drive creation to complete
112131 await currentDialog ( page )
113132 . locator ( 'footer button' , { hasText : 'Create' } )
114133 . click ( ) ;
115134
116- await navigationPromise ;
135+ // Wait for the dialog to disappear (indicates the action completed)
136+ await currentDialog ( page ) . waitFor ( { state : 'hidden' , timeout : 30000 } ) ;
137+
138+ // Wait for the URL to change to the new drive (more reliable indicator)
139+ await page . waitForFunction (
140+ ( ) => {
141+ // URL should change from a simple path to include a drive resource ID
142+ const currentUrl = window . location . href ;
143+ return currentUrl . includes ( '/show' ) || currentUrl . includes ( '/collections' ) ||
144+ currentUrl . includes ( '/app' ) && ! currentUrl . endsWith ( '/app' ) ;
145+ } ,
146+ { timeout : 30000 }
147+ ) ;
117148
118149 // Wait for the sidebar to update with the new drive title
119- await expect ( currentDriveTitle ( page ) ) . not . toHaveText ( startDriveName ) ;
120- await expect ( currentDriveTitle ( page ) ) . toHaveText ( driveTitle ) ;
150+ await expect ( currentDriveTitle ( page ) ) . toContainText ( driveTitle , { timeout : 10000 } ) ;
121151 const driveURL = await getCurrentSubject ( page ) ;
122152 expect ( driveURL ) . toContain ( SERVER_URL ) ;
123153
@@ -132,6 +162,7 @@ export async function makeDrivePublic(page: Page) {
132162 publicReadRightLocator ( page ) ,
133163 'The drive was public from the start' ,
134164 ) . not . toBeChecked ( ) ;
165+ await expect ( publicReadRightLocator ( page ) ) . toBeEnabled ( ) ;
135166 await publicReadRightLocator ( page ) . click ( ) ;
136167 await page . locator ( 'text=Save' ) . click ( ) ;
137168 await expect ( page . locator ( 'text="Share settings saved"' ) ) . toBeVisible ( ) ;
@@ -216,8 +247,25 @@ export async function editProfileAndCommit(page: Page) {
216247 ) . toBeVisible ( ) ;
217248 await expect ( page . getByRole ( 'main' ) . getByText ( 'loading' ) ) . not . toBeVisible ( ) ;
218249
219- const navigationPromise = page . waitForNavigation ( { timeout : 5000 } ) ;
220- await page . getByRole ( 'button' , { name : 'Edit profile' } ) . click ( ) ;
250+ // Wait for the page to be fully interactive before clicking
251+ await page . waitForLoadState ( 'networkidle' , { timeout : 10000 } ) ;
252+
253+ const navigationPromise = page . waitForNavigation ( { timeout : 10000 } ) ;
254+
255+ // Retry click with exponential backoff if it fails
256+ let retries = 3 ;
257+
258+ while ( retries > 0 ) {
259+ try {
260+ await page . getByRole ( 'button' , { name : 'Edit profile' } ) . click ( ) ;
261+ break ;
262+ } catch ( error ) {
263+ retries -- ;
264+ if ( retries === 0 ) throw error ;
265+ await page . waitForTimeout ( 100 * ( 4 - retries ) ) ; // 100ms, 200ms, 300ms
266+ }
267+ }
268+
221269 await navigationPromise ;
222270 const advancedButton = page . getByRole ( 'button' , { name : 'advanced' } ) ;
223271 await advancedButton . scrollIntoViewIfNeeded ( ) ;
@@ -243,21 +291,98 @@ export async function fillSearchBox(
243291 } = { } ,
244292) {
245293 const { nth, container, label } = options ;
246- const selector = container ?? page ;
294+ const scope = container ?? page ;
247295
296+ // Open the search dropdown
248297 if ( nth !== undefined ) {
249- await selector
298+ await scope
250299 . getByRole ( 'button' , { name : label ?? placeholder } )
251300 . nth ( nth )
252301 . click ( ) ;
253302 } else {
254- await selector . getByRole ( 'button' , { name : label ?? placeholder } ) . click ( ) ;
303+ await scope . getByRole ( 'button' , { name : label ?? placeholder } ) . click ( ) ;
255304 }
256305
257- await selector . getByPlaceholder ( placeholder ) . fill ( fillText ) ;
306+ // Focus and type
307+ const input = scope . getByPlaceholder ( placeholder ) ;
308+ await input . focus ( ) ;
309+ await input . fill ( fillText ) ;
310+
311+ // Wait for results using multiple strategies
312+ const waitForResults = async ( ) => {
313+ const deadline = Date . now ( ) + 10000 ;
314+
315+ while ( Date . now ( ) < deadline ) {
316+ const hasContainer = await ( scope as any )
317+ . getByTestId ( 'searchbox-results' )
318+ . isVisible ( )
319+ . catch ( ( ) => false ) ;
320+ if ( hasContainer ) return ;
321+
322+ const anyOptionVisible = await ( scope as any )
323+ . getByRole ( 'option' )
324+ . first ( )
325+ . isVisible ( )
326+ . catch ( ( ) => false ) ;
327+ if ( anyOptionVisible ) return ;
328+
329+ const anyListItemVisible = await ( scope as any )
330+ . locator (
331+ 'li[role="option"], [role="menuitem"], [data-test="searchbox-results"] li' ,
332+ )
333+ . first ( )
334+ . isVisible ( )
335+ . catch ( ( ) => false ) ;
336+ if ( anyListItemVisible ) return ;
337+
338+ if ( 'waitForTimeout' in page ) {
339+ await ( page as Page ) . waitForTimeout ( 200 ) ;
340+ }
341+ }
258342
343+ throw new Error ( 'Search results did not appear in time' ) ;
344+ } ;
345+
346+ await waitForResults ( ) ;
347+
348+ // Return a clicker that tries multiple selection strategies
259349 return async ( name : string ) => {
260- await selector . getByTestId ( 'searchbox-results' ) . getByText ( name ) . click ( ) ;
350+ const container = ( scope as any )
351+ . getByTestId ( 'searchbox-results' )
352+ . getByText ( name )
353+ . first ( ) ;
354+
355+ if ( await container . isVisible ( ) . catch ( ( ) => false ) ) {
356+ await container . click ( ) ;
357+
358+ return ;
359+ }
360+
361+ const optionByRole = ( scope as any )
362+ . getByRole ( 'option' , { name, exact : false } )
363+ . first ( ) ;
364+
365+ if ( await optionByRole . isVisible ( ) . catch ( ( ) => false ) ) {
366+ await optionByRole . click ( ) ;
367+
368+ return ;
369+ }
370+
371+ const topOption = ( scope as any ) . getByRole ( 'option' ) . first ( ) ;
372+
373+ if ( await topOption . isVisible ( ) . catch ( ( ) => false ) ) {
374+ await topOption . click ( ) ;
375+
376+ return ;
377+ }
378+
379+ if ( 'keyboard' in page ) {
380+ await ( page as Page ) . keyboard . press ( 'Enter' ) ;
381+
382+ return ;
383+ }
384+
385+ throw new Error ( `Option not found: ${ name } ` ) ;
261386 } ;
262387}
263388
@@ -429,52 +554,62 @@ type CommitFilter = {
429554 // TODO: Add push and delete filters when they're needed.
430555} ;
431556
432- export const waitForCommit = async ( page : Page , filter ?: CommitFilter ) =>
433- page . waitForResponse ( async response => {
434- if (
435- ! response . url ( ) . endsWith ( '/commit' ) ||
436- response . request ( ) . method ( ) !== 'POST'
437- ) {
438- return false ;
439- }
440-
441- const commit = response . request ( ) . postDataJSON ( ) as Record < string , unknown > ;
557+ export const waitForCommit = async (
558+ page : Page ,
559+ filter ?: CommitFilter ,
560+ timeout = 10000 ,
561+ ) =>
562+ page . waitForResponse (
563+ async response => {
564+ if (
565+ ! response . url ( ) . endsWith ( '/commit' ) ||
566+ response . request ( ) . method ( ) !== 'POST'
567+ ) {
568+ return false ;
569+ }
442570
443- const isA = commit [ PROPERTIES . isA ] as string [ ] ;
571+ const commit = response . request ( ) . postDataJSON ( ) as Record <
572+ string ,
573+ unknown
574+ > ;
444575
445- if ( ! isA . includes ( 'https://atomicdata.dev/classes/Commit' ) ) {
446- return false ;
447- }
576+ const isA = commit [ PROPERTIES . isA ] as string [ ] ;
448577
449- // We have a commit and there is no filter so we can stop waiting.
450- if ( ! filter ) {
451- return true ;
452- }
453-
454- if ( filter . set ) {
455- if ( ! ( PROPERTIES . set in commit ) ) {
578+ if ( ! isA . includes ( 'https://atomicdata.dev/classes/Commit' ) ) {
456579 return false ;
457580 }
458581
459- const set = commit [ PROPERTIES . set ] as Record < string , unknown > ;
582+ // We have a commit and there is no filter so we can stop waiting.
583+ if ( ! filter ) {
584+ return true ;
585+ }
460586
461- for ( const [ key , value ] of Object . entries ( filter . set ) ) {
462- if ( ! ( key in set ) ) {
587+ if ( filter . set ) {
588+ if ( ! ( PROPERTIES . set in commit ) ) {
463589 return false ;
464590 }
465591
466- if ( value === anyValue ) {
467- continue ;
468- }
592+ const set = commit [ PROPERTIES . set ] as Record < string , unknown > ;
469593
470- if ( JSON . stringify ( set [ key ] ) !== JSON . stringify ( value ) ) {
471- return false ;
594+ for ( const [ key , value ] of Object . entries ( filter . set ) ) {
595+ if ( ! ( key in set ) ) {
596+ return false ;
597+ }
598+
599+ if ( value === anyValue ) {
600+ continue ;
601+ }
602+
603+ if ( JSON . stringify ( set [ key ] ) !== JSON . stringify ( value ) ) {
604+ return false ;
605+ }
472606 }
473607 }
474- }
475608
476- return true ;
477- } ) ;
609+ return true ;
610+ } ,
611+ { timeout } ,
612+ ) ;
478613
479614export function currentDialog ( page : Page ) {
480615 return page . locator ( 'dialog[data-top-level="true"]' ) ;
0 commit comments