From f2abc80a008313e61a68acfe6da5736fd2632ace Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Tue, 11 Mar 2025 22:49:06 -0400 Subject: [PATCH] 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. --- .../components/AddCustomBlocks/index.scss | 43 +----- .../components/AddCustomBlocks/index.tsx | 126 +++++++++--------- test/fields/collections/Blocks/e2e.spec.ts | 15 +-- test/fields/payload-types.ts | 2 + test/helpers/e2e/scrollEntirePage.ts | 34 +++++ test/helpers/e2e/waitForPageStability.ts | 54 ++++++++ tsconfig.base.json | 2 +- 7 files changed, 159 insertions(+), 117 deletions(-) create mode 100644 test/helpers/e2e/scrollEntirePage.ts create mode 100644 test/helpers/e2e/waitForPageStability.ts diff --git a/test/fields/collections/Blocks/components/AddCustomBlocks/index.scss b/test/fields/collections/Blocks/components/AddCustomBlocks/index.scss index c1d67a73a..a9979a867 100644 --- a/test/fields/collections/Blocks/components/AddCustomBlocks/index.scss +++ b/test/fields/collections/Blocks/components/AddCustomBlocks/index.scss @@ -1,43 +1,4 @@ -@mixin btn-reset { - border: 0; - background: none; - box-shadow: none; - border-radius: 0; - padding: 0; - color: currentColor; - cursor: pointer; -} - -#field-customBlocks { - margin-bottom: var(--base); - - .blocks-field__drawer-toggler { - display: none; - } -} - .custom-blocks-field-management { - &__blocks-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: calc(var(--base) * 2); - } - - &__block-button { - @include btn-reset; - - border: 1px solid var(--theme-border-color); - width: 100%; - padding: 25px 10px; - - &:hover { - border-color: var(--theme-elevation-400); - } - } - - &__replace-block-button { - margin-top: calc(var(--base) * 1.5); - color: var(--theme-bg); - background: var(--theme-text); - } + display: flex; + gap: var(--base); } diff --git a/test/fields/collections/Blocks/components/AddCustomBlocks/index.tsx b/test/fields/collections/Blocks/components/AddCustomBlocks/index.tsx index 2c9b62330..9a1cfa20d 100644 --- a/test/fields/collections/Blocks/components/AddCustomBlocks/index.tsx +++ b/test/fields/collections/Blocks/components/AddCustomBlocks/index.tsx @@ -1,6 +1,6 @@ 'use client' -import { useField, useForm } from '@payloadcms/ui' +import { Button, useField, useForm } from '@payloadcms/ui' import * as React from 'react' import './index.scss' @@ -10,7 +10,7 @@ const baseClass = 'custom-blocks-field-management' const blocksPath = 'customBlocks' export const AddCustomBlocks: React.FC = (props) => { - const { addFieldRow, replaceFieldRow } = useForm() + const { addFieldRow, initializing, replaceFieldRow } = useForm() const field = useField({ path: blocksPath }) const { value } = field @@ -18,73 +18,67 @@ export const AddCustomBlocks: React.FC = (props) => { return (
-
- - - + -
- -
- + -
+ }, + }) + } + type="button" + > + Replace Block {value} +
) } diff --git a/test/fields/collections/Blocks/e2e.spec.ts b/test/fields/collections/Blocks/e2e.spec.ts index d2a47bad1..449a3a39a 100644 --- a/test/fields/collections/Blocks/e2e.spec.ts +++ b/test/fields/collections/Blocks/e2e.spec.ts @@ -4,6 +4,7 @@ import { expect, test } from '@playwright/test' import { addBlock } from 'helpers/e2e/addBlock.js' import { openBlocksDrawer } from 'helpers/e2e/openBlocksDrawer.js' import { reorderBlocks } from 'helpers/e2e/reorderBlocks.js' +import { scrollEntirePage } from 'helpers/e2e/scrollEntirePage.js' import path from 'path' import { fileURLToPath } from 'url' @@ -329,20 +330,16 @@ describe('Block fields', () => { test('should add 2 new block rows', async () => { await page.goto(url.create) + await scrollEntirePage(page) + await page .locator('.custom-blocks-field-management') .getByRole('button', { name: 'Add Block 1' }) .click() - const customBlocks = page.locator( - '#field-customBlocks input[name="customBlocks.0.block1Title"]', - ) - - await page.mouse.wheel(0, 1750) - - await customBlocks.scrollIntoViewIfNeeded() - - await expect(customBlocks).toHaveValue('Block 1: Prefilled Title') + await expect( + page.locator('#field-customBlocks input[name="customBlocks.0.block1Title"]'), + ).toHaveValue('Block 1: Prefilled Title') await page .locator('.custom-blocks-field-management') diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index 11d14d44c..e54cb0582 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -1086,6 +1086,7 @@ export interface CodeField { json?: string | null; html?: string | null; css?: string | null; + codeWithPadding?: string | null; updatedAt: string; createdAt: string; } @@ -2853,6 +2854,7 @@ export interface CodeFieldsSelect { json?: T; html?: T; css?: T; + codeWithPadding?: T; updatedAt?: T; createdAt?: T; } diff --git a/test/helpers/e2e/scrollEntirePage.ts b/test/helpers/e2e/scrollEntirePage.ts new file mode 100644 index 000000000..9578c3786 --- /dev/null +++ b/test/helpers/e2e/scrollEntirePage.ts @@ -0,0 +1,34 @@ +import type { Page } from '@playwright/test' + +import { waitForPageStability } from './waitForPageStability.js' + +/** + * Scroll to bottom of the page continuously until no new content is loaded. + * This is needed because we conditionally render fields as they enter the viewport. + * This will ensure that all fields are rendered and fully loaded before we start testing. + * Without this step, Playwright's `locator.scrollIntoView()` might not work as expected. + * @param page - Playwright page object + * @returns Promise + */ +export const scrollEntirePage = async (page: Page) => { + let previousHeight = await page.evaluate(() => document.body.scrollHeight) + + while (true) { + await page.evaluate(() => { + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) + }) + + // Wait for the page to stabilize after scrolling + await waitForPageStability({ page }) + + // Get the new page height after stability check + const newHeight = await page.evaluate(() => document.body.scrollHeight) + + // Stop if the height hasn't changed, meaning no new content was loaded + if (newHeight === previousHeight) { + break + } + + previousHeight = newHeight + } +} diff --git a/test/helpers/e2e/waitForPageStability.ts b/test/helpers/e2e/waitForPageStability.ts new file mode 100644 index 000000000..84f6f55d2 --- /dev/null +++ b/test/helpers/e2e/waitForPageStability.ts @@ -0,0 +1,54 @@ +import type { Page } from '@playwright/test' + +/** + * Checks if the page is stable by continually polling until the page size remains constant in size and there are no loading shimmers. + * A page is considered stable if it passes this test multiple times. + * This will ensure that the page won't unexpectedly change while testing. + * @param page - Playwright page object + * @param intervalMs - Polling interval in milliseconds + * @param stableChecksRequired - Number of stable checks required to consider page stable + * @returns Promise + */ +export const waitForPageStability = async ({ + page, + interval = 1000, + stableChecksRequired = 3, +}: { + interval?: number + page: Page + stableChecksRequired?: number +}) => { + await page.waitForLoadState('networkidle') // Wait for network to be idle + + await page.waitForFunction( + async ({ interval, stableChecksRequired }) => { + return new Promise((resolve) => { + let previousHeight = document.body.scrollHeight + let stableChecks = 0 + + const checkStability = () => { + const currentHeight = document.body.scrollHeight + const loadingShimmers = document.querySelectorAll('.shimmer-effect') + const pageSizeChanged = currentHeight !== previousHeight + + if (!pageSizeChanged && loadingShimmers.length === 0) { + stableChecks++ // Increment stability count + } else { + stableChecks = 0 // Reset stability count if page changes + } + + previousHeight = currentHeight + + if (stableChecks >= stableChecksRequired) { + resolve(true) // Only resolve after multiple stable checks + } else { + setTimeout(checkStability, interval) // Poll again + } + } + + checkStability() + }) + }, + { interval, stableChecksRequired }, + ) +} diff --git a/tsconfig.base.json b/tsconfig.base.json index c9793d25c..93d171a6a 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -31,7 +31,7 @@ } ], "paths": { - "@payload-config": ["./test/_community/config.ts"], + "@payload-config": ["./test/fields/config.ts"], "@payloadcms/admin-bar": ["./packages/admin-bar/src"], "@payloadcms/live-preview": ["./packages/live-preview/src"], "@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],