diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 29190801..aa4e398c 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -28,6 +28,9 @@ steps: 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/docs/test-cases.md b/docs/test-cases.md index 63b2d1c0..374c982a 100644 --- a/docs/test-cases.md +++ b/docs/test-cases.md @@ -1,90 +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. - -### 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. - -### 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. diff --git a/e2e/assets/test-audio.mp3 b/e2e/assets/test-audio.mp3 new file mode 100644 index 00000000..269a03e8 Binary files /dev/null and b/e2e/assets/test-audio.mp3 differ diff --git a/e2e/assets/test-file.pdf b/e2e/assets/test-file.pdf new file mode 100644 index 00000000..7c249578 Binary files /dev/null and b/e2e/assets/test-file.pdf differ diff --git a/e2e/assets/test-image-2.png b/e2e/assets/test-image-2.png new file mode 100644 index 00000000..08cd6f2b Binary files /dev/null and b/e2e/assets/test-image-2.png differ diff --git a/e2e/assets/test-image.png b/e2e/assets/test-image.png new file mode 100644 index 00000000..32fe19d8 Binary files /dev/null and b/e2e/assets/test-image.png differ diff --git a/e2e/assets/test-video.mp4 b/e2e/assets/test-video.mp4 new file mode 100644 index 00000000..cafab890 Binary files /dev/null and b/e2e/assets/test-video.mp4 differ diff --git a/e2e/audio-upload.spec.js b/e2e/audio-upload.spec.js new file mode 100644 index 00000000..c944c281 --- /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', () => { + 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/color-gradient.spec.js b/e2e/color-gradient.spec.js new file mode 100644 index 00000000..ee1400b2 --- /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', () => { + 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/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/embed-content.spec.js b/e2e/embed-content.spec.js new file mode 100644 index 00000000..b92045df --- /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', () => { + 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/file-upload.spec.js b/e2e/file-upload.spec.js new file mode 100644 index 00000000..19185932 --- /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', () => { + 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..187968d5 --- /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', () => { + 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..8e1c4635 --- /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', () => { + 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/pattern-insertion.spec.js b/e2e/pattern-insertion.spec.js new file mode 100644 index 00000000..51088bac --- /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', () => { + 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..bd29d261 --- /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', () => { + 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' ); + } ); +} ); diff --git a/e2e/video-upload.spec.js b/e2e/video-upload.spec.js new file mode 100644 index 00000000..84cd746e --- /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', () => { + 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(); + } ); +} ); 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 ); +}