Skip to content

Commit a07cf47

Browse files
authored
test: deflake sort e2e (#14746)
The sort e2e test suite was flaky in both CI and locally. Reference: https://github.com/payloadcms/payload/actions/runs/19707665246/job/56460054510?pr=14117
1 parent cb3a078 commit a07cf47

9 files changed

Lines changed: 296 additions & 160 deletions

File tree

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1616,12 +1616,12 @@ describe('List View', () => {
16161616
test('should sort', async () => {
16171617
await page.reload()
16181618

1619-
await sortColumn(page, { fieldPath: 'number', fieldLabel: 'Number', targetState: 'asc' })
1619+
await sortColumn(page, { fieldPath: 'number', targetState: 'asc' })
16201620

16211621
await expect(page.locator('.row-1 .cell-number')).toHaveText('1')
16221622
await expect(page.locator('.row-2 .cell-number')).toHaveText('2')
16231623

1624-
await sortColumn(page, { fieldPath: 'number', fieldLabel: 'Number', targetState: 'desc' })
1624+
await sortColumn(page, { fieldPath: 'number', targetState: 'desc' })
16251625

16261626
await expect(page.locator('.row-1 .cell-number')).toHaveText('2')
16271627
await expect(page.locator('.row-2 .cell-number')).toHaveText('1')
@@ -1638,7 +1638,6 @@ describe('List View', () => {
16381638

16391639
await sortColumn(page, {
16401640
fieldPath: 'namedGroup.someTextField',
1641-
fieldLabel: 'Named Group > Some Text Field',
16421641
targetState: 'asc',
16431642
})
16441643

@@ -1652,7 +1651,6 @@ describe('List View', () => {
16521651

16531652
await sortColumn(page, {
16541653
fieldPath: 'namedGroup.someTextField',
1655-
fieldLabel: 'Named Group > Some Text Field',
16561654
targetState: 'desc',
16571655
})
16581656

@@ -1676,7 +1674,6 @@ describe('List View', () => {
16761674

16771675
await sortColumn(page, {
16781676
fieldPath: 'namedTab.nestedTextFieldInNamedTab',
1679-
fieldLabel: 'Named Tab > Nested Text Field In Named Tab',
16801677
targetState: 'asc',
16811678
})
16821679

@@ -1690,7 +1687,6 @@ describe('List View', () => {
16901687

16911688
await sortColumn(page, {
16921689
fieldPath: 'namedTab.nestedTextFieldInNamedTab',
1693-
fieldLabel: 'Named Tab > Nested Text Field In Named Tab',
16941690
targetState: 'desc',
16951691
})
16961692

test/group-by/e2e.spec.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,6 @@ test.describe('Group By', () => {
343343
const table1 = page.locator('.table-wrap').first()
344344

345345
await sortColumn(page, {
346-
fieldLabel: 'Title',
347346
fieldPath: 'title',
348347
scope: table1,
349348
targetState: 'asc',
@@ -365,7 +364,6 @@ test.describe('Group By', () => {
365364
await expect(table2Titles.nth(1)).toHaveText(table2AscOrder[1] || '')
366365

367366
await sortColumn(page, {
368-
fieldLabel: 'Title',
369367
fieldPath: 'title',
370368
scope: table1,
371369
targetState: 'desc',
@@ -616,7 +614,6 @@ test.describe('Group By', () => {
616614
const secondTableRows = secondTable.locator('tbody tr')
617615

618616
await sortColumn(page, {
619-
fieldLabel: 'Title',
620617
fieldPath: 'title',
621618
scope: firstTable,
622619
targetState: 'asc',
@@ -661,7 +658,6 @@ test.describe('Group By', () => {
661658
const secondTableRows = secondTable.locator('tbody tr')
662659

663660
await sortColumn(page, {
664-
fieldLabel: 'Title',
665661
fieldPath: 'title',
666662
scope: firstTable,
667663
targetState: 'asc',

test/helpers/e2e/columns/sortColumn.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,30 @@ import { expect } from '@playwright/test'
99
export const sortColumn = async (
1010
page: Page,
1111
options: {
12-
fieldLabel: string
1312
fieldPath: string
1413
/**
15-
* Scope the sorting to a specific scope. If not provided, will search the whole page for the column heading.
14+
* Scope the sorting to a specific DOM tree.
15+
* If not provided, will search the whole page for the column heading.
1616
*/
1717
scope?: Locator
1818
targetState: 'asc' | 'desc'
1919
},
2020
) => {
2121
const pathAsClassName = options.fieldPath.replace(/\./g, '__')
22-
const field = (options.scope || page).locator(`#heading-${pathAsClassName}`)
22+
const columnHeading = (options.scope || page).locator(`#heading-${pathAsClassName}`)
2323

24-
const upChevron = field.locator('button.sort-column__asc')
25-
const downChevron = field.locator('button.sort-column__desc')
24+
const upChevron = columnHeading.locator('button.sort-column__asc')
25+
const downChevron = columnHeading.locator('button.sort-column__desc')
2626

2727
if (options.targetState === 'asc') {
2828
await upChevron.click()
29-
await expect(field.locator('button.sort-column__asc.sort-column--active')).toBeVisible()
29+
await expect(columnHeading.locator('button.sort-column__asc.sort-column--active')).toBeVisible()
3030
await page.waitForURL(() => page.url().includes(`sort=${options.fieldPath}`))
3131
} else if (options.targetState === 'desc') {
3232
await downChevron.click()
33-
await expect(field.locator('button.sort-column__desc.sort-column--active')).toBeVisible()
33+
await expect(
34+
columnHeading.locator('button.sort-column__desc.sort-column--active'),
35+
).toBeVisible()
3436
await page.waitForURL(() => page.url().includes(`sort=-${options.fieldPath}`))
3537
}
3638
}

test/helpers/e2e/sort/moveRow.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { Locator, Page } from 'playwright'
2+
3+
import { expect } from 'playwright/test'
4+
5+
export async function moveRow(
6+
page: Page,
7+
{
8+
fromIndex,
9+
toIndex,
10+
expected = 'success',
11+
scope,
12+
}: {
13+
expected?: 'success' | 'warning'
14+
fromIndex: number
15+
/**
16+
* Scope the sorting to a specific table in the DOM.
17+
* Useful when there are multiple sortable tables on the page.
18+
* If not provided, will search the first table on the page.
19+
*/
20+
scope?: Locator
21+
toIndex: number
22+
},
23+
) {
24+
const table = (scope || page).locator(`tbody`)
25+
await table.scrollIntoViewIfNeeded()
26+
27+
const dragHandle = table.locator(`.sort-row`)
28+
const source = dragHandle.nth(fromIndex)
29+
const target = dragHandle.nth(toIndex)
30+
31+
const sourceBox = await source.boundingBox()
32+
const targetBox = await target.boundingBox()
33+
34+
if (!sourceBox || !targetBox) {
35+
throw new Error(
36+
`Could not find elements to DnD. Probably the dndkit animation is not finished. Try increasing the timeout`,
37+
)
38+
}
39+
40+
// steps is important: move slightly to trigger the drag sensor of DnD-kit
41+
await page.mouse.move(sourceBox.x + sourceBox.width / 2, sourceBox.y + sourceBox.height / 2, {
42+
steps: 10,
43+
})
44+
45+
await page.mouse.down()
46+
await page.mouse.move(targetBox.x + targetBox.width / 2, targetBox.y + targetBox.height / 2, {
47+
steps: 10,
48+
})
49+
50+
await page.mouse.up()
51+
52+
await page.waitForTimeout(400) // dndkit animation
53+
54+
if (expected === 'warning') {
55+
const toast = page.locator('.payload-toast-item.toast-warning')
56+
await expect(toast).toHaveText(
57+
'To reorder the rows you must first sort them by the "Order" column',
58+
)
59+
}
60+
}

test/helpers/executePromises.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44
export async function executePromises<T extends Array<() => Promise<any>>>(
55
promiseFns: T,
6-
parallel: boolean,
6+
parallel?: boolean,
77
): Promise<{ [K in keyof T]: Awaited<ReturnType<T[K]>> }> {
88
if (parallel) {
99
// Parallel execution with Promise.all and maintain proper typing

test/sort/config.ts

Lines changed: 7 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1-
import type { CollectionSlug, Payload } from 'payload'
2-
31
import { fileURLToPath } from 'node:url'
42
import path from 'path'
5-
import { wait } from 'payload/shared'
63

74
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
8-
import { devUser } from '../credentials.js'
95
import { DefaultSortCollection } from './collections/DefaultSort/index.js'
106
import { DraftsCollection } from './collections/Drafts/index.js'
117
import { LocalizedCollection } from './collections/Localized/index.js'
12-
import { NonUniqueSortCollection, nonUniqueSortSlug } from './collections/NonUniqueSort/index.js'
8+
import { NonUniqueSortCollection } from './collections/NonUniqueSort/index.js'
139
import { OrderableCollection } from './collections/Orderable/index.js'
1410
import { OrderableJoinCollection } from './collections/OrderableJoin/index.js'
1511
import { PostsCollection } from './collections/Posts/index.js'
12+
import { seed, seedSortable } from './seed.js'
13+
1614
const filename = fileURLToPath(import.meta.url)
1715
const dirname = path.dirname(filename)
1816

@@ -37,6 +35,7 @@ export default buildConfigWithDefaults({
3735
method: 'post',
3836
handler: async (req) => {
3937
await seedSortable(req.payload)
38+
4039
return new Response(JSON.stringify({ success: true }), {
4140
headers: { 'Content-Type': 'application/json' },
4241
status: 200,
@@ -50,70 +49,11 @@ export default buildConfigWithDefaults({
5049
defaultLocale: 'en',
5150
},
5251
onInit: async (payload) => {
53-
await payload.create({
54-
collection: 'users',
55-
data: {
56-
email: devUser.email,
57-
password: devUser.password,
58-
},
59-
})
60-
await seedSortable(payload)
52+
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
53+
await seed(payload)
54+
}
6155
},
6256
typescript: {
6357
outputFile: path.resolve(dirname, 'payload-types.ts'),
6458
},
6559
})
66-
67-
export async function createData(
68-
payload: Payload,
69-
collection: CollectionSlug,
70-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
71-
data: Record<string, any>[],
72-
) {
73-
for (const item of data) {
74-
await payload.create({ collection, data: item })
75-
}
76-
}
77-
78-
async function seedSortable(payload: Payload) {
79-
await payload.delete({ collection: 'orderable', where: {} })
80-
await payload.delete({ collection: 'orderable-join', where: {} })
81-
82-
const joinA = await payload.create({ collection: 'orderable-join', data: { title: 'Join A' } })
83-
84-
await createData(payload, 'orderable', [
85-
{ title: 'A', orderableField: joinA.id },
86-
{ title: 'B', orderableField: joinA.id },
87-
{ title: 'C', orderableField: joinA.id },
88-
{ title: 'D', orderableField: joinA.id },
89-
])
90-
91-
await payload.create({ collection: 'orderable-join', data: { title: 'Join B' } })
92-
93-
// Create 10 items to be sorted by non-unique field
94-
for (const i of Array.from({ length: 10 }, (_, index) => index)) {
95-
let order = 1
96-
97-
if (i > 3) {
98-
order = 2
99-
} else if (i > 6) {
100-
order = 3
101-
}
102-
103-
await payload.create({
104-
collection: nonUniqueSortSlug,
105-
data: {
106-
title: `Post ${i}`,
107-
order,
108-
},
109-
})
110-
111-
// Wait 2 seconds to guarantee that the createdAt date is different
112-
// await wait(2000)
113-
}
114-
115-
return new Response(JSON.stringify({ success: true }), {
116-
headers: { 'Content-Type': 'application/json' },
117-
status: 200,
118-
})
119-
}

0 commit comments

Comments
 (0)