From 98eb9c12aa4949b44c94312e05a663d2d6ee7104 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 20 Feb 2026 11:23:16 -0500 Subject: [PATCH 01/11] docs: Remove S.4 save/publish from manual test cases GutenbergKit delegates save/publish to the native host app, so this test case does not belong in the editor's E2E suite. --- docs/test-cases.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/test-cases.md b/docs/test-cases.md index 63b2d1c0..783b61d0 100644 --- a/docs/test-cases.md +++ b/docs/test-cases.md @@ -25,13 +25,6 @@ - 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.4. Save and publish a post - -- **Steps:** - - Create a new post with text and media. - - Save as draft, then publish. -- **Expected Outcome:** Post is saved and published successfully; content appears as expected. - ## Functionality Tests **Purpose:** Validate deeper content and formatting features, advanced block settings, and robust editor behaviors. From 8adfb56631a9488771cb1ae46507cf5baebe9951 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 20 Feb 2026 11:23:25 -0500 Subject: [PATCH 02/11] test: Add wp-env fixtures and migrate EditorPage to wp-env Introduce `e2e/wp-env-fixtures.js` which reads wp-env credentials from `.wp-env.credentials.json` and fetches editor settings from the WP REST API. Update `EditorPage.setup()` to inject wp-env config (siteApiRoot, authHeader, editorSettings) by default so all E2E tests run against a real WordPress backend. Add helper methods to EditorPage: `getBlockAttribute()`, `setTextAlignment()`, `openBlockSettings()`, and `waitForMediaUpload()`. Update `editor-error.spec.js` to use an unreachable API root so the plugin load failure test still works with wp-env credentials present. --- e2e/editor-error.spec.js | 4 +- e2e/editor-page.js | 106 ++++++++++++++++++++++++++++++++++++--- e2e/wp-env-fixtures.js | 70 ++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 8 deletions(-) create mode 100644 e2e/wp-env-fixtures.js diff --git a/e2e/editor-error.spec.js b/e2e/editor-error.spec.js index d2d354eb..94e5c2a9 100644 --- a/e2e/editor-error.spec.js +++ b/e2e/editor-error.spec.js @@ -80,10 +80,12 @@ test.describe( 'Editor Error Handling', () => { test( 'should show plugin load failure notice and keep editor functional', async ( { page, } ) => { - // Enable plugins without providing API endpoints. This causes + // Enable plugins with an unreachable API root. This causes // fetchEditorAssets to fail, resulting in the plugin load notice. const editor = new EditorPage( page ); await editor.setup( { + siteApiRoot: 'http://localhost:1/', + authHeader: '', post: { id: 1, type: 'post', diff --git a/e2e/editor-page.js b/e2e/editor-page.js index 3dcdf745..b96b891c 100644 --- a/e2e/editor-page.js +++ b/e2e/editor-page.js @@ -6,14 +6,25 @@ */ /** - * Default GBKit configuration for dev-mode testing. + * Internal dependencies + */ +import { credentials, getEditorSettings } from './wp-env-fixtures'; + +/** + * Default GBKit configuration for wp-env testing. * * @type {Object} */ const DEFAULT_GBKIT = { + siteApiRoot: credentials.siteApiRoot, + authHeader: credentials.authHeader, + siteApiNamespace: [], + namespaceExcludedPaths: [], post: { - id: -1, + id: 1, type: 'post', + restBase: 'posts', + restNamespace: 'wp/v2', status: 'draft', title: '', content: '', @@ -34,12 +45,24 @@ export default class EditorPage { /** * Navigate to the editor and wait for it to be fully ready. * - * @param {Object} [gbkit] Optional GBKit config override. + * @param {Object} [gbkit] Optional GBKit config override (merged with defaults). */ - async setup( gbkit = DEFAULT_GBKIT ) { - await this.#page.addInitScript( ( config ) => { - window.GBKit = config; - }, gbkit ); + async setup( gbkit = {} ) { + const editorSettings = await getEditorSettings(); + + const config = { + ...DEFAULT_GBKIT, + editorSettings, + ...gbkit, + post: { + ...DEFAULT_GBKIT.post, + ...gbkit.post, + }, + }; + + await this.#page.addInitScript( ( cfg ) => { + window.GBKit = cfg; + }, config ); await this.#page.goto( '/?dev_mode=1' ); @@ -165,4 +188,73 @@ export default class EditorPage { window.wp.data.select( 'core/block-editor' ).getBlocks() ); } + + /** + * Read a single attribute from a root-level block. + * + * @param {number} index Zero-based block index. + * @param {string} attribute Attribute name. + * @return {Promise<*>} The attribute value. + */ + async getBlockAttribute( index, attribute ) { + return await this.#page.evaluate( + ( { idx, attr } ) => { + const blocks = window.wp.data + .select( 'core/block-editor' ) + .getBlocks(); + return blocks[ idx ]?.attributes?.[ attr ]; + }, + { idx: index, attr: attribute } + ); + } + + /** + * Open the "Align text" dropdown and select an alignment option. + * + * @param {string} alignment Alignment label (e.g. 'Align text center'). + */ + async setTextAlignment( alignment ) { + await this.#page.getByRole( 'button', { name: 'Align text' } ).click(); + await this.#page + .getByRole( 'menuitemradio', { name: alignment } ) + .click(); + } + + /** + * Open the block settings popover (the "Block Settings" cog in the toolbar). + * + * The popover is only available when a block is selected. + */ + async openBlockSettings() { + await this.#page + .getByRole( 'button', { name: 'Block Settings' } ) + .click(); + } + + /** + * Wait for a media upload to complete on a block at the given index. + * + * Polls the block's `id` attribute until it becomes a positive number, + * indicating the upload has finished and the media was assigned a WP ID. + * + * @param {number} index Zero-based block index. + * @param {number} timeout Max wait time in milliseconds. + * @return {Promise} The block's attributes after upload completes. + */ + async waitForMediaUpload( index, timeout = 30_000 ) { + await this.#page.waitForFunction( + ( { idx } ) => { + const blocks = window.wp.data + .select( 'core/block-editor' ) + .getBlocks(); + const block = blocks[ idx ]; + return block?.attributes?.id > 0; + }, + { idx: index }, + { timeout } + ); + + const blocks = await this.getBlocks(); + return blocks[ index ].attributes; + } } diff --git a/e2e/wp-env-fixtures.js b/e2e/wp-env-fixtures.js new file mode 100644 index 00000000..8e5145a6 --- /dev/null +++ b/e2e/wp-env-fixtures.js @@ -0,0 +1,70 @@ +/** + * External dependencies + */ +import fs from 'node:fs'; +import path from 'node:path'; + +const CREDENTIALS_PATH = path.resolve( + import.meta.dirname, + '../.wp-env.credentials.json' +); + +/** + * Read wp-env credentials from the JSON file written by bin/wp-env-setup.sh. + * + * @return {Object} Credentials object with siteUrl, siteApiRoot, authHeader, etc. + */ +function readCredentials() { + if ( ! fs.existsSync( CREDENTIALS_PATH ) ) { + throw new Error( + `wp-env credentials not found at ${ CREDENTIALS_PATH }.\n` + + 'Run "make wp-env-start" to provision the local WordPress environment.' + ); + } + return JSON.parse( fs.readFileSync( CREDENTIALS_PATH, 'utf-8' ) ); +} + +/** Cached editor settings — fetched once and reused across all tests. */ +let cachedEditorSettings = null; + +/** + * Fetch editor settings from the wp-env WordPress instance. + * + * The result is cached in module scope so the REST call is made only once + * per test run. + * + * @param {Object} creds Credentials object from readCredentials(). + * @return {Promise} Editor settings suitable for window.GBKit.editorSettings. + */ +async function fetchEditorSettings( creds ) { + if ( cachedEditorSettings ) { + return cachedEditorSettings; + } + + const response = await fetch( + `${ creds.siteApiRoot }wp-block-editor/v1/settings`, + { + headers: { Authorization: creds.authHeader }, + } + ); + + if ( ! response.ok ) { + throw new Error( + `Failed to fetch editor settings: ${ response.status } ${ response.statusText }` + ); + } + + cachedEditorSettings = await response.json(); + return cachedEditorSettings; +} + +export const credentials = readCredentials(); + +/** + * Get editor settings, fetching from wp-env on first call. + * + * @return {Promise} Editor settings object. + */ +export async function getEditorSettings() { + return fetchEditorSettings( credentials ); +} From 8587cfe534a5a958a7c382051ceed1e5760da986 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 20 Feb 2026 11:23:30 -0500 Subject: [PATCH 03/11] test: Add test media assets for E2E upload tests Add minimal test files used by the media upload E2E tests: - test-image.png (1x1 RGBA PNG) - test-image-2.png (1x1 RGBA PNG, second image for gallery) - test-video.mp4 (minimal MP4 container) - test-audio.mp3 (minimal MP3 frame) - test-file.pdf (minimal PDF document) --- e2e/assets/test-audio.mp3 | Bin 0 -> 336 bytes e2e/assets/test-file.pdf | Bin 0 -> 312 bytes e2e/assets/test-image-2.png | Bin 0 -> 70 bytes e2e/assets/test-image.png | Bin 0 -> 70 bytes e2e/assets/test-video.mp4 | Bin 0 -> 1037 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 e2e/assets/test-audio.mp3 create mode 100644 e2e/assets/test-file.pdf create mode 100644 e2e/assets/test-image-2.png create mode 100644 e2e/assets/test-image.png create mode 100644 e2e/assets/test-video.mp4 diff --git a/e2e/assets/test-audio.mp3 b/e2e/assets/test-audio.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..269a03e875504e11454bd4eccdd3b534de462758 GIT binary patch literal 336 tcmezWd%_V0AmCL15{!ndrXpu{964|>bZ~+2|L>dykX*?*NZlD43INcBZh!y) literal 0 HcmV?d00001 diff --git a/e2e/assets/test-file.pdf b/e2e/assets/test-file.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7c249578dd243829eae04c4a0c5ec882d1245cb2 GIT binary patch literal 312 zcmZXP%?iRW5QOh}ioNuxNo*qup$C6@5wTQn;vw3tRHTt4h3eBABUsVB?Kd+F6XlC+ zz=w=*V4#l|De3NhLpoPZUGWmgfUM$iSo6=jt_hk(hx~faNk6*q6o$I!7z(0W( z{t3`EmbIGcZqK|JLwxX627N4a(E_b*%?YiPslRtK=?qqcK|FVdQ&MBb@0K^v$t^fc4 literal 0 HcmV?d00001 diff --git a/e2e/assets/test-video.mp4 b/e2e/assets/test-video.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..cafab8904d57b04603dd4ce7ce7f7943d62d96d9 GIT binary patch literal 1037 zcmZuwy=xRf6yGE!*oasNmLsN;+}-TnN0Nhq3n7?Bu(GgOW@l$_huhhio1MGc6~Po1 zDXcAmVkHO`mfGkaVWRJ58cE?9ThW-}|1~^*j$Hw$h?d((@KQC*cj~ znE6t-e9!aFCWdpcy_7MvAfK-+Pp8k`?|gdo;c)lGgSB_)@XD93WzcD%8e+8>Gcar`U1dD6D8RT`3$VWqWNNMMHX4)3 zq&^lgR~gOg%JdtqNS7?Y^Oz?X>QE&~!7Zr;$fgD&T%t^= z0Zl;(Pq-oz40`!PV3eC#4JIVDt`Cu0ZZZl^z(?80&?;m~YHC4ea75NnkwXe_G}QSd zhDu(*VMU>?0E?IefN4y%n<5G$K?_$z7(Sb8!bLx|5!jUGdDvGPtaDOB=zv#{n*?qA z8-`NkprV5DoU@T7E$rGf)E%keMGE1Dg~#JMyJ3Wx1jz`_)r|!T37U0`hHe;w>m7jF zC7wxIlFlX&h2@&GkkD{R@Ik;GLM}Rl`**kL?ZFp(p6}oDn$x52pI6zWga7~l literal 0 HcmV?d00001 From 09f44845975beef205acb950f99eb1e10d399401 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 20 Feb 2026 11:23:37 -0500 Subject: [PATCH 04/11] test: Add E2E tests for media uploads (S.2, S.3, F.4, F.6, F.7) Add Playwright E2E tests that verify media upload flows against wp-env: - S.2 image-upload: Upload image via Image block, verify block attributes - S.3 video-upload: Upload video via Video block, verify src attribute - F.4 gallery-block: Upload multiple images, verify inner blocks and caption editing - F.6 audio-upload: Upload audio via Audio block, verify src attribute - F.7 file-upload: Upload file via File block, verify href and fileName All tests use the filechooser event to simulate file selection and `waitForMediaUpload()` to wait for the upload to complete. --- e2e/audio-upload.spec.js | 39 +++++++++++++ e2e/file-upload.spec.js | 40 ++++++++++++++ e2e/gallery-block.spec.js | 112 ++++++++++++++++++++++++++++++++++++++ e2e/image-upload.spec.js | 37 +++++++++++++ e2e/video-upload.spec.js | 37 +++++++++++++ 5 files changed, 265 insertions(+) create mode 100644 e2e/audio-upload.spec.js create mode 100644 e2e/file-upload.spec.js create mode 100644 e2e/gallery-block.spec.js create mode 100644 e2e/image-upload.spec.js create mode 100644 e2e/video-upload.spec.js diff --git a/e2e/audio-upload.spec.js b/e2e/audio-upload.spec.js new file mode 100644 index 00000000..adf0046c --- /dev/null +++ b/e2e/audio-upload.spec.js @@ -0,0 +1,39 @@ +/** + * External dependencies + */ +import path from 'node:path'; +import { test, expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import EditorPage from './editor-page'; + +const TEST_AUDIO = path.resolve( import.meta.dirname, 'assets/test-audio.mp3' ); + +test.describe( 'Audio Upload (F.6)', () => { + test( 'should upload an audio file via the Audio block', async ( { + page, + } ) => { + const editor = new EditorPage( page ); + await editor.setup(); + + await editor.insertBlock( 'core/audio' ); + + // Use the "Upload" button which triggers a file input. + const fileChooserPromise = page.waitForEvent( 'filechooser' ); + await page + .getByRole( 'button', { name: 'Upload', exact: true } ) + .click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles( TEST_AUDIO ); + + // Wait for the upload to complete (block gets a numeric media ID). + const attrs = await editor.waitForMediaUpload( 0 ); + expect( attrs.id ).toBeGreaterThan( 0 ); + expect( attrs.src ).toContain( 'localhost:8888' ); + + // Verify the audio element is rendered. + await expect( page.locator( '.wp-block-audio audio' ) ).toBeAttached(); + } ); +} ); diff --git a/e2e/file-upload.spec.js b/e2e/file-upload.spec.js new file mode 100644 index 00000000..3800e3e3 --- /dev/null +++ b/e2e/file-upload.spec.js @@ -0,0 +1,40 @@ +/** + * External dependencies + */ +import path from 'node:path'; +import { test, expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import EditorPage from './editor-page'; + +const TEST_FILE = path.resolve( import.meta.dirname, 'assets/test-file.pdf' ); + +test.describe( 'File Upload (F.7)', () => { + test( 'should upload a file via the File block', async ( { page } ) => { + const editor = new EditorPage( page ); + await editor.setup(); + + await editor.insertBlock( 'core/file' ); + + // Use the "Upload" button which triggers a file input. + const fileChooserPromise = page.waitForEvent( 'filechooser' ); + await page + .getByRole( 'button', { name: 'Upload', exact: true } ) + .click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles( TEST_FILE ); + + // Wait for the upload to complete (block gets a numeric media ID). + const attrs = await editor.waitForMediaUpload( 0 ); + expect( attrs.id ).toBeGreaterThan( 0 ); + expect( attrs.href ).toContain( 'localhost:8888' ); + + // Verify the filename and download link are rendered. + await expect( + page.locator( '.wp-block-file a' ).first() + ).toBeVisible(); + expect( attrs.fileName ).toContain( 'test-file' ); + } ); +} ); diff --git a/e2e/gallery-block.spec.js b/e2e/gallery-block.spec.js new file mode 100644 index 00000000..54387ccb --- /dev/null +++ b/e2e/gallery-block.spec.js @@ -0,0 +1,112 @@ +/** + * External dependencies + */ +import path from 'node:path'; +import { test, expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import EditorPage from './editor-page'; + +const TEST_IMAGE_1 = path.resolve( + import.meta.dirname, + 'assets/test-image.png' +); +const TEST_IMAGE_2 = path.resolve( + import.meta.dirname, + 'assets/test-image-2.png' +); + +test.describe( 'Gallery Block (F.4)', () => { + test( 'should upload multiple images to a gallery', async ( { page } ) => { + const editor = new EditorPage( page ); + await editor.setup(); + + await editor.insertBlock( 'core/gallery' ); + + // Use the "Upload" button to trigger the file input. + const fileChooserPromise = page.waitForEvent( 'filechooser' ); + await page + .getByRole( 'button', { name: 'Upload', exact: true } ) + .click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles( [ TEST_IMAGE_1, TEST_IMAGE_2 ] ); + + // Wait for both images to upload by checking the gallery's inner blocks. + await page.waitForFunction( + () => { + const blocks = window.wp.data + .select( 'core/block-editor' ) + .getBlocks(); + const gallery = blocks[ 0 ]; + return ( + gallery?.innerBlocks?.length === 2 && + gallery.innerBlocks.every( + ( img ) => img.attributes.id > 0 + ) + ); + }, + { timeout: 30_000 } + ); + + const blocks = await editor.getBlocks(); + const gallery = blocks[ 0 ]; + expect( gallery.name ).toBe( 'core/gallery' ); + expect( gallery.innerBlocks ).toHaveLength( 2 ); + expect( gallery.innerBlocks[ 0 ].name ).toBe( 'core/image' ); + expect( gallery.innerBlocks[ 1 ].name ).toBe( 'core/image' ); + } ); + + test( 'should add a caption to an image in the gallery', async ( { + page, + } ) => { + const editor = new EditorPage( page ); + await editor.setup(); + + await editor.insertBlock( 'core/gallery' ); + + // Upload a single image. + const fileChooserPromise = page.waitForEvent( 'filechooser' ); + await page + .getByRole( 'button', { name: 'Upload', exact: true } ) + .click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles( TEST_IMAGE_1 ); + + // Wait for the upload to complete. + await page.waitForFunction( + () => { + const blocks = window.wp.data + .select( 'core/block-editor' ) + .getBlocks(); + const gallery = blocks[ 0 ]; + return ( + gallery?.innerBlocks?.length === 1 && + gallery.innerBlocks[ 0 ].attributes.id > 0 + ); + }, + { timeout: 30_000 } + ); + + // Set a caption on the inner Image block via the data store. + await page.evaluate( () => { + const blocks = window.wp.data + .select( 'core/block-editor' ) + .getBlocks(); + const innerImage = blocks[ 0 ]?.innerBlocks?.[ 0 ]; + if ( innerImage ) { + window.wp.data + .dispatch( 'core/block-editor' ) + .updateBlockAttributes( innerImage.clientId, { + caption: 'Test caption', + } ); + } + } ); + + // Verify the caption was applied. + const blocks = await editor.getBlocks(); + const innerImage = blocks[ 0 ].innerBlocks[ 0 ]; + expect( innerImage.attributes.caption ).toBe( 'Test caption' ); + } ); +} ); diff --git a/e2e/image-upload.spec.js b/e2e/image-upload.spec.js new file mode 100644 index 00000000..dc4012a9 --- /dev/null +++ b/e2e/image-upload.spec.js @@ -0,0 +1,37 @@ +/** + * External dependencies + */ +import path from 'node:path'; +import { test, expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import EditorPage from './editor-page'; + +const TEST_IMAGE = path.resolve( import.meta.dirname, 'assets/test-image.png' ); + +test.describe( 'Image Upload (S.2)', () => { + test( 'should upload an image via the Image block', async ( { page } ) => { + const editor = new EditorPage( page ); + await editor.setup(); + + await editor.insertBlock( 'core/image' ); + + // Use the "Upload" button which triggers a file input. + const fileChooserPromise = page.waitForEvent( 'filechooser' ); + await page + .getByRole( 'button', { name: 'Upload', exact: true } ) + .click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles( TEST_IMAGE ); + + // Wait for the upload to complete (block gets a numeric media ID). + const attrs = await editor.waitForMediaUpload( 0 ); + expect( attrs.id ).toBeGreaterThan( 0 ); + expect( attrs.url ).toContain( 'localhost:8888' ); + + // Verify the image element is rendered. + await expect( page.locator( '.wp-block-image img' ) ).toBeVisible(); + } ); +} ); diff --git a/e2e/video-upload.spec.js b/e2e/video-upload.spec.js new file mode 100644 index 00000000..30f73b77 --- /dev/null +++ b/e2e/video-upload.spec.js @@ -0,0 +1,37 @@ +/** + * External dependencies + */ +import path from 'node:path'; +import { test, expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import EditorPage from './editor-page'; + +const TEST_VIDEO = path.resolve( import.meta.dirname, 'assets/test-video.mp4' ); + +test.describe( 'Video Upload (S.3)', () => { + test( 'should upload a video via the Video block', async ( { page } ) => { + const editor = new EditorPage( page ); + await editor.setup(); + + await editor.insertBlock( 'core/video' ); + + // Use the "Upload" button which triggers a file input. + const fileChooserPromise = page.waitForEvent( 'filechooser' ); + await page + .getByRole( 'button', { name: 'Upload', exact: true } ) + .click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles( TEST_VIDEO ); + + // Wait for the upload to complete (block gets a numeric media ID). + const attrs = await editor.waitForMediaUpload( 0 ); + expect( attrs.id ).toBeGreaterThan( 0 ); + expect( attrs.src ).toContain( 'localhost:8888' ); + + // Verify the video element is rendered. + await expect( page.locator( '.wp-block-video video' ) ).toBeVisible(); + } ); +} ); From 25ce6ac1dc558b8b04bc8bd2d9f7f42b788889a8 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 20 Feb 2026 11:23:45 -0500 Subject: [PATCH 05/11] test: Add E2E tests for editor features (F.1, F.2, F.3, F.5) Add Playwright E2E tests for non-upload editor features against wp-env: - F.1 text-alignment: Verify center/right alignment on paragraph and verse blocks via the toolbar dropdown - F.2 embed-content: Insert YouTube embed URL and verify oEmbed resolution; test fallback for non-embeddable URLs - F.3 color-gradient: Apply background color via Block Settings UI, verify theme gradients are available, apply gradient via data store - F.5 pattern-insertion: Fetch patterns from WP REST API, insert parsed pattern content via wp.blocks.parse() + insertBlocks() --- e2e/color-gradient.spec.js | 104 ++++++++++++++++++++++++++++++++++ e2e/embed-content.spec.js | 81 ++++++++++++++++++++++++++ e2e/pattern-insertion.spec.js | 61 ++++++++++++++++++++ e2e/text-alignment.spec.js | 56 ++++++++++++++++++ 4 files changed, 302 insertions(+) create mode 100644 e2e/color-gradient.spec.js create mode 100644 e2e/embed-content.spec.js create mode 100644 e2e/pattern-insertion.spec.js create mode 100644 e2e/text-alignment.spec.js diff --git a/e2e/color-gradient.spec.js b/e2e/color-gradient.spec.js new file mode 100644 index 00000000..2421cc9d --- /dev/null +++ b/e2e/color-gradient.spec.js @@ -0,0 +1,104 @@ +/** + * External dependencies + */ +import { test, expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import EditorPage from './editor-page'; + +test.describe( 'Color and Gradient (F.3)', () => { + test( 'should apply a background color to a button via settings', async ( { + page, + } ) => { + const editor = new EditorPage( page ); + await editor.setup(); + + await editor.insertBlock( 'core/buttons' ); + + // Type into the button so the inner button block is focused. + const buttonText = page.getByRole( 'textbox', { + name: 'Button text', + } ); + await buttonText.click(); + await page.keyboard.type( 'Colored' ); + + // Open block settings and navigate to the Styles tab. + await editor.openBlockSettings(); + await page.getByRole( 'tab', { name: 'Styles' } ).click(); + + // Click the Background color control. + await page + .locator( '.block-settings-menu' ) + .getByRole( 'button', { name: 'Background' } ) + .click(); + + // Pick the first color option in the palette. + await page + .locator( '.components-circular-option-picker__option' ) + .first() + .click(); + + // Close the settings popover before reading attributes. + await page.keyboard.press( 'Escape' ); + + // Verify the inner button block got a background color. + const blocks = await editor.getBlocks(); + const innerButton = blocks[ 0 ].innerBlocks[ 0 ]; + expect( + innerButton.attributes.backgroundColor || + innerButton.attributes.style?.color?.background + ).toBeTruthy(); + } ); + + test( 'should have theme gradients available in editor settings', async ( { + page, + } ) => { + const editor = new EditorPage( page ); + await editor.setup(); + + // Verify that theme gradients are loaded from wp-env editor settings. + const hasGradients = await page.evaluate( () => { + const settings = window.wp.data + .select( 'core/block-editor' ) + .getSettings(); + return ( + Array.isArray( settings.gradients ) && + settings.gradients.length > 0 + ); + } ); + expect( hasGradients ).toBe( true ); + } ); + + test( 'should apply a gradient to a button via data store', async ( { + page, + } ) => { + const editor = new EditorPage( page ); + await editor.setup(); + + await editor.insertBlock( 'core/buttons' ); + + // Apply a gradient to the inner button block via the data store. + await page.evaluate( () => { + const blocks = window.wp.data + .select( 'core/block-editor' ) + .getBlocks(); + const innerButton = blocks[ 0 ]?.innerBlocks?.[ 0 ]; + if ( innerButton ) { + window.wp.data + .dispatch( 'core/block-editor' ) + .updateBlockAttributes( innerButton.clientId, { + gradient: 'vivid-cyan-blue-to-vivid-purple', + } ); + } + } ); + + // Verify the gradient was applied. + const blocks = await editor.getBlocks(); + const innerButton = blocks[ 0 ].innerBlocks[ 0 ]; + expect( innerButton.attributes.gradient ).toBe( + 'vivid-cyan-blue-to-vivid-purple' + ); + } ); +} ); diff --git a/e2e/embed-content.spec.js b/e2e/embed-content.spec.js new file mode 100644 index 00000000..0e265bb0 --- /dev/null +++ b/e2e/embed-content.spec.js @@ -0,0 +1,81 @@ +/** + * External dependencies + */ +import { test, expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import EditorPage from './editor-page'; + +test.describe( 'Embedded Content (F.2)', () => { + test( 'should insert an embed block with a YouTube URL', async ( { + page, + } ) => { + const editor = new EditorPage( page ); + await editor.setup(); + + await editor.insertBlock( 'core/embed' ); + + // Type a YouTube URL into the embed URL input. + const urlInput = page.getByRole( 'textbox', { + name: 'Embed URL', + } ); + await expect( urlInput ).toBeVisible(); + await urlInput.fill( 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' ); + await page.keyboard.press( 'Enter' ); + + // Wait for the oEmbed proxy to resolve — the block should get + // a `url` attribute and the embed preview should render. + await page.waitForFunction( + () => { + const blocks = window.wp.data + .select( 'core/block-editor' ) + .getBlocks(); + const embed = blocks[ 0 ]; + return ( + embed?.attributes?.url && + embed?.attributes?.providerNameSlug + ); + }, + { timeout: 30_000 } + ); + + const blocks = await editor.getBlocks(); + expect( blocks[ 0 ].attributes.url ).toContain( 'youtube.com' ); + expect( blocks[ 0 ].attributes.providerNameSlug ).toBe( 'youtube' ); + } ); + + test( 'should show a fallback link for a non-embeddable URL', async ( { + page, + } ) => { + const editor = new EditorPage( page ); + await editor.setup(); + + await editor.insertBlock( 'core/embed' ); + + const urlInput = page.getByRole( 'textbox', { + name: 'Embed URL', + } ); + await expect( urlInput ).toBeVisible(); + await urlInput.fill( 'https://example.com/not-embeddable' ); + await page.keyboard.press( 'Enter' ); + + // Wait for the embed to resolve (or fall back). + // The block should still render with the URL attribute. + await page.waitForFunction( + () => { + const blocks = window.wp.data + .select( 'core/block-editor' ) + .getBlocks(); + return blocks[ 0 ]?.attributes?.url; + }, + { timeout: 30_000 } + ); + + const blocks = await editor.getBlocks(); + expect( blocks[ 0 ].attributes.url ).toContain( + 'example.com/not-embeddable' + ); + } ); +} ); diff --git a/e2e/pattern-insertion.spec.js b/e2e/pattern-insertion.spec.js new file mode 100644 index 00000000..db27e7c3 --- /dev/null +++ b/e2e/pattern-insertion.spec.js @@ -0,0 +1,61 @@ +/** + * External dependencies + */ +import { test, expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import EditorPage from './editor-page'; +import { credentials } from './wp-env-fixtures'; + +test.describe( 'Pattern Insertion (F.5)', () => { + test( 'should fetch patterns from the WP REST API', async () => { + // Verify that patterns are available from the wp-env backend, + // even though GBK delivers them through the native inserter bridge + // rather than the web inserter. + const response = await fetch( + `${ credentials.siteApiRoot }wp/v2/block-patterns/patterns`, + { headers: { Authorization: credentials.authHeader } } + ); + expect( response.ok ).toBe( true ); + + const patterns = await response.json(); + expect( patterns.length ).toBeGreaterThan( 0 ); + + // Verify patterns have the expected structure. + const first = patterns[ 0 ]; + expect( first ).toHaveProperty( 'name' ); + expect( first ).toHaveProperty( 'content' ); + } ); + + test( 'should insert parsed pattern content into the editor', async ( { + page, + } ) => { + const editor = new EditorPage( page ); + await editor.setup(); + + // Fetch a pattern from the REST API. + const response = await fetch( + `${ credentials.siteApiRoot }wp/v2/block-patterns/patterns`, + { headers: { Authorization: credentials.authHeader } } + ); + const patterns = await response.json(); + const pattern = patterns.find( ( p ) => p.content ); + + // Parse and insert the pattern content via the data store. + const blockCount = await page.evaluate( ( content ) => { + const blocks = window.wp.blocks.parse( content ); + window.wp.data + .dispatch( 'core/block-editor' ) + .insertBlocks( blocks ); + return blocks.length; + }, pattern.content ); + + expect( blockCount ).toBeGreaterThan( 0 ); + + // Verify blocks appeared in the editor. + const blocks = await editor.getBlocks(); + expect( blocks.length ).toBeGreaterThanOrEqual( blockCount ); + } ); +} ); diff --git a/e2e/text-alignment.spec.js b/e2e/text-alignment.spec.js new file mode 100644 index 00000000..26ffdb07 --- /dev/null +++ b/e2e/text-alignment.spec.js @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import { test, expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import EditorPage from './editor-page'; + +test.describe( 'Text Alignment (F.1)', () => { + test( 'should align a paragraph block to center', async ( { page } ) => { + const editor = new EditorPage( page ); + await editor.setup(); + + await editor.clickBlockAppender(); + await page.keyboard.type( 'Centered text' ); + await editor.setTextAlignment( 'Align text center' ); + + const blocks = await editor.getBlocks(); + expect( blocks[ 0 ].attributes.style.typography.textAlign ).toBe( + 'center' + ); + } ); + + test( 'should align a paragraph block to right', async ( { page } ) => { + const editor = new EditorPage( page ); + await editor.setup(); + + await editor.clickBlockAppender(); + await page.keyboard.type( 'Right-aligned text' ); + await editor.setTextAlignment( 'Align text right' ); + + const blocks = await editor.getBlocks(); + expect( blocks[ 0 ].attributes.style.typography.textAlign ).toBe( + 'right' + ); + } ); + + test( 'should set textAlign on a verse block', async ( { page } ) => { + const editor = new EditorPage( page ); + await editor.setup(); + + await editor.insertBlock( 'core/verse' ); + const verseInput = page.locator( + 'pre[aria-label="Block: Verse"][contenteditable="true"]' + ); + await verseInput.click(); + await page.keyboard.type( 'A verse line' ); + + await editor.setTextAlignment( 'Align text center' ); + + const textAlign = await editor.getBlockAttribute( 0, 'textAlign' ); + expect( textAlign ).toBe( 'center' ); + } ); +} ); From 532ae0eaaad4440ac11431d1d781f215e6c10dd2 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 20 Feb 2026 11:24:05 -0500 Subject: [PATCH 06/11] docs: Remove automated test cases from test-cases.md Automated tests live in the E2E suite and don't need to be duplicated in the manual test cases document. Only S.1 (Undo/Redo) remains since it requires the native toolbar. --- docs/test-cases.md | 77 ++-------------------------------------------- 1 file changed, 2 insertions(+), 75 deletions(-) diff --git a/docs/test-cases.md b/docs/test-cases.md index 783b61d0..374c982a 100644 --- a/docs/test-cases.md +++ b/docs/test-cases.md @@ -1,83 +1,10 @@ # Mobile Editor Tests -## Smoke Tests +These test cases require the native undo/redo toolbar and must be verified manually on iOS or Android. -**Purpose:** Verify the editor's core functionality: writing/formatting text, uploading media, saving/publishing, and basic block manipulation. - -### S.1. Undo/Redo Actions +## S.1. 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.2. 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.3. 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. - -## Functionality Tests - -**Purpose:** Validate deeper content and formatting features, advanced block settings, and robust editor behaviors. - -### F.1. Text alignment options - -- **Steps:** - - Add a Paragraph or Verse block. - - Type text and use alignment options (left, center, right). -- **Expected Outcome:** Selected alignment is applied to the block content. - -### F.2. Add and preview embedded content - -- **Steps:** - - Add a Shortcode or Embed block. - - Insert a YouTube or Twitter link. - - Preview the post. -- **Expected Outcome:** Embedded content (e.g., YouTube video) displays correctly in preview. - -### F.3. Color and gradient customization - -- **Steps:** - - Add a block supporting color (e.g., Buttons, Cover). - - Open color settings, switch between solid and gradient, pick custom colors, and apply. -- **Expected Outcome:** Selected colors/gradients are applied; UI updates accordingly. - -### F.4. Gallery block: image uploads and captions - -- **Steps:** - - Add a Gallery block, upload multiple images. - - Add captions to gallery and individual images, apply formatting. -- **Expected Outcome:** An activity indicator is shown while the images are uploading. Captions and formatting display as expected. - -### F.5. Pattern insertion - -- **Steps:** - - Insert a pattern from the inserter. -- **Expected Outcome:** Pattern content appears. - -### F.6. Upload an audio file - -Known issue: [Audio block unable to upload expected file formats](https://github.com/wordpress-mobile/GutenbergKit/issues/123) - -- **Steps:** - - Add an Audio block. - - Tap "Choose from device" and select an audio file. -- **Expected Outcome:** Audio uploads and displays in the block. An activity indicator is shown while the audio is uploading. - -### F.7. Upload a file - -Known issue: [File block unable to upload expected file formats](https://github.com/wordpress-mobile/GutenbergKit/issues/124) - -- **Steps:** - - Add a File block. - - Tap "Choose from device" and select a file. -- **Expected Outcome:** File uploads, filename and download button appear when upload completes. From 25ec72c3228076700ce64467775896b305991aec Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 20 Feb 2026 11:29:25 -0500 Subject: [PATCH 07/11] test: Remove legacy test case ID suffixes from describe blocks --- e2e/audio-upload.spec.js | 2 +- e2e/color-gradient.spec.js | 2 +- e2e/embed-content.spec.js | 2 +- e2e/file-upload.spec.js | 2 +- e2e/gallery-block.spec.js | 2 +- e2e/image-upload.spec.js | 2 +- e2e/pattern-insertion.spec.js | 2 +- e2e/text-alignment.spec.js | 2 +- e2e/video-upload.spec.js | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/e2e/audio-upload.spec.js b/e2e/audio-upload.spec.js index adf0046c..c944c281 100644 --- a/e2e/audio-upload.spec.js +++ b/e2e/audio-upload.spec.js @@ -11,7 +11,7 @@ import EditorPage from './editor-page'; const TEST_AUDIO = path.resolve( import.meta.dirname, 'assets/test-audio.mp3' ); -test.describe( 'Audio Upload (F.6)', () => { +test.describe( 'Audio Upload', () => { test( 'should upload an audio file via the Audio block', async ( { page, } ) => { diff --git a/e2e/color-gradient.spec.js b/e2e/color-gradient.spec.js index 2421cc9d..ee1400b2 100644 --- a/e2e/color-gradient.spec.js +++ b/e2e/color-gradient.spec.js @@ -8,7 +8,7 @@ import { test, expect } from '@playwright/test'; */ import EditorPage from './editor-page'; -test.describe( 'Color and Gradient (F.3)', () => { +test.describe( 'Color and Gradient', () => { test( 'should apply a background color to a button via settings', async ( { page, } ) => { diff --git a/e2e/embed-content.spec.js b/e2e/embed-content.spec.js index 0e265bb0..b92045df 100644 --- a/e2e/embed-content.spec.js +++ b/e2e/embed-content.spec.js @@ -8,7 +8,7 @@ import { test, expect } from '@playwright/test'; */ import EditorPage from './editor-page'; -test.describe( 'Embedded Content (F.2)', () => { +test.describe( 'Embedded Content', () => { test( 'should insert an embed block with a YouTube URL', async ( { page, } ) => { diff --git a/e2e/file-upload.spec.js b/e2e/file-upload.spec.js index 3800e3e3..19185932 100644 --- a/e2e/file-upload.spec.js +++ b/e2e/file-upload.spec.js @@ -11,7 +11,7 @@ import EditorPage from './editor-page'; const TEST_FILE = path.resolve( import.meta.dirname, 'assets/test-file.pdf' ); -test.describe( 'File Upload (F.7)', () => { +test.describe( 'File Upload', () => { test( 'should upload a file via the File block', async ( { page } ) => { const editor = new EditorPage( page ); await editor.setup(); diff --git a/e2e/gallery-block.spec.js b/e2e/gallery-block.spec.js index 54387ccb..187968d5 100644 --- a/e2e/gallery-block.spec.js +++ b/e2e/gallery-block.spec.js @@ -18,7 +18,7 @@ const TEST_IMAGE_2 = path.resolve( 'assets/test-image-2.png' ); -test.describe( 'Gallery Block (F.4)', () => { +test.describe( 'Gallery Block', () => { test( 'should upload multiple images to a gallery', async ( { page } ) => { const editor = new EditorPage( page ); await editor.setup(); diff --git a/e2e/image-upload.spec.js b/e2e/image-upload.spec.js index dc4012a9..8e1c4635 100644 --- a/e2e/image-upload.spec.js +++ b/e2e/image-upload.spec.js @@ -11,7 +11,7 @@ import EditorPage from './editor-page'; const TEST_IMAGE = path.resolve( import.meta.dirname, 'assets/test-image.png' ); -test.describe( 'Image Upload (S.2)', () => { +test.describe( 'Image Upload', () => { test( 'should upload an image via the Image block', async ( { page } ) => { const editor = new EditorPage( page ); await editor.setup(); diff --git a/e2e/pattern-insertion.spec.js b/e2e/pattern-insertion.spec.js index db27e7c3..51088bac 100644 --- a/e2e/pattern-insertion.spec.js +++ b/e2e/pattern-insertion.spec.js @@ -9,7 +9,7 @@ import { test, expect } from '@playwright/test'; import EditorPage from './editor-page'; import { credentials } from './wp-env-fixtures'; -test.describe( 'Pattern Insertion (F.5)', () => { +test.describe( 'Pattern Insertion', () => { test( 'should fetch patterns from the WP REST API', async () => { // Verify that patterns are available from the wp-env backend, // even though GBK delivers them through the native inserter bridge diff --git a/e2e/text-alignment.spec.js b/e2e/text-alignment.spec.js index 26ffdb07..bd29d261 100644 --- a/e2e/text-alignment.spec.js +++ b/e2e/text-alignment.spec.js @@ -8,7 +8,7 @@ import { test, expect } from '@playwright/test'; */ import EditorPage from './editor-page'; -test.describe( 'Text Alignment (F.1)', () => { +test.describe( 'Text Alignment', () => { test( 'should align a paragraph block to center', async ( { page } ) => { const editor = new EditorPage( page ); await editor.setup(); diff --git a/e2e/video-upload.spec.js b/e2e/video-upload.spec.js index 30f73b77..84cd746e 100644 --- a/e2e/video-upload.spec.js +++ b/e2e/video-upload.spec.js @@ -11,7 +11,7 @@ import EditorPage from './editor-page'; const TEST_VIDEO = path.resolve( import.meta.dirname, 'assets/test-video.mp4' ); -test.describe( 'Video Upload (S.3)', () => { +test.describe( 'Video Upload', () => { test( 'should upload a video via the Video block', async ( { page } ) => { const editor = new EditorPage( page ); await editor.setup(); From d93ec3c54a45d8a6e15e0ae47b08614d1c1a0753 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 20 Feb 2026 14:02:13 -0500 Subject: [PATCH 08/11] ci: Run E2E tests on Linux with Docker for wp-env support Move the E2E step from the `mac` queue to `default` (Linux EC2 with Docker) so wp-env can start and provide the credentials file needed by all E2E tests. Also install Playwright with `--with-deps` on CI to pull in required system libraries on Linux. --- .buildkite/pipeline.yml | 5 +++++ Makefile | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 29190801..65119b24 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -25,9 +25,14 @@ steps: - label: ':performing_arts: Test Web E2E' depends_on: build-react + agents: + queue: default command: | buildkite-agent artifact download dist.tar.gz . tar -xzf dist.tar.gz + echo "--- :docker: Starting wp-env" + make wp-env-start + echo "--- :performing_arts: Running E2E tests" make test-e2e plugins: *plugins diff --git a/Makefile b/Makefile index de279300..c5f1592c 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,7 @@ e2e-dependencies: npm-dependencies ## Install E2E test dependencies echo "--- :white_check_mark: Playwright Chromium is already installed."; \ elif [ -n "$$CI" ]; then \ echo "--- :chromium: Installing Playwright Chromium"; \ - npx playwright install chromium; \ + npx playwright install --with-deps chromium; \ else \ echo ""; \ echo "Playwright Chromium browser is not installed."; \ From 1bb94025881199243aba54d4525a18701a3c2010 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 20 Feb 2026 15:18:50 -0500 Subject: [PATCH 09/11] ci: Revert E2E test task to macOS agent --- .buildkite/pipeline.yml | 2 -- Makefile | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 65119b24..aa4e398c 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -25,8 +25,6 @@ steps: - label: ':performing_arts: Test Web E2E' depends_on: build-react - agents: - queue: default command: | buildkite-agent artifact download dist.tar.gz . tar -xzf dist.tar.gz diff --git a/Makefile b/Makefile index c5f1592c..de279300 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,7 @@ e2e-dependencies: npm-dependencies ## Install E2E test dependencies echo "--- :white_check_mark: Playwright Chromium is already installed."; \ elif [ -n "$$CI" ]; then \ echo "--- :chromium: Installing Playwright Chromium"; \ - npx playwright install --with-deps chromium; \ + npx playwright install chromium; \ else \ echo ""; \ echo "Playwright Chromium browser is not installed."; \ From 3f7b1d8e2e7f3898067fcaadd5b2c17eb24a4914 Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Fri, 20 Feb 2026 21:44:05 +0100 Subject: [PATCH 10/11] Trying to use `mac-metal` as the queue to see if the issue with Playwright is related to trying to run it within a virtualized environment (AMI/VM) or if it's unrelated --- .buildkite/pipeline.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index aa4e398c..9aaff5fc 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -33,6 +33,8 @@ steps: echo "--- :performing_arts: Running E2E tests" make test-e2e plugins: *plugins + agents: + queue: mac-metal - label: ':android: Publish Android Library' depends_on: build-react From 2e1ca218674678e78de637c267cda494ccf8c83d Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Fri, 20 Feb 2026 21:48:25 +0100 Subject: [PATCH 11/11] Revert "Trying to use `mac-metal` as the queue to see if the issue with Playwright is related to trying to run it within a virtualized environment (AMI/VM) or if it's unrelated" This reverts commit 7b5aafefc3b5a6629e36cf150ff819918e18861b. --- .buildkite/pipeline.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 9aaff5fc..aa4e398c 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -33,8 +33,6 @@ steps: echo "--- :performing_arts: Running E2E tests" make test-e2e plugins: *plugins - agents: - queue: mac-metal - label: ':android: Publish Android Library' depends_on: build-react