Skip to content

Commit f2abc80

Browse files
authored
test: deflakes blocks e2e (#11640)
The blocks e2e tests were flaky due to how we conditionally render fields as they enter the viewport. This prevented Playwright from every reaching the target element when running `locator.scrollIntoViewIfNeeded()`. This is especially flaky on pages with many fields because the page size would continually grow as it was scrolled. To fix this there are new `scrollEntirePage` and `waitForPageStability` helpers. Together, these will ensure that all fields are rendered and fully loaded before we start testing. An early attempt at this was made via `page.mouse.wheel(0, 1750)`, but this is an arbitrary pixel value that is error prone and is not future proof. These tests were also flaky by an attempt to trigger a form state action before it was ready to receive events. The fix here is to disable any buttons while the form is initializing and let Playwright wait for an interactive state.
1 parent 88eeeaa commit f2abc80

File tree

7 files changed

+159
-117
lines changed

7 files changed

+159
-117
lines changed
Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,4 @@
1-
@mixin btn-reset {
2-
border: 0;
3-
background: none;
4-
box-shadow: none;
5-
border-radius: 0;
6-
padding: 0;
7-
color: currentColor;
8-
cursor: pointer;
9-
}
10-
11-
#field-customBlocks {
12-
margin-bottom: var(--base);
13-
14-
.blocks-field__drawer-toggler {
15-
display: none;
16-
}
17-
}
18-
191
.custom-blocks-field-management {
20-
&__blocks-grid {
21-
display: grid;
22-
grid-template-columns: repeat(3, 1fr);
23-
gap: calc(var(--base) * 2);
24-
}
25-
26-
&__block-button {
27-
@include btn-reset;
28-
29-
border: 1px solid var(--theme-border-color);
30-
width: 100%;
31-
padding: 25px 10px;
32-
33-
&:hover {
34-
border-color: var(--theme-elevation-400);
35-
}
36-
}
37-
38-
&__replace-block-button {
39-
margin-top: calc(var(--base) * 1.5);
40-
color: var(--theme-bg);
41-
background: var(--theme-text);
42-
}
2+
display: flex;
3+
gap: var(--base);
434
}
Lines changed: 60 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useField, useForm } from '@payloadcms/ui'
3+
import { Button, useField, useForm } from '@payloadcms/ui'
44
import * as React from 'react'
55

66
import './index.scss'
@@ -10,81 +10,75 @@ const baseClass = 'custom-blocks-field-management'
1010
const blocksPath = 'customBlocks'
1111

1212
export const AddCustomBlocks: React.FC<any> = (props) => {
13-
const { addFieldRow, replaceFieldRow } = useForm()
13+
const { addFieldRow, initializing, replaceFieldRow } = useForm()
1414
const field = useField<number>({ path: blocksPath })
1515
const { value } = field
1616

1717
const schemaPath = props.schemaPath.replace(`.${props.field.name}`, `.${blocksPath}`)
1818

1919
return (
2020
<div className={baseClass}>
21-
<div className={`${baseClass}__blocks-grid`}>
22-
<button
23-
className={`${baseClass}__block-button`}
24-
onClick={() => {
25-
addFieldRow({
26-
blockType: 'block-1',
27-
path: blocksPath,
28-
schemaPath,
29-
subFieldState: {
30-
block1Title: {
31-
initialValue: 'Block 1: Prefilled Title',
32-
valid: true,
33-
value: 'Block 1: Prefilled Title',
34-
},
21+
<Button
22+
disabled={initializing}
23+
onClick={() => {
24+
addFieldRow({
25+
blockType: 'block-1',
26+
path: blocksPath,
27+
schemaPath,
28+
subFieldState: {
29+
block1Title: {
30+
initialValue: 'Block 1: Prefilled Title',
31+
valid: true,
32+
value: 'Block 1: Prefilled Title',
3533
},
36-
})
37-
}}
38-
type="button"
39-
>
40-
Add Block 1
41-
</button>
42-
43-
<button
44-
className={`${baseClass}__block-button`}
45-
onClick={() => {
46-
addFieldRow({
47-
blockType: 'block-2',
48-
path: blocksPath,
49-
schemaPath,
50-
subFieldState: {
51-
block2Title: {
52-
initialValue: 'Block 2: Prefilled Title',
53-
valid: true,
54-
value: 'Block 2: Prefilled Title',
55-
},
34+
},
35+
})
36+
}}
37+
type="button"
38+
>
39+
Add Block 1
40+
</Button>
41+
<Button
42+
disabled={initializing}
43+
onClick={() => {
44+
addFieldRow({
45+
blockType: 'block-2',
46+
path: blocksPath,
47+
schemaPath,
48+
subFieldState: {
49+
block2Title: {
50+
initialValue: 'Block 2: Prefilled Title',
51+
valid: true,
52+
value: 'Block 2: Prefilled Title',
5653
},
57-
})
58-
}}
59-
type="button"
60-
>
61-
Add Block 2
62-
</button>
63-
</div>
64-
65-
<div>
66-
<button
67-
className={`${baseClass}__block-button ${baseClass}__replace-block-button`}
68-
onClick={() =>
69-
replaceFieldRow({
70-
blockType: 'block-1',
71-
path: blocksPath,
72-
rowIndex: value,
73-
schemaPath,
74-
subFieldState: {
75-
block1Title: {
76-
initialValue: 'REPLACED BLOCK',
77-
valid: true,
78-
value: 'REPLACED BLOCK',
79-
},
54+
},
55+
})
56+
}}
57+
type="button"
58+
>
59+
Add Block 2
60+
</Button>
61+
<Button
62+
disabled={initializing}
63+
onClick={() =>
64+
replaceFieldRow({
65+
blockType: 'block-1',
66+
path: blocksPath,
67+
rowIndex: value,
68+
schemaPath,
69+
subFieldState: {
70+
block1Title: {
71+
initialValue: 'REPLACED BLOCK',
72+
valid: true,
73+
value: 'REPLACED BLOCK',
8074
},
81-
})
82-
}
83-
type="button"
84-
>
85-
Replace Block {value}
86-
</button>
87-
</div>
75+
},
76+
})
77+
}
78+
type="button"
79+
>
80+
Replace Block {value}
81+
</Button>
8882
</div>
8983
)
9084
}

test/fields/collections/Blocks/e2e.spec.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { expect, test } from '@playwright/test'
44
import { addBlock } from 'helpers/e2e/addBlock.js'
55
import { openBlocksDrawer } from 'helpers/e2e/openBlocksDrawer.js'
66
import { reorderBlocks } from 'helpers/e2e/reorderBlocks.js'
7+
import { scrollEntirePage } from 'helpers/e2e/scrollEntirePage.js'
78
import path from 'path'
89
import { fileURLToPath } from 'url'
910

@@ -329,20 +330,16 @@ describe('Block fields', () => {
329330
test('should add 2 new block rows', async () => {
330331
await page.goto(url.create)
331332

333+
await scrollEntirePage(page)
334+
332335
await page
333336
.locator('.custom-blocks-field-management')
334337
.getByRole('button', { name: 'Add Block 1' })
335338
.click()
336339

337-
const customBlocks = page.locator(
338-
'#field-customBlocks input[name="customBlocks.0.block1Title"]',
339-
)
340-
341-
await page.mouse.wheel(0, 1750)
342-
343-
await customBlocks.scrollIntoViewIfNeeded()
344-
345-
await expect(customBlocks).toHaveValue('Block 1: Prefilled Title')
340+
await expect(
341+
page.locator('#field-customBlocks input[name="customBlocks.0.block1Title"]'),
342+
).toHaveValue('Block 1: Prefilled Title')
346343

347344
await page
348345
.locator('.custom-blocks-field-management')

test/fields/payload-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,7 @@ export interface CodeField {
10861086
json?: string | null;
10871087
html?: string | null;
10881088
css?: string | null;
1089+
codeWithPadding?: string | null;
10891090
updatedAt: string;
10901091
createdAt: string;
10911092
}
@@ -2853,6 +2854,7 @@ export interface CodeFieldsSelect<T extends boolean = true> {
28532854
json?: T;
28542855
html?: T;
28552856
css?: T;
2857+
codeWithPadding?: T;
28562858
updatedAt?: T;
28572859
createdAt?: T;
28582860
}

test/helpers/e2e/scrollEntirePage.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { Page } from '@playwright/test'
2+
3+
import { waitForPageStability } from './waitForPageStability.js'
4+
5+
/**
6+
* Scroll to bottom of the page continuously until no new content is loaded.
7+
* This is needed because we conditionally render fields as they enter the viewport.
8+
* This will ensure that all fields are rendered and fully loaded before we start testing.
9+
* Without this step, Playwright's `locator.scrollIntoView()` might not work as expected.
10+
* @param page - Playwright page object
11+
* @returns Promise<void>
12+
*/
13+
export const scrollEntirePage = async (page: Page) => {
14+
let previousHeight = await page.evaluate(() => document.body.scrollHeight)
15+
16+
while (true) {
17+
await page.evaluate(() => {
18+
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })
19+
})
20+
21+
// Wait for the page to stabilize after scrolling
22+
await waitForPageStability({ page })
23+
24+
// Get the new page height after stability check
25+
const newHeight = await page.evaluate(() => document.body.scrollHeight)
26+
27+
// Stop if the height hasn't changed, meaning no new content was loaded
28+
if (newHeight === previousHeight) {
29+
break
30+
}
31+
32+
previousHeight = newHeight
33+
}
34+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { Page } from '@playwright/test'
2+
3+
/**
4+
* Checks if the page is stable by continually polling until the page size remains constant in size and there are no loading shimmers.
5+
* A page is considered stable if it passes this test multiple times.
6+
* This will ensure that the page won't unexpectedly change while testing.
7+
* @param page - Playwright page object
8+
* @param intervalMs - Polling interval in milliseconds
9+
* @param stableChecksRequired - Number of stable checks required to consider page stable
10+
* @returns Promise<void>
11+
*/
12+
export const waitForPageStability = async ({
13+
page,
14+
interval = 1000,
15+
stableChecksRequired = 3,
16+
}: {
17+
interval?: number
18+
page: Page
19+
stableChecksRequired?: number
20+
}) => {
21+
await page.waitForLoadState('networkidle') // Wait for network to be idle
22+
23+
await page.waitForFunction(
24+
async ({ interval, stableChecksRequired }) => {
25+
return new Promise((resolve) => {
26+
let previousHeight = document.body.scrollHeight
27+
let stableChecks = 0
28+
29+
const checkStability = () => {
30+
const currentHeight = document.body.scrollHeight
31+
const loadingShimmers = document.querySelectorAll('.shimmer-effect')
32+
const pageSizeChanged = currentHeight !== previousHeight
33+
34+
if (!pageSizeChanged && loadingShimmers.length === 0) {
35+
stableChecks++ // Increment stability count
36+
} else {
37+
stableChecks = 0 // Reset stability count if page changes
38+
}
39+
40+
previousHeight = currentHeight
41+
42+
if (stableChecks >= stableChecksRequired) {
43+
resolve(true) // Only resolve after multiple stable checks
44+
} else {
45+
setTimeout(checkStability, interval) // Poll again
46+
}
47+
}
48+
49+
checkStability()
50+
})
51+
},
52+
{ interval, stableChecksRequired },
53+
)
54+
}

tsconfig.base.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
}
3232
],
3333
"paths": {
34-
"@payload-config": ["./test/_community/config.ts"],
34+
"@payload-config": ["./test/fields/config.ts"],
3535
"@payloadcms/admin-bar": ["./packages/admin-bar/src"],
3636
"@payloadcms/live-preview": ["./packages/live-preview/src"],
3737
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],

0 commit comments

Comments
 (0)