From 6406577320465fc6655a45a7c9a58474391a1c57 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Sat, 7 Feb 2026 17:16:29 -0500 Subject: [PATCH 01/10] test: Add Playwright E2E test infrastructure Install @playwright/test, configure Chromium-only project with Vite dev server integration, add shared editor-setup helper, ESLint overrides for the e2e directory, npm scripts (test:e2e, test:e2e:ui), Makefile targets (test-e2e, test-e2e-ui), and gitignore entries for test artifacts. --- .gitignore | 4 +++ Makefile | 8 ++++++ e2e/.eslintrc.cjs | 10 +++++++ e2e/editor-setup.js | 64 ++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 64 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +++ playwright.config.js | 31 +++++++++++++++++++++ 7 files changed, 184 insertions(+) create mode 100644 e2e/.eslintrc.cjs create mode 100644 e2e/editor-setup.js create mode 100644 playwright.config.js diff --git a/.gitignore b/.gitignore index 152f10959..8b3b69210 100644 --- a/.gitignore +++ b/.gitignore @@ -196,5 +196,9 @@ local.properties # Translation files src/translations/ +# Playwright +e2e/test-results/ +playwright-report/ + # Claude .claude/settings.local.json diff --git a/Makefile b/Makefile index 79bb8ef1f..a24a0dc5c 100644 --- a/Makefile +++ b/Makefile @@ -136,6 +136,14 @@ lint-swift: ## Lint Swift code # Testing Targets ################################################################################ +.PHONY: test-e2e +test-e2e: npm-dependencies ## Run end-to-end tests + npm run test:e2e + +.PHONY: test-e2e-ui +test-e2e-ui: npm-dependencies ## Run end-to-end tests in UI mode + npm run test:e2e:ui + .PHONY: test-js test-js: npm-dependencies ## Run JavaScript tests npm run test:unit diff --git a/e2e/.eslintrc.cjs b/e2e/.eslintrc.cjs new file mode 100644 index 000000000..a788a0f36 --- /dev/null +++ b/e2e/.eslintrc.cjs @@ -0,0 +1,10 @@ +module.exports = { + extends: [ 'plugin:playwright/recommended' ], + rules: { + // Allow non-literal titles for test.describe/test parameterized tests. + 'playwright/valid-title': 'off', + // The WordPress ESLint config enforces jsdoc on all functions, but + // test files don't benefit from it. + 'jsdoc/require-jsdoc': 'off', + }, +}; diff --git a/e2e/editor-setup.js b/e2e/editor-setup.js new file mode 100644 index 000000000..4ca117c25 --- /dev/null +++ b/e2e/editor-setup.js @@ -0,0 +1,64 @@ +/** + * Shared editor setup helper for E2E tests. + * + * Navigates to the editor in dev mode, injects the GBKit config, and waits + * for the editor to reach a ready state. + */ + +/** + * Default GBKit configuration for dev-mode testing. + * + * @type {Object} + */ +const DEFAULT_GBKIT = { + post: { + id: -1, + type: 'post', + status: 'draft', + title: '', + content: '', + }, +}; + +/** + * Navigate to the editor and wait for it to be fully ready. + * + * @param {import('@playwright/test').Page} page Playwright page object. + * @param {Object} [gbkit] Optional GBKit config override. + */ +export async function setupEditor( page, gbkit = DEFAULT_GBKIT ) { + // Inject GBKit config before page scripts run. + await page.addInitScript( ( config ) => { + window.GBKit = config; + }, gbkit ); + + // Navigate to the editor in dev mode. + await page.goto( '/?dev_mode=1' ); + + // Wait for the visual editor container to be visible. + await page.locator( '.gutenberg-kit-visual-editor' ).waitFor( { + state: 'visible', + timeout: 30_000, + } ); + + // Wait for WordPress editor data store to report ready. + await page.waitForFunction( + () => + window.wp?.data + ?.select( 'core/editor' ) + ?.__unstableIsEditorReady?.(), + { timeout: 30_000 } + ); +} + +/** + * Retrieve all blocks from the editor via the WP data store. + * + * @param {import('@playwright/test').Page} page Playwright page object. + * @return {Promise} Array of block objects. + */ +export async function getBlocks( page ) { + return await page.evaluate( () => + window.wp.data.select( 'core/block-editor' ).getBlocks() + ); +} diff --git a/package-lock.json b/package-lock.json index e3860f185..d5f45278a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,6 +68,7 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@playwright/test": "^1.58.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", @@ -3591,6 +3592,22 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@preact/signals": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-1.3.2.tgz", @@ -14713,6 +14730,53 @@ "node": ">=10" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", diff --git a/package.json b/package.json index 636df1d58..a6b7b759b 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "postinstall": "patch-package && npm run prep-translations && npm run generate-version", "prep-translations": "node bin/prep-translations.js", "preview": "vite preview --host", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", "test:unit": "vitest run", "test:unit:watch": "vitest" }, @@ -82,6 +84,7 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@playwright/test": "^1.58.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 000000000..a856ee123 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig( { + testDir: './e2e', + outputDir: './e2e/test-results', + fullyParallel: false, + workers: 1, + retries: process.env.CI ? 2 : 0, + timeout: 60_000, + reporter: process.env.CI ? 'list' : 'html', + use: { + baseURL: 'http://localhost:5173', + actionTimeout: 10_000, + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices[ 'Desktop Chrome' ] }, + }, + ], + webServer: { + command: 'npm run dev', + url: 'http://localhost:5173', + reuseExistingServer: true, + timeout: 30_000, + }, +} ); From ad663dc2cd817931487024476d134dcc5cbae6b8 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Sat, 7 Feb 2026 17:16:36 -0500 Subject: [PATCH 02/10] test: Add editor load E2E tests Verify the editor initializes and reaches a usable state: visual editor visible, post title wrapper present, block list container rendered, toolbar shown, and default block appender attached. Also test empty editor state and loading with initial block content. --- e2e/editor-load.spec.js | 67 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 e2e/editor-load.spec.js diff --git a/e2e/editor-load.spec.js b/e2e/editor-load.spec.js new file mode 100644 index 000000000..5f33d175f --- /dev/null +++ b/e2e/editor-load.spec.js @@ -0,0 +1,67 @@ +/** + * External dependencies + */ +import { test, expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { setupEditor, getBlocks } from './editor-setup'; + +test.describe( 'Editor Load', () => { + test( 'should load the editor and reach ready state', async ( { + page, + } ) => { + await setupEditor( page ); + + await expect( + page.locator( '.gutenberg-kit-visual-editor' ) + ).toBeVisible(); + + await expect( + page.locator( '.editor-visual-editor__post-title-wrapper' ) + ).toBeVisible(); + + await expect( + page.locator( '.block-editor-block-list__layout.is-root-container' ) + ).toBeVisible(); + + await expect( + page.locator( '.gutenberg-kit-editor-toolbar' ) + ).toBeVisible(); + + await expect( + page.locator( 'button.gutenberg-kit-default-block-appender' ) + ).toBeAttached(); + } ); + + test( 'should display an empty editor with no initial content', async ( { + page, + } ) => { + await setupEditor( page ); + + const blocks = await getBlocks( page ); + expect( blocks ).toHaveLength( 0 ); + } ); + + test( 'should load the editor with initial content', async ( { page } ) => { + const contentHtml = + '\n

Hello from E2E

\n'; + + await setupEditor( page, { + post: { + id: 1, + type: 'post', + status: 'draft', + title: '', + content: contentHtml, + }, + } ); + + const blocks = await getBlocks( page ); + expect( blocks ).toHaveLength( 1 ); + expect( blocks[ 0 ].name ).toBe( 'core/paragraph' ); + + await expect( page.getByText( 'Hello from E2E' ) ).toBeVisible(); + } ); +} ); From 4f0ca0e91cb0bc281502261c56b12abbdc0fb74b Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Sat, 7 Feb 2026 17:17:11 -0500 Subject: [PATCH 03/10] test: Add text formatting E2E tests Verify typing text into a new paragraph block, applying bold (Cmd+B), italic (Cmd+I), and combined bold+italic formatting via keyboard shortcuts. Asserts block content attributes contain the expected inline formatting markup. --- e2e/text-formatting.spec.js | 74 +++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 e2e/text-formatting.spec.js diff --git a/e2e/text-formatting.spec.js b/e2e/text-formatting.spec.js new file mode 100644 index 000000000..cfec3c584 --- /dev/null +++ b/e2e/text-formatting.spec.js @@ -0,0 +1,74 @@ +/** + * External dependencies + */ +import { test, expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { setupEditor, getBlocks } from './editor-setup'; + +test.describe( 'Text Formatting', () => { + test( 'should type text into a new paragraph block', async ( { page } ) => { + await setupEditor( page ); + + await page + .locator( 'button.gutenberg-kit-default-block-appender' ) + .click(); + await page.keyboard.type( 'Hello World' ); + + const blocks = await getBlocks( page ); + expect( blocks ).toHaveLength( 1 ); + expect( blocks[ 0 ].attributes.content ).toBe( 'Hello World' ); + } ); + + test( 'should apply bold formatting', async ( { page } ) => { + await setupEditor( page ); + + await page + .locator( 'button.gutenberg-kit-default-block-appender' ) + .click(); + await page.keyboard.type( 'Bold text' ); + await page.keyboard.press( 'ControlOrMeta+a' ); + await page.keyboard.press( 'ControlOrMeta+b' ); + + const blocks = await getBlocks( page ); + expect( blocks[ 0 ].attributes.content ).toBe( + 'Bold text' + ); + } ); + + test( 'should apply italic formatting', async ( { page } ) => { + await setupEditor( page ); + + await page + .locator( 'button.gutenberg-kit-default-block-appender' ) + .click(); + await page.keyboard.type( 'Italic text' ); + await page.keyboard.press( 'ControlOrMeta+a' ); + await page.keyboard.press( 'ControlOrMeta+i' ); + + const blocks = await getBlocks( page ); + expect( blocks[ 0 ].attributes.content ).toBe( 'Italic text' ); + } ); + + test( 'should apply combined formatting (bold + italic)', async ( { + page, + } ) => { + await setupEditor( page ); + + await page + .locator( 'button.gutenberg-kit-default-block-appender' ) + .click(); + await page.keyboard.type( 'Styled text' ); + await page.keyboard.press( 'ControlOrMeta+a' ); + await page.keyboard.press( 'ControlOrMeta+b' ); + await page.keyboard.press( 'ControlOrMeta+i' ); + + const blocks = await getBlocks( page ); + const content = blocks[ 0 ].attributes.content; + expect( content ).toContain( '' ); + expect( content ).toContain( '' ); + expect( content ).toContain( 'Styled text' ); + } ); +} ); From 829dc301bc5946c4a96fd4b88369e7a2b8211947 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Sat, 7 Feb 2026 17:18:33 -0500 Subject: [PATCH 04/10] test: Add split and merge block E2E tests Verify splitting a paragraph with Enter, merging with Backspace, round-trip content preservation, and inline formatting preservation across split/merge operations. --- e2e/split-merge.spec.js | 126 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 e2e/split-merge.spec.js diff --git a/e2e/split-merge.spec.js b/e2e/split-merge.spec.js new file mode 100644 index 000000000..73d2e04ad --- /dev/null +++ b/e2e/split-merge.spec.js @@ -0,0 +1,126 @@ +/** + * External dependencies + */ +import { test, expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { setupEditor, getBlocks } from './editor-setup'; + +test.describe( 'Split and Merge Blocks', () => { + test( 'should split a paragraph block with Enter', async ( { page } ) => { + await setupEditor( page ); + + await page + .locator( 'button.gutenberg-kit-default-block-appender' ) + .click(); + await page.keyboard.type( 'FirstSecond' ); + + // Move caret between "First" and "Second". + for ( let i = 0; i < 6; i++ ) { + await page.keyboard.press( 'ArrowLeft' ); + } + + await page.keyboard.press( 'Enter' ); + + const blocks = await getBlocks( page ); + expect( blocks ).toHaveLength( 2 ); + expect( blocks[ 0 ].attributes.content ).toBe( 'First' ); + expect( blocks[ 1 ].attributes.content ).toBe( 'Second' ); + } ); + + test( 'should merge two paragraph blocks with Backspace', async ( { + page, + } ) => { + await setupEditor( page ); + + // Create two blocks by typing and splitting. + await page + .locator( 'button.gutenberg-kit-default-block-appender' ) + .click(); + await page.keyboard.type( 'FirstSecond' ); + for ( let i = 0; i < 6; i++ ) { + await page.keyboard.press( 'ArrowLeft' ); + } + await page.keyboard.press( 'Enter' ); + + // Cursor is now at the start of the second block. Press Backspace to merge. + await page.keyboard.press( 'Home' ); + await page.keyboard.press( 'Backspace' ); + + const blocks = await getBlocks( page ); + expect( blocks ).toHaveLength( 1 ); + expect( blocks[ 0 ].attributes.content ).toBe( 'FirstSecond' ); + } ); + + test( 'should preserve content after split and merge roundtrip', async ( { + page, + } ) => { + await setupEditor( page ); + + const originalText = 'The quick brown fox'; + + await page + .locator( 'button.gutenberg-kit-default-block-appender' ) + .click(); + await page.keyboard.type( originalText ); + + // Split in the middle ("The quick" | " brown fox"). + for ( let i = 0; i < 10; i++ ) { + await page.keyboard.press( 'ArrowLeft' ); + } + await page.keyboard.press( 'Enter' ); + + let blocks = await getBlocks( page ); + expect( blocks ).toHaveLength( 2 ); + + // Merge back. + await page.keyboard.press( 'Home' ); + await page.keyboard.press( 'Backspace' ); + + blocks = await getBlocks( page ); + expect( blocks ).toHaveLength( 1 ); + expect( blocks[ 0 ].attributes.content ).toBe( originalText ); + } ); + + test( 'should split and merge preserving inline formatting', async ( { + page, + } ) => { + await setupEditor( page ); + + await page + .locator( 'button.gutenberg-kit-default-block-appender' ) + .click(); + + // Type "Hello " then bold "World". + await page.keyboard.type( 'Hello ' ); + await page.keyboard.press( 'ControlOrMeta+b' ); + await page.keyboard.type( 'World' ); + await page.keyboard.press( 'ControlOrMeta+b' ); + + // Split between "Hello " and "World". + await page.keyboard.press( 'Home' ); + for ( let i = 0; i < 6; i++ ) { + await page.keyboard.press( 'ArrowRight' ); + } + await page.keyboard.press( 'Enter' ); + + let blocks = await getBlocks( page ); + expect( blocks ).toHaveLength( 2 ); + expect( blocks[ 0 ].attributes.content ).toBe( 'Hello ' ); + expect( blocks[ 1 ].attributes.content ).toBe( + 'World' + ); + + // Merge back. + await page.keyboard.press( 'Home' ); + await page.keyboard.press( 'Backspace' ); + + blocks = await getBlocks( page ); + expect( blocks ).toHaveLength( 1 ); + expect( blocks[ 0 ].attributes.content ).toBe( + 'Hello World' + ); + } ); +} ); From 2cb8e7425dd66443b905d7ac2baf5eae4045e85a Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Sat, 7 Feb 2026 21:55:20 -0500 Subject: [PATCH 05/10] fix: Include digits in Makefile help target grep pattern The help target's grep pattern `[a-zA-Z_-]` excluded digits, so targets containing numbers (e.g. test-e2e) were not listed. Add `0-9` to the character class. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a24a0dc5c..70fde73f8 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ help: ## Display this help menu @echo "Usage: make [target]" @echo "" @echo "Available targets:" - @grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | \ + @grep -E '^[a-zA-Z0-9_-]+:.*?## ' $(MAKEFILE_LIST) | \ awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-25s\033[0m %s\n", $$1, $$2}' | \ sort @echo "" From 6feaef8e09dc48e594ba09e32ffdcf797adae51d Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Sat, 7 Feb 2026 22:02:44 -0500 Subject: [PATCH 06/10] ci: Add E2E test step to Buildkite pipeline Install Chromium and run Playwright E2E tests alongside the existing JS unit test and lint steps. --- .buildkite/pipeline.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index f29971747..18942c83a 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -19,6 +19,12 @@ steps: command: make test-js plugins: *plugins + - label: ':performing_arts: Test E2E' + command: | + npx playwright install chromium + make test-e2e + plugins: *plugins + - label: ':android: Publish Android Library' command: | make build REFRESH_L10N=1 REFRESH_JS_BUILD=1 From 180a098207f3dd84a140f204b557b8cd32d710b9 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Sat, 7 Feb 2026 22:10:14 -0500 Subject: [PATCH 07/10] docs: Remove manual test cases covered by E2E tests Remove S.1 (Write and format text) and S.3 (Merge and split blocks) from the manual smoke tests, as these flows are now covered by the Playwright E2E test suite. Renumber remaining smoke tests. --- docs/test-cases.md | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/docs/test-cases.md b/docs/test-cases.md index 91e722c2c..a2d9eab79 100644 --- a/docs/test-cases.md +++ b/docs/test-cases.md @@ -4,15 +4,7 @@ **Purpose:** Verify the editor's core functionality: writing/formatting text, uploading media, saving/publishing, and basic block manipulation. -### S.1. Write and format text - -- **Steps:** - - Add a Paragraph, List, or Heading block. - - Type some text. - - Apply bold, italic, and strikethrough formatting using the toolbar. -- **Expected Outcome:** Text is entered and formatting is applied as expected. - -### S.2. Add a link to a paragraph +### S.1. Add a link to a paragraph - **Steps:** - Add a Paragraph, List, or Heading block. @@ -20,36 +12,28 @@ - Apply a link to the text. - **Expected Outcome:** Link is applied to the text as expected. -### S.3. Merge and split blocks - -- **Steps:** - - Write a long paragraph or list of multiple items. - - Place the cursor somwewhere in the middle and split the block into two blocks using Enter. - - Merge them back by deleting content at the start of the second block. -- **Expected Outcome:** Blocks split and merge as expected; content remains intact. - -### S.4. Undo/Redo Actions +### S.2. Undo/Redo Actions - **Steps:** - Add, remove, and edit blocks and text. - Use Undo and Redo buttons. - **Expected Outcome:** Editor correctly undoes and redoes actions, restoring previous states. -### S.5. Upload an image +### S.3. Upload an image - **Steps:** - Add an Image block. - Tap "Choose from device" and select an image. - **Expected Outcome:** Image uploads and displays in the block. An activity indicator is shown while the image is uploading. -### S.6. Upload an video +### S.4. Upload an video - **Steps:** - Add a Video block. - Tap "Choose from device" and select a video. - **Expected Outcome:** Video uploads and displays in the block. An activity indicator is shown while the video is uploading. -### S.7. Reorder blocks +### S.5. Reorder blocks - **Steps:** - Add several content blocks to a post. @@ -57,7 +41,7 @@ - Use the up/down arrows in the block toolbar to relocate the block. - **Expected Outcome:** The block ordering is updated as expected. -### S.8. Save and publish a post +### S.6. Save and publish a post - **Steps:** - Create a new post with text and media. From 55ac0cda6ae9133de6a4ebf29edf88589ec24735 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Sat, 7 Feb 2026 22:20:21 -0500 Subject: [PATCH 08/10] fix: Exclude E2E tests from Vitest test runner Vitest was picking up Playwright spec files in e2e/ and failing because Playwright's test.describe() is incompatible with Vitest's test context. --- vitest.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/vitest.config.js b/vitest.config.js index 283dafd82..5680b58cc 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -7,6 +7,7 @@ import react from '@vitejs/plugin-react'; export default defineConfig( { plugins: [ react() ], test: { + exclude: [ 'e2e/**', 'node_modules/**' ], setupFiles: [ './vitest.setup.js' ], css: false, environment: 'jsdom', From 6dc24cc3fa3ce9fe7fe5c663ff0c8684e69452b6 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Sat, 7 Feb 2026 22:32:18 -0500 Subject: [PATCH 09/10] refactor: Use WordPress ESLint Playwright config instead of direct plugin extend Switch from `plugin:playwright/recommended` to `plugin:@wordpress/eslint-plugin/test-playwright` to align with the upstream Gutenberg E2E linting setup. The WordPress config is currently a thin wrapper around the same Playwright recommended rules, but using it ensures we automatically pick up any Playwright-specific rules WordPress adds in the future. --- e2e/.eslintrc.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/.eslintrc.cjs b/e2e/.eslintrc.cjs index a788a0f36..694a32f2f 100644 --- a/e2e/.eslintrc.cjs +++ b/e2e/.eslintrc.cjs @@ -1,5 +1,5 @@ module.exports = { - extends: [ 'plugin:playwright/recommended' ], + extends: [ 'plugin:@wordpress/eslint-plugin/test-playwright' ], rules: { // Allow non-literal titles for test.describe/test parameterized tests. 'playwright/valid-title': 'off', From beaa105dabee45df1c642c8e074aadce7aa6e720 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Sat, 7 Feb 2026 22:32:36 -0500 Subject: [PATCH 10/10] style: Ban deprecated Playwright APIs in E2E tests Add no-restricted-syntax rules to discourage `page.$()`, `page.$$()` and `page.waitForTimeout()` in favor of the locator API. These rules are aligned with the upstream Gutenberg E2E ESLint configuration and help prevent flaky tests. --- e2e/.eslintrc.cjs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/e2e/.eslintrc.cjs b/e2e/.eslintrc.cjs index 694a32f2f..e6297875b 100644 --- a/e2e/.eslintrc.cjs +++ b/e2e/.eslintrc.cjs @@ -6,5 +6,23 @@ module.exports = { // The WordPress ESLint config enforces jsdoc on all functions, but // test files don't benefit from it. 'jsdoc/require-jsdoc': 'off', + // Discourage deprecated Playwright APIs in favor of locators, aligned + // with the upstream Gutenberg ESLint configuration. + 'no-restricted-syntax': [ + 'error', + { + selector: 'CallExpression[callee.property.name="$"]', + message: '`$` is discouraged, please use `locator` instead.', + }, + { + selector: 'CallExpression[callee.property.name="$$"]', + message: '`$$` is discouraged, please use `locator` instead.', + }, + { + selector: + 'CallExpression[callee.object.name="page"][callee.property.name="waitForTimeout"]', + message: 'Prefer page.locator instead.', + }, + ], }, };