Skip to content

Commit b0f00c4

Browse files
authored
test: fixes pagination and sorting list-view tests due to hydration timing issues (#15925)
# Overview Fixes flaky list view pagination / sorting e2e test failures in slow CI environments caused by Next.js 16 hydration timing issues. <img width="1123" height="536" alt="Screenshot 2026-03-12 at 9 25 25 AM" src="https://github.com/user-attachments/assets/357d968c-5838-4308-adda-cc9b37002a07" /> ## Key Changes - Added `wait(500)` calls at strategic interaction points: - After page loads/reloads before user interactions - After clicks that trigger UI state changes - After navigation actions - After column/drawer toggles - Applied to pagination, per-page limits, list drawer, and column sorting tests - Added CPU throttling (4x slower) to these tests to simulate slow CI and catch regressions ## Design Decisions In slow CI environments, React hydration takes longer to complete. Playwright sees elements as visible and actionable before React event handlers are fully attached, leading to: - Clicks happening before handlers are ready - Drawers/modals appearing briefly then disappearing (hydration mismatch) - Tests timing out before pages become truly interactive The 500ms waits give Next.js 16 enough time to complete hydration before interactions. CPU throttling was added to replicate slow CI conditions locally and prevent future regressions.
1 parent ef507a6 commit b0f00c4

File tree

4 files changed

+87
-15
lines changed

4 files changed

+87
-15
lines changed

test/access-control/e2e.spec.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,8 @@ describe('Access Control', () => {
324324
const regression1URL = new AdminUrlUtil(serverURL, 'regression1')
325325
await page.goto(regression1URL.list)
326326

327-
await page.locator('.cell-id a').first().click()
328-
await page.waitForURL(`**/collections/regression1/**`)
327+
const href = await page.locator('.cell-id a').first().getAttribute('href')
328+
await page.goto(`${serverURL}${href}`)
329329

330330
await ensureRegression1FieldsHaveCorrectAccess()
331331

@@ -368,8 +368,9 @@ describe('Access Control', () => {
368368
const regression2URL = new AdminUrlUtil(serverURL, 'regression2')
369369
await page.goto(regression2URL.list)
370370

371-
await page.locator('.cell-id a').first().click()
372-
await page.waitForURL(`**/collections/regression2/**`)
371+
const href = await page.locator('.cell-id a').first().getAttribute('href')
372+
await page.goto(`${serverURL}${href}`)
373+
await wait(500)
373374

374375
await ensureRegression2FieldsHaveCorrectAccess()
375376

@@ -717,8 +718,8 @@ describe('Access Control', () => {
717718
await wait(1000)
718719

719720
// Ensure admin user cannot unlock other users
720-
await adminUserRow.locator('.cell-id a').click()
721-
await page.waitForURL(`**/collections/users/**`)
721+
const adminUserHref = await adminUserRow.locator('.cell-id a').getAttribute('href')
722+
await page.goto(`${serverURL}${adminUserHref}`)
722723

723724
const unlockButton = page.locator('#force-unlock')
724725
await expect(unlockButton).toBeVisible()
@@ -731,8 +732,8 @@ describe('Access Control', () => {
731732
await wait(1000)
732733

733734
// Ensure non-admin user cannot see unlock button
734-
await nonAdminUserRow.locator('.cell-id a').click()
735-
await page.waitForURL(`**/collections/users/**`)
735+
const nonAdminUserHref = await nonAdminUserRow.locator('.cell-id a').getAttribute('href')
736+
await page.goto(`${serverURL}${nonAdminUserHref}`)
736737
await expect(page.locator('#force-unlock')).toBeHidden()
737738
})
738739
})

test/access-control/payload-types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ export interface Config {
166166
'read-not-update-global': ReadNotUpdateGlobalSelect<false> | ReadNotUpdateGlobalSelect<true>;
167167
};
168168
locale: null;
169+
widgets: {
170+
collections: CollectionsWidget;
171+
};
169172
user: User | PublicUser | AuthCollection;
170173
jobs: {
171174
tasks: unknown;
@@ -1916,6 +1919,16 @@ export interface ReadNotUpdateGlobalSelect<T extends boolean = true> {
19161919
createdAt?: T;
19171920
globalType?: T;
19181921
}
1922+
/**
1923+
* This interface was referenced by `Config`'s JSON-Schema
1924+
* via the `definition` "collections_widget".
1925+
*/
1926+
export interface CollectionsWidget {
1927+
data?: {
1928+
[k: string]: unknown;
1929+
};
1930+
width: 'full';
1931+
}
19191932
/**
19201933
* This interface was referenced by `Config`'s JSON-Schema
19211934
* via the `definition` "auth".

test/admin/e2e/list-view/e2e.spec.ts

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1519,7 +1519,9 @@ describe('List View', () => {
15191519
})
15201520

15211521
await page.goto(postsUrl.list)
1522-
await expect(page.locator('.per-page .per-page__base-button')).toContainText('Per Page: 5')
1522+
await expect
1523+
.poll(async () => await page.locator('.per-page .per-page__base-button').textContent())
1524+
.toContain('Per Page: 5')
15231525
await expect(page.locator(tableRowLocator)).toHaveCount(5)
15241526
})
15251527

@@ -1531,9 +1533,17 @@ describe('List View', () => {
15311533
})
15321534

15331535
await page.goto(postsUrl.list)
1536+
1537+
await wait(1000)
1538+
1539+
await expect
1540+
.poll(async () => await page.locator('.per-page .popup-button').isVisible())
1541+
.toBe(true)
1542+
15341543
await page.locator('.per-page .popup-button').click()
1544+
await wait(500)
15351545
const options = page.locator('.popup__content button.per-page__button')
1536-
await expect(options).toHaveCount(3)
1546+
await expect.poll(async () => await options.count()).toBe(3)
15371547
await expect(options.nth(0)).toContainText('5')
15381548
await expect(options.nth(1)).toContainText('10')
15391549
await expect(options.nth(2)).toContainText('15')
@@ -1547,15 +1557,22 @@ describe('List View', () => {
15471557
})
15481558

15491559
await page.reload()
1550-
await expect(page.locator(tableRowLocator)).toHaveCount(5)
1560+
1561+
await wait(1000)
1562+
1563+
await expect.poll(async () => await page.locator(tableRowLocator).count()).toBe(5)
15511564
await expect(page.locator('.page-controls__page-info')).toHaveText('1-5 of 6')
15521565
await expect(page.locator('.per-page')).toContainText('Per Page: 5')
15531566

1567+
await wait(500)
1568+
15541569
await goToNextPage(page)
1555-
await expect(page.locator(tableRowLocator)).toHaveCount(1)
1570+
await wait(500)
1571+
await expect.poll(async () => await page.locator(tableRowLocator).count()).toBe(1)
15561572

15571573
await goToPreviousPage(page)
1558-
await expect(page.locator(tableRowLocator)).toHaveCount(5)
1574+
await wait(500)
1575+
await expect.poll(async () => await page.locator(tableRowLocator).count()).toBe(5)
15591576
})
15601577

15611578
test('should paginate without resetting selected limit', async () => {
@@ -1566,22 +1583,32 @@ describe('List View', () => {
15661583
})
15671584

15681585
await page.reload()
1586+
1587+
await wait(1000)
1588+
15691589
const tableItems = page.locator(tableRowLocator)
1570-
await expect(tableItems).toHaveCount(5)
1590+
await expect.poll(async () => await tableItems.count()).toBe(5)
15711591
await expect(page.locator('.page-controls__page-info')).toHaveText('1-5 of 16')
15721592
await expect(page.locator('.per-page')).toContainText('Per Page: 5')
1593+
1594+
await wait(500)
1595+
15731596
await page.locator('.per-page .popup-button').click()
15741597

1598+
await wait(500)
1599+
15751600
await page
15761601
.locator('.popup__content button.per-page__button', {
15771602
hasText: '15',
15781603
})
15791604
.click()
1605+
await wait(500)
15801606

15811607
await expect(tableItems).toHaveCount(15)
15821608
await expect(page.locator('.per-page .per-page__base-button')).toContainText('Per Page: 15')
15831609

15841610
await goToNextPage(page)
1611+
await wait(500)
15851612
await expect(tableItems).toHaveCount(1)
15861613
await expect(page.locator('.per-page')).toContainText('Per Page: 15') // ensure this hasn't changed
15871614
await expect(page.locator('.page-controls__page-info')).toHaveText('16-16 of 16')
@@ -1594,6 +1621,8 @@ describe('List View', () => {
15941621

15951622
await page.goto(noTimestampsUrl.list)
15961623

1624+
await wait(1000)
1625+
15971626
await page.locator('.per-page .popup-button').click()
15981627
await page.getByRole('button', { name: '5', exact: true }).click()
15991628
await page.waitForURL(/limit=5/)
@@ -1627,11 +1656,16 @@ describe('List View', () => {
16271656

16281657
await page.goto(withListViewUrl.list)
16291658

1659+
await wait(1000)
1660+
16301661
// Open the list drawer via the "Select posts" button
16311662
const selectButton = page.locator('button:has-text("Select posts")')
16321663
await selectButton.waitFor({ state: 'visible' })
1664+
16331665
await selectButton.click()
16341666

1667+
await wait(1000)
1668+
16351669
const listDrawer = page.locator('.list-drawer.drawer--is-open')
16361670
await listDrawer.waitFor({ state: 'visible' })
16371671
await expect(listDrawer).toBeVisible()
@@ -1792,12 +1826,19 @@ describe('List View', () => {
17921826
})
17931827

17941828
await page.goto(postsUrl.list)
1829+
1830+
await wait(1000)
1831+
17951832
await expect(page.locator(tableRowLocator).first()).toBeVisible()
17961833

17971834
// sort by title
17981835
const sortButton = page.locator('#heading-title button.sort-column__asc')
17991836
await sortButton.waitFor({ state: 'visible' })
1837+
18001838
await sortButton.click()
1839+
1840+
await wait(1000)
1841+
18011842
await page
18021843
.locator('#heading-title button.sort-column__asc.sort-column--active')
18031844
.waitFor({ state: 'visible' })
@@ -1806,6 +1847,8 @@ describe('List View', () => {
18061847
// enable a column that is _not_ part of this collection's default columns
18071848
await toggleColumn(page, { columnLabel: 'Status', targetState: 'on', columnName: '_status' })
18081849

1850+
await wait(500)
1851+
18091852
await page.locator('#heading-_status').waitFor({ state: 'visible' })
18101853

18111854
const columnAfterSort = page.locator(
@@ -1824,12 +1867,14 @@ describe('List View', () => {
18241867
targetState: 'on',
18251868
columnName: 'wavelengths',
18261869
})
1870+
await wait(500)
18271871

18281872
await toggleColumn(page, {
18291873
columnLabel: 'Select Field',
18301874
targetState: 'on',
18311875
columnName: 'selectField',
18321876
})
1877+
await wait(500)
18331878

18341879
// check that the cells have the classes added per value selected
18351880
await expect(
@@ -1849,7 +1894,7 @@ describe('List View', () => {
18491894
await page.waitForURL(/sort=-title/)
18501895

18511896
// allow time for components to re-render
1852-
await wait(100)
1897+
await wait(500)
18531898

18541899
// ensure the column is still visible
18551900
const columnAfterSecondSort = page.locator(

test/admin/payload-types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ export interface Config {
174174
settings: SettingsSelect<false> | SettingsSelect<true>;
175175
};
176176
locale: 'es' | 'en';
177+
widgets: {
178+
collections: CollectionsWidget;
179+
};
177180
user: User;
178181
jobs: {
179182
tasks: unknown;
@@ -1548,6 +1551,16 @@ export interface SettingsSelect<T extends boolean = true> {
15481551
createdAt?: T;
15491552
globalType?: T;
15501553
}
1554+
/**
1555+
* This interface was referenced by `Config`'s JSON-Schema
1556+
* via the `definition` "collections_widget".
1557+
*/
1558+
export interface CollectionsWidget {
1559+
data?: {
1560+
[k: string]: unknown;
1561+
};
1562+
width: 'full';
1563+
}
15511564
/**
15521565
* This interface was referenced by `Config`'s JSON-Schema
15531566
* via the `definition` "auth".

0 commit comments

Comments
 (0)