From 1170a382e4faee63b9a246cfce170888c2a22391 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 31 Aug 2023 22:11:10 +0200 Subject: [PATCH] Migrate RichText e2e tests to Playwright (#53493) --- .../src/page-utils/press-keys.ts | 30 +- .../__snapshots__/rich-text.test.js.snap | 231 ----- .../specs/editor/various/rich-text.test.js | 570 ------------ .../performance/post-editor.test.results.json | 19 + .../performance/site-editor.test.results.json | 60 ++ .../specs/editor/various/rich-text.spec.js | 826 ++++++++++++++++++ 6 files changed, 924 insertions(+), 812 deletions(-) delete mode 100644 packages/e2e-tests/specs/editor/various/__snapshots__/rich-text.test.js.snap delete mode 100644 packages/e2e-tests/specs/editor/various/rich-text.test.js create mode 100644 packages/e2e-tests/specs/performance/post-editor.test.results.json create mode 100644 packages/e2e-tests/specs/performance/site-editor.test.results.json create mode 100644 test/e2e/specs/editor/various/rich-text.spec.js diff --git a/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts b/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts index 9929ebf19d01a..3b187625fd47c 100644 --- a/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts +++ b/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts @@ -20,11 +20,13 @@ import { } from '@wordpress/keycodes'; let clipboardDataHolder: { - plainText: string; - html: string; + 'text/plain': string; + 'text/html': string; + 'rich-text': string; } = { - plainText: '', - html: '', + 'text/plain': '', + 'text/html': '', + 'rich-text': '', }; /** @@ -38,11 +40,12 @@ let clipboardDataHolder: { */ export function setClipboardData( this: PageUtils, - { plainText = '', html = '' }: typeof clipboardDataHolder + { plainText = '', html = '' } ) { clipboardDataHolder = { - plainText, - html, + 'text/plain': plainText, + 'text/html': html, + 'rich-text': '', }; } @@ -57,11 +60,15 @@ async function emulateClipboard( page: Page, type: 'copy' | 'cut' | 'paste' ) { if ( _type === 'paste' ) { clipboardDataTransfer.setData( 'text/plain', - _clipboardData.plainText + _clipboardData[ 'text/plain' ] ); clipboardDataTransfer.setData( 'text/html', - _clipboardData.html + _clipboardData[ 'text/html' ] + ); + clipboardDataTransfer.setData( + 'rich-text', + _clipboardData[ 'rich-text' ] ); } else { const selection = canvasDoc.defaultView.getSelection()!; @@ -91,8 +98,9 @@ async function emulateClipboard( page: Page, type: 'copy' | 'cut' | 'paste' ) { ); return { - plainText: clipboardDataTransfer.getData( 'text/plain' ), - html: clipboardDataTransfer.getData( 'text/html' ), + 'text/plain': clipboardDataTransfer.getData( 'text/plain' ), + 'text/html': clipboardDataTransfer.getData( 'text/html' ), + 'rich-text': clipboardDataTransfer.getData( 'rich-text' ), }; }, [ type, clipboardDataHolder ] as const diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/rich-text.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/rich-text.test.js.snap deleted file mode 100644 index 7705ff11cbff9..0000000000000 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/rich-text.test.js.snap +++ /dev/null @@ -1,231 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RichText should apply active formatting for inline paste 1`] = ` -" -

1323

-" -`; - -exports[`RichText should apply formatting when selection is collapsed 1`] = ` -" -

Some bold.

-" -`; - -exports[`RichText should apply formatting with primary shortcut 1`] = ` -" -

test

-" -`; - -exports[`RichText should apply multiple formats when selection is collapsed 1`] = ` -" -

1.

-" -`; - -exports[`RichText should copy/paste heading 1`] = ` -" -

Heading

- - - -

Heading

-" -`; - -exports[`RichText should handle Home and End keys 1`] = ` -" -

-12+

-" -`; - -exports[`RichText should handle change in tag name gracefully 1`] = ` -" -

-" -`; - -exports[`RichText should keep internal selection after blur 1`] = ` -" -

12

-" -`; - -exports[`RichText should make bold after split and merge 1`] = ` -" -

12

-" -`; - -exports[`RichText should navigate arround emoji 1`] = ` -" -

1🍓

-" -`; - -exports[`RichText should navigate consecutive format boundaries 1`] = ` -" -

12

-" -`; - -exports[`RichText should navigate consecutive format boundaries 2`] = ` -" -

1-2

-" -`; - -exports[`RichText should not format text after code backtick 1`] = ` -" -

A backtick and more.

-" -`; - -exports[`RichText should not lose selection direction 1`] = ` -" -

12-3

-" -`; - -exports[`RichText should not split rich text on inline paste 1`] = ` -" -

123

-" -`; - -exports[`RichText should not split rich text on inline paste with formatting 1`] = ` -" -

a123b

-" -`; - -exports[`RichText should not undo backtick transform with backspace after selection change 1`] = `""`; - -exports[`RichText should not undo backtick transform with backspace after typing 1`] = `""`; - -exports[`RichText should only mutate text data on input 1`] = ` -" -

1234

-" -`; - -exports[`RichText should paste list contents into paragraph 1`] = ` -" - - - - - -" -`; - -exports[`RichText should paste paragraph contents into list 1`] = ` -" -

1
2

- - - - -" -`; - -exports[`RichText should preserve internal formatting 1`] = ` -" -

1

-" -`; - -exports[`RichText should preserve internal formatting 2`] = ` -" -

1

- - - -

1

-" -`; - -exports[`RichText should return focus when pressing formatting button 1`] = ` -" -

Some bold.

-" -`; - -exports[`RichText should run input rules after composition end 1`] = ` -" -

a

-" -`; - -exports[`RichText should split rich text on paste 1`] = ` -" -

a

- - - -

1

- - - -

2

- - - -

b

-" -`; - -exports[`RichText should transform backtick to code 1`] = ` -" -

A backtick

-" -`; - -exports[`RichText should transform backtick to code 2`] = ` -" -

A \`backtick\`

-" -`; - -exports[`RichText should transform when typing backtick over selection 1`] = ` -" -

A selection test.

-" -`; - -exports[`RichText should transform when typing backtick over selection 2`] = ` -" -

A \`selection\` test.

-" -`; - -exports[`RichText should undo backtick transform with backspace 1`] = ` -" -

\`a\`

-" -`; - -exports[`RichText should update internal selection after fresh focus 1`] = ` -" -

12

-" -`; diff --git a/packages/e2e-tests/specs/editor/various/rich-text.test.js b/packages/e2e-tests/specs/editor/various/rich-text.test.js deleted file mode 100644 index ff651e61d52ea..0000000000000 --- a/packages/e2e-tests/specs/editor/various/rich-text.test.js +++ /dev/null @@ -1,570 +0,0 @@ -/** - * WordPress dependencies - */ -import { - createNewPost, - getEditedPostContent, - insertBlock, - clickBlockAppender, - pressKeyWithModifier, - showBlockToolbar, - clickBlockToolbarButton, - canvas, -} from '@wordpress/e2e-test-utils'; - -describe( 'RichText', () => { - beforeEach( async () => { - await createNewPost(); - } ); - - it( 'should handle change in tag name gracefully', async () => { - // Regression test: The heading block changes the tag name of its - // RichText element. Historically this has been prone to breakage, - // because the Editable component prevents rerenders, so React cannot - // update the element by itself. - // - // See: https://github.com/WordPress/gutenberg/issues/3091 - await insertBlock( 'Heading' ); - await page.waitForSelector( '[aria-label="Change level"]' ); - await page.click( '[aria-label="Change level"]' ); - await page.click( '[aria-label="Heading 3"]' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should apply formatting with primary shortcut', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'test' ); - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'b' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should apply formatting when selection is collapsed', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'Some ' ); - // All following characters should now be bold. - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( 'bold' ); - // All following characters should no longer be bold. - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '.' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should apply multiple formats when selection is collapsed', async () => { - await clickBlockAppender(); - await pressKeyWithModifier( 'primary', 'b' ); - await pressKeyWithModifier( 'primary', 'i' ); - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'primary', 'i' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '.' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should not highlight more than one format', async () => { - await clickBlockAppender(); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( ' 2' ); - await pressKeyWithModifier( 'shift', 'ArrowLeft' ); - await pressKeyWithModifier( 'primary', 'b' ); - - const count = await canvas().evaluate( - () => - document.querySelectorAll( '*[data-rich-text-format-boundary]' ) - .length - ); - - expect( count ).toBe( 1 ); - } ); - - it( 'should return focus when pressing formatting button', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'Some ' ); - await showBlockToolbar(); - await page.click( '[aria-label="Bold"]' ); - await page.keyboard.type( 'bold' ); - await showBlockToolbar(); - await page.click( '[aria-label="Bold"]' ); - await page.keyboard.type( '.' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should transform backtick to code', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'A `backtick`' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - - await pressKeyWithModifier( 'primary', 'z' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should undo backtick transform with backspace', async () => { - await clickBlockAppender(); - await page.keyboard.type( '`a`' ); - await page.keyboard.press( 'Backspace' ); - - // Expect "`a`" to be restored. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should not undo backtick transform with backspace after typing', async () => { - await clickBlockAppender(); - await page.keyboard.type( '`a`' ); - await page.keyboard.type( 'b' ); - await page.keyboard.press( 'Backspace' ); - await page.keyboard.press( 'Backspace' ); - - // Expect "a" to be deleted. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should not undo backtick transform with backspace after selection change', async () => { - await clickBlockAppender(); - await page.keyboard.type( '`a`' ); - await page.evaluate( () => new Promise( window.requestIdleCallback ) ); - // Move inside format boundary. - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.press( 'Backspace' ); - - // Expect "a" to be deleted. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should not format text after code backtick', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'A `backtick` and more.' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should transform when typing backtick over selection', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'A selection test.' ); - await page.keyboard.press( 'Home' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.press( 'ArrowRight' ); - await pressKeyWithModifier( 'shiftAlt', 'ArrowRight' ); - await page.keyboard.type( '`' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // Should undo the transform. - await pressKeyWithModifier( 'primary', 'z' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should only mutate text data on input', async () => { - await clickBlockAppender(); - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '3' ); - - await canvas().evaluate( () => { - let called; - const { body } = document; - const config = { - attributes: true, - childList: true, - characterData: true, - subtree: true, - }; - - const mutationObserver = new MutationObserver( ( records ) => { - if ( called || records.length > 1 ) { - throw new Error( 'Typing should only mutate once.' ); - } - - records.forEach( ( record ) => { - if ( record.type !== 'characterData' ) { - throw new Error( - `Typing mutated more than character data: ${ record.type }` - ); - } - } ); - - called = true; - } ); - - mutationObserver.observe( body, config ); - - window.unsubscribes = [ () => mutationObserver.disconnect() ]; - - document.addEventListener( - 'selectionchange', - () => { - function throwMultipleSelectionChange() { - throw new Error( - 'Typing should only emit one selection change event.' - ); - } - - document.addEventListener( - 'selectionchange', - throwMultipleSelectionChange, - { - once: true, - } - ); - - window.unsubscribes.push( () => { - document.removeEventListener( - 'selectionchange', - throwMultipleSelectionChange - ); - } ); - }, - { once: true } - ); - } ); - - await page.keyboard.type( '4' ); - - await canvas().evaluate( () => { - // The selection change event should be called once. If there's only - // one item in `window.unsubscribes`, it means that only one - // function is present to disconnect the `mutationObserver`. - if ( window.unsubscribes.length === 1 ) { - throw new Error( - 'The selection change event listener was never called.' - ); - } - - window.unsubscribes.forEach( ( unsubscribe ) => unsubscribe() ); - } ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should not lose selection direction', async () => { - await clickBlockAppender(); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '23' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.down( 'Shift' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.up( 'Shift' ); - - // There should be no selection. The following should insert "-" without - // deleting the numbers. - await page.keyboard.type( '-' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should handle Home and End keys', async () => { - await page.keyboard.press( 'Enter' ); - - // Wait for rich text editor to load. - await canvas().waitForSelector( '.block-editor-rich-text__editable' ); - - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '12' ); - await pressKeyWithModifier( 'primary', 'b' ); - - await page.keyboard.press( 'Home' ); - await page.keyboard.type( '-' ); - await page.keyboard.press( 'End' ); - await page.keyboard.type( '+' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should update internal selection after fresh focus', async () => { - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( '1' ); - await page.keyboard.press( 'Tab' ); - await pressKeyWithModifier( 'shift', 'Tab' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'b' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should keep internal selection after blur', async () => { - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( '1' ); - // Simulate moving focus to a different app, then moving focus back, - // without selection being changed. - await canvas().evaluate( () => { - const activeElement = document.activeElement; - activeElement.blur(); - activeElement.focus(); - } ); - // Wait for the next animation frame, see the focus event listener in - // RichText. - await page.evaluate( - () => new Promise( window.requestAnimationFrame ) - ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'b' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should split rich text on paste', async () => { - await clickBlockAppender(); - await page.keyboard.type( '1' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'x' ); - await page.keyboard.type( 'ab' ); - await page.keyboard.press( 'ArrowLeft' ); - await pressKeyWithModifier( 'primary', 'v' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should not split rich text on inline paste', async () => { - await clickBlockAppender(); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'x' ); - await page.keyboard.type( '13' ); - await page.keyboard.press( 'ArrowLeft' ); - await pressKeyWithModifier( 'primary', 'v' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should not split rich text on inline paste with formatting', async () => { - await clickBlockAppender(); - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '3' ); - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'x' ); - await page.keyboard.type( 'ab' ); - await page.keyboard.press( 'ArrowLeft' ); - await pressKeyWithModifier( 'primary', 'v' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should make bold after split and merge', async () => { - await clickBlockAppender(); - await page.keyboard.type( '1' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.press( 'Backspace' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '2' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should apply active formatting for inline paste', async () => { - await clickBlockAppender(); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '1' ); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '3' ); - await pressKeyWithModifier( 'shift', 'ArrowLeft' ); - await pressKeyWithModifier( 'primary', 'c' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowLeft' ); - await pressKeyWithModifier( 'primary', 'v' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should preserve internal formatting', async () => { - await clickBlockAppender(); - - // Add text and select to color. - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'primary', 'a' ); - await clickBlockToolbarButton( 'More' ); - - const button = await page.waitForXPath( - `//button[text()='Highlight']` - ); - // Clicks may fail if the button is out of view. Assure it is before click. - await button.evaluate( ( element ) => element.scrollIntoView() ); - await button.click(); - - // Wait for the popover with "Text" tab to appear. - await page.waitForXPath( - '//button[@role="tab"][@aria-selected="true"][text()="Text"]' - ); - // Initial focus is on the "Text" tab. - // Tab to the "Custom color picker". - await page.keyboard.press( 'Tab' ); - // Tab to black. - await page.keyboard.press( 'Tab' ); - // Select color other than black. - await page.keyboard.press( 'Tab' ); - await page.keyboard.press( 'Enter' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // Dismiss color picker popover. - await page.keyboard.press( 'Escape' ); - - // Navigate to the block. - await page.keyboard.press( 'Tab' ); - - // Copy the colored text. - await pressKeyWithModifier( 'primary', 'c' ); - - // Collapse the selection to the end. - await page.keyboard.press( 'ArrowRight' ); - - // Create a new paragraph. - await page.keyboard.press( 'Enter' ); - - // Paste the colored text. - await pressKeyWithModifier( 'primary', 'v' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should paste paragraph contents into list', async () => { - await clickBlockAppender(); - - // Create two lines of text in a paragraph. - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'shift', 'Enter' ); - await page.keyboard.type( '2' ); - - // Select all and copy. - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'c' ); - - // Collapse the selection to the end. - await page.keyboard.press( 'ArrowRight' ); - - // Create a list. - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( '* ' ); - - // Paste paragraph contents. - await pressKeyWithModifier( 'primary', 'v' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should paste list contents into paragraph', async () => { - await clickBlockAppender(); - - // Create an indented list of two lines. - await page.keyboard.type( '* 1' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( ' 2' ); - - // Select all text. - await pressKeyWithModifier( 'primary', 'a' ); - // Select the nested list. - await pressKeyWithModifier( 'primary', 'a' ); - // Select the parent list item. - await pressKeyWithModifier( 'primary', 'a' ); - // Select all the parent list item text. - await pressKeyWithModifier( 'primary', 'a' ); - // Select the entire list. - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'c' ); - - await page.keyboard.press( 'Enter' ); - - // Paste paragraph contents. - await pressKeyWithModifier( 'primary', 'v' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should navigate arround emoji', async () => { - await clickBlockAppender(); - await page.keyboard.type( '🍓' ); - // Only one press on arrow left should be required to move in front of - // the emoji. - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.type( '1' ); - - // Expect '1🍓'. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should run input rules after composition end', async () => { - await clickBlockAppender(); - // Puppeteer doesn't support composition, so emulate it by inserting - // text in the DOM directly, setting selection in the right place, and - // firing `compositionend`. - // See https://github.com/puppeteer/puppeteer/issues/4981. - await canvas().evaluate( async () => { - document.activeElement.textContent = '`a`'; - const selection = window.getSelection(); - // The `selectionchange` and `compositionend` events should run in separate event - // loop ticks to process all data store updates in time. Native events would be - // scheduled the same way. - selection.selectAllChildren( document.activeElement ); - selection.collapseToEnd(); - await new Promise( ( r ) => setTimeout( r, 0 ) ); - document.activeElement.dispatchEvent( - new CompositionEvent( 'compositionend' ) - ); - } ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should navigate consecutive format boundaries', async () => { - await clickBlockAppender(); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'primary', 'b' ); - await pressKeyWithModifier( 'primary', 'i' ); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'i' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // Should move into the second format. - await page.keyboard.press( 'ArrowLeft' ); - // Should move to the start of the second format. - await page.keyboard.press( 'ArrowLeft' ); - // Should move between the first and second format. - await page.keyboard.press( 'ArrowLeft' ); - - await page.keyboard.type( '-' ); - - // Expect: 1-2 - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - test( 'should copy/paste heading', async () => { - await insertBlock( 'Heading' ); - await page.keyboard.type( 'Heading' ); - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'c' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.press( 'Enter' ); - await pressKeyWithModifier( 'primary', 'v' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); -} ); diff --git a/packages/e2e-tests/specs/performance/post-editor.test.results.json b/packages/e2e-tests/specs/performance/post-editor.test.results.json new file mode 100644 index 0000000000000..4317d3ef68807 --- /dev/null +++ b/packages/e2e-tests/specs/performance/post-editor.test.results.json @@ -0,0 +1,19 @@ +{ + "serverResponse": [], + "firstPaint": [], + "domContentLoaded": [], + "loaded": [], + "firstContentfulPaint": [], + "firstBlock": [], + "type": [ + 75.483, 96.376, 82.063, 101.192, 87.573, 56.599000000000004, + 63.778999999999996, 93.079, 98.277, 95.365, 94.48599999999999, 55.739, + 81.715, 66.875, 60.897, 54.249, 52.537, 58.745, 49.615 + ], + "typeContainer": [], + "focus": [], + "listViewOpen": [], + "inserterOpen": [], + "inserterHover": [], + "inserterSearch": [] +} diff --git a/packages/e2e-tests/specs/performance/site-editor.test.results.json b/packages/e2e-tests/specs/performance/site-editor.test.results.json new file mode 100644 index 0000000000000..a043c552f12d3 --- /dev/null +++ b/packages/e2e-tests/specs/performance/site-editor.test.results.json @@ -0,0 +1,60 @@ +{ + "serverResponse": [ + 409.40000009536743, 405.59999990463257, 410.09999990463257 + ], + "firstPaint": [ 438.59999990463257, 447.3999996185303, 449.59999990463257 ], + "domContentLoaded": [ 676, 690.0999999046326, 693 ], + "loaded": [ 1405.6999998092651, 1400.3999996185303, 1425.9000000953674 ], + "firstContentfulPaint": [ + 903.2999997138977, 921.0999999046326, 925.2999997138977 + ], + "firstBlock": [ + 3166.7999997138977, 3206.5999999046326, 3238.4000000953674 + ], + "type": [ + 81.45900000000002, 37.088, 36.051, 38.596000000000004, 49.931, 40.322, + 38.99999999999999, 34.235, 33.608999999999995, 32.88399999999999, 30.44, + 37.113, 31.534999999999997, 33.792, 36.942, 35.251000000000005, 33.722, + 33.471999999999994, 35.26499999999999, 29.682, 30.173, + 30.674999999999997, 35.668000000000006, 38.278, 37.62, 37.562, 38.091, + 32.237, 28.119999999999997, 31.342, 39.89, 37.443, 37.761, 40.262, + 37.922, 30.727, 30.955000000000002, 36.53000000000001, 32.293, 37.299, + 38.55800000000001, 39.85699999999999, 33.721999999999994, 30.139, + 29.294, 31.016, 35.7, 36.839, 31.061000000000003, 29.540000000000003, + 48.998999999999995, 35.423, 33.650000000000006, 29.404999999999998, + 32.744, 30.584999999999997, 30.705, 31.873, 28.907, 30.516, 30.882, + 29.257, 29.794, 31.150000000000002, 32.095, 31.066000000000003, 32.872, + 31.894, 31.331, 31.796, 31.675, 30.427999999999997, 30.872, 30.974, + 32.707, 31.849999999999998, 28.935, 28.441000000000003, + 30.566000000000003, 29.014, 33.158, 32.272, 28.990000000000002, 28.76, + 28.967000000000002, 29.418, 28.503, 31.255000000000003, 28.703, + 30.369000000000003, 34.910000000000004, 31.03, 28.523, + 32.361999999999995, 33.870000000000005, 30.11, 30.944000000000003, + 28.601, 30.572999999999997, 33.216, 30.822, 28.892000000000003, + 32.95099999999999, 31.228, 28.251, 34.89, 30.131000000000004, 29.395, + 31.557000000000002, 28.137, 32.051, 38.242, 36.382999999999996, 35.037, + 36.2, 31.717999999999996, 28.927999999999997, 32.540000000000006, + 35.448, 28.292, 35.059999999999995, 31.345000000000002, 36.122, 31.69, + 28.492, 29.308, 30.793000000000003, 28.784000000000002, + 28.275999999999996, 36.577999999999996, 30.220000000000002, 35.832, + 31.192, 36.102999999999994, 30.733999999999998, 30.574, + 35.455999999999996, 29.963, 37.967, 29.323999999999998, 36.643, + 31.200000000000003, 36.864999999999995, 32.344, 30.321, 29.214, 28.627, + 29.71, 29.006, 36.067, 29.583, 29.562, 37.795, 30.166999999999998, + 30.811999999999998, 33.319, 32.939, 39.233999999999995, 28.856, + 34.81700000000001, 30.324, 33.611000000000004, 33.707, + 30.191000000000003, 29.191, 29.23, 30.715, 29.281, 28.168, + 33.449000000000005, 36.36600000000001, 29.086, 30.589, 29.13, 28.789, + 29.156000000000002, 43.327, 34.439, 28.777, 30.586, 28.973000000000003, + 30.026, 40.023, 30.203, 28.328000000000003, 30.825000000000003, 29.739, + 31.504, 43.708000000000006, 29.296999999999997, 32.294, 31.733, 30.44, + 28.879, 30.349999999999998, 29.466, 29.302999999999997, 30, + 29.468999999999998, 28.740000000000002 + ], + "typeContainer": [], + "focus": [], + "inserterOpen": [], + "inserterHover": [], + "inserterSearch": [], + "listViewOpen": [] +} diff --git a/test/e2e/specs/editor/various/rich-text.spec.js b/test/e2e/specs/editor/various/rich-text.spec.js new file mode 100644 index 0000000000000..1644bf50252c9 --- /dev/null +++ b/test/e2e/specs/editor/various/rich-text.spec.js @@ -0,0 +1,826 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'RichText', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'should handle change in tag name gracefully', async ( { + page, + editor, + } ) => { + // Regression test: The heading block changes the tag name of its + // RichText element. Historically this has been prone to breakage, + // because the Editable component prevents rerenders, so React cannot + // update the element by itself. + // + // See: https://github.com/WordPress/gutenberg/issues/3091 + await editor.insertBlock( { name: 'core/heading' } ); + await editor.clickBlockToolbarButton( 'Change level' ); + await page.locator( 'button[aria-label="Heading 3"]' ).click(); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/heading', + attributes: { level: 3 }, + }, + ] ); + } ); + + test( 'should apply formatting with primary shortcut', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'test' ); + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+b' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'test' }, + }, + ] ); + } ); + + test( 'should apply formatting when selection is collapsed', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'Some ' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( 'bold' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '.' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'Some bold.' }, + }, + ] ); + } ); + + test( 'should apply multiple formats when selection is collapsed', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await pageUtils.pressKeys( 'primary+b' ); + await pageUtils.pressKeys( 'primary+i' ); + await page.keyboard.type( '1' ); + await pageUtils.pressKeys( 'primary+i' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '.' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '1.' }, + }, + ] ); + } ); + + test( 'should not highlight more than one format', async ( { + page, + editor, + pageUtils, + } ) => { + await page.keyboard.press( 'Enter' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '1' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( ' 2' ); + await pageUtils.pressKeys( 'shift+ArrowLeft' ); + await pageUtils.pressKeys( 'primary+b' ); + + const count = await editor.canvas.evaluate( + () => + document.querySelectorAll( '*[data-rich-text-format-boundary]' ) + .length + ); + expect( count ).toBe( 1 ); + } ); + + test( 'should return focus when pressing formatting button', async ( { + page, + editor, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'Some ' ); + await editor.clickBlockToolbarButton( 'Bold' ); + await page.keyboard.type( 'bold' ); + await editor.clickBlockToolbarButton( 'Bold' ); + await page.keyboard.type( '.' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'Some bold.' }, + }, + ] ); + } ); + + test( 'should transform backtick to code', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'A `backtick`' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'A backtick' }, + }, + ] ); + + await pageUtils.pressKeys( 'primary+z' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { name: 'core/paragraph' }, + ] ); + } ); + + test( 'should undo backtick transform with backspace', async ( { + page, + editor, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '`a`' ); + await page.keyboard.press( 'Backspace' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '`a`' }, + }, + ] ); + } ); + + test( 'should not undo backtick transform with backspace after typing', async ( { + page, + editor, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '`a`' ); + await page.keyboard.type( 'b' ); + await page.keyboard.press( 'Backspace' ); + await page.keyboard.press( 'Backspace' ); + + expect( await editor.getBlocks() ).toMatchObject( [] ); + } ); + + test( 'should not undo backtick transform with backspace after selection change', async ( { + page, + editor, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '`a`' ); + await page.evaluate( () => new Promise( window.requestIdleCallback ) ); + // Move inside format boundary. + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'Backspace' ); + + expect( await editor.getBlocks() ).toMatchObject( [] ); + } ); + + test( 'should not format text after code backtick', async ( { + page, + editor, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'A `backtick` and more.' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'A backtick and more.' }, + }, + ] ); + } ); + + test( 'should transform when typing backtick over selection', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'A selection test.' ); + await page.keyboard.press( 'Home' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'ArrowRight' ); + await pageUtils.pressKeys( 'shiftAlt+ArrowRight' ); + await page.keyboard.type( '`' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'A selection test.' }, + }, + ] ); + + await pageUtils.pressKeys( 'primary+z' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'A `selection` test.' }, + }, + ] ); + } ); + + test( 'should only mutate text data on input', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '1' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '2' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '3' ); + + await editor.canvas.evaluate( () => { + let called; + const { body } = document; + const config = { + attributes: true, + childList: true, + characterData: true, + subtree: true, + }; + + const mutationObserver = new MutationObserver( ( records ) => { + if ( called || records.length > 1 ) { + throw new Error( 'Typing should only mutate once.' ); + } + + records.forEach( ( record ) => { + if ( record.type !== 'characterData' ) { + throw new Error( + `Typing mutated more than character data: ${ record.type }` + ); + } + } ); + + called = true; + } ); + + mutationObserver.observe( body, config ); + + window.unsubscribes = [ () => mutationObserver.disconnect() ]; + + document.addEventListener( + 'selectionchange', + () => { + function throwMultipleSelectionChange() { + throw new Error( + 'Typing should only emit one selection change event.' + ); + } + + document.addEventListener( + 'selectionchange', + throwMultipleSelectionChange, + { + once: true, + } + ); + + window.unsubscribes.push( () => { + document.removeEventListener( + 'selectionchange', + throwMultipleSelectionChange + ); + } ); + }, + { once: true } + ); + } ); + + await page.keyboard.type( '4' ); + + await editor.canvas.evaluate( () => { + // The selection change event should be called once. If there's only + // one item in `window.unsubscribes`, it means that only one + // function is present to disconnect the `mutationObserver`. + if ( window.unsubscribes.length === 1 ) { + throw new Error( + 'The selection change event listener was never called.' + ); + } + + window.unsubscribes.forEach( ( unsubscribe ) => unsubscribe() ); + } ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '1234' }, + }, + ] ); + } ); + + test( 'should not lose selection direction', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '1' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '23' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.down( 'Shift' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.up( 'Shift' ); + + // There should be no selection. The following should insert "-" without + // deleting the numbers. + await page.keyboard.type( '-' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '12-3' }, + }, + ] ); + } ); + + test( 'should handle Home and End keys', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '12' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.press( 'Home' ); + await page.keyboard.type( '-' ); + await page.keyboard.press( 'End' ); + await page.keyboard.type( '+' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '-12+' }, + }, + ] ); + } ); + + test( 'should update internal selection after fresh focus', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '1' ); + await page.keyboard.press( 'Tab' ); + await pageUtils.pressKeys( 'shift+Tab' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '2' ); + await pageUtils.pressKeys( 'primary+b' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '12' }, + }, + ] ); + } ); + + test( 'should keep internal selection after blur', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '1' ); + // Simulate moving focus to a different app, then moving focus back, + // without selection being changed. + await editor.canvas.evaluate( () => { + const activeElement = document.activeElement; + activeElement.blur(); + activeElement.focus(); + } ); + // Wait for the next animation frame, see the focus event listener in + // RichText. + await page.evaluate( + () => new Promise( window.requestAnimationFrame ) + ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '2' ); + await pageUtils.pressKeys( 'primary+b' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '12' }, + }, + ] ); + } ); + + test( 'should split rich text on paste', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '1' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '2' ); + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+x' ); + await page.keyboard.type( 'ab' ); + await page.keyboard.press( 'ArrowLeft' ); + await pageUtils.pressKeys( 'primary+v' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'a' }, + }, + { + name: 'core/paragraph', + attributes: { content: '1' }, + }, + { + name: 'core/paragraph', + attributes: { content: '2' }, + }, + { + name: 'core/paragraph', + attributes: { content: 'b' }, + }, + ] ); + } ); + + test( 'should not split rich text on inline paste', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '2' ); + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+x' ); + await page.keyboard.type( '13' ); + await page.keyboard.press( 'ArrowLeft' ); + await pageUtils.pressKeys( 'primary+v' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '123' }, + }, + ] ); + } ); + + test( 'should not split rich text on inline paste with formatting', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '1' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '2' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '3' ); + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+x' ); + await page.keyboard.type( 'ab' ); + await page.keyboard.press( 'ArrowLeft' ); + await pageUtils.pressKeys( 'primary+v' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'a123b' }, + }, + ] ); + } ); + + test( 'should make bold after split and merge', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '1' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Backspace' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '2' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '12' }, + }, + ] ); + } ); + + test( 'should apply active formatting for inline paste', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '1' ); + await page.keyboard.type( '2' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '3' ); + await pageUtils.pressKeys( 'shift+ArrowLeft' ); + await pageUtils.pressKeys( 'primary+c' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowLeft' ); + await pageUtils.pressKeys( 'primary+v' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '1323' }, + }, + ] ); + } ); + + test( 'should preserve internal formatting', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + + // Add text and select to color. + await page.keyboard.type( '1' ); + await pageUtils.pressKeys( 'primary+a' ); + await editor.clickBlockToolbarButton( 'More' ); + await page.locator( 'button:text("Highlight")' ).click(); + + // Initial focus is on the "Text" tab. + // Tab to the "Custom color picker". + await page.keyboard.press( 'Tab' ); + // Tab to black. + await page.keyboard.press( 'Tab' ); + // Select color other than black. + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Enter' ); + + const result = { + name: 'core/paragraph', + attributes: { + content: + '1', + }, + }; + + expect( await editor.getBlocks() ).toMatchObject( [ result ] ); + + // Dismiss color picker popover. + await page.keyboard.press( 'Escape' ); + + // Navigate to the block. + await page.keyboard.press( 'Tab' ); + + // Copy the colored text. + await pageUtils.pressKeys( 'primary+c' ); + + // Collapse the selection to the end. + await page.keyboard.press( 'ArrowRight' ); + + // Create a new paragraph. + await page.keyboard.press( 'Enter' ); + + // Paste the colored text. + await pageUtils.pressKeys( 'primary+v' ); + + expect( await editor.getBlocks() ).toMatchObject( + Array( 2 ).fill( result ) + ); + } ); + + test( 'should paste paragraph contents into list', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + // Create two lines of text in a paragraph. + await page.keyboard.type( '1' ); + await pageUtils.pressKeys( 'shift+Enter' ); + await page.keyboard.type( '2' ); + + // Select all and copy. + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+c' ); + + // Collapse the selection to the end. + await page.keyboard.press( 'ArrowRight' ); + + // Create a list. + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '* ' ); + + // Paste paragraph contents. + await pageUtils.pressKeys( 'primary+v' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '1
2' }, + }, + { + name: 'core/list', + innerBlocks: [ + { + name: 'core/list-item', + attributes: { content: '1' }, + }, + { + name: 'core/list-item', + attributes: { content: '2' }, + }, + ], + }, + ] ); + } ); + + test( 'should paste list contents into paragraph', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + + // Create an indented list of two lines. + await page.keyboard.type( '* 1' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( ' 2' ); + + // Select all text. + await pageUtils.pressKeys( 'primary+a' ); + // Select the nested list. + await pageUtils.pressKeys( 'primary+a' ); + // Select the parent list item. + await pageUtils.pressKeys( 'primary+a' ); + // Select all the parent list item text. + await pageUtils.pressKeys( 'primary+a' ); + // Select the entire list. + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+c' ); + + await page.keyboard.press( 'Enter' ); + + // Paste paragraph contents. + await pageUtils.pressKeys( 'primary+v' ); + + expect( await editor.getBlocks() ).toMatchObject( + Array( 2 ).fill( { + name: 'core/list', + innerBlocks: [ + { + name: 'core/list-item', + attributes: { content: '1' }, + innerBlocks: [ + { + name: 'core/list', + innerBlocks: [ + { + name: 'core/list-item', + attributes: { content: '2' }, + }, + ], + }, + ], + }, + ], + } ) + ); + } ); + + test( 'should navigate arround emoji', async ( { page, editor } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '🍓' ); + // Only one press on arrow left should be required to move in front of + // the emoji. + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.type( '1' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '1🍓' }, + }, + ] ); + } ); + + test( 'should run input rules after composition end', async ( { + editor, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + // Playwright doesn't support composition, so emulate it by inserting + // text in the DOM directly, setting selection in the right place, and + // firing `compositionend`. + // See https://github.com/puppeteer/puppeteer/issues/4981. + await editor.canvas.evaluate( async () => { + document.activeElement.textContent = '`a`'; + const selection = window.getSelection(); + // The `selectionchange` and `compositionend` events should run in separate event + // loop ticks to process all data store updates in time. Native events would be + // scheduled the same way. + selection.selectAllChildren( document.activeElement ); + selection.collapseToEnd(); + await new Promise( ( r ) => setTimeout( r, 0 ) ); + document.activeElement.dispatchEvent( + new CompositionEvent( 'compositionend' ) + ); + } ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'a' }, + }, + ] ); + } ); + + test( 'should navigate consecutive format boundaries', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.canvas.click( 'role=button[name="Add default block"i]' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( '1' ); + await pageUtils.pressKeys( 'primary+b' ); + await pageUtils.pressKeys( 'primary+i' ); + await page.keyboard.type( '2' ); + await pageUtils.pressKeys( 'primary+i' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '12' }, + }, + ] ); + + // Should move into the second format. + await page.keyboard.press( 'ArrowLeft' ); + // Should move to the start of the second format. + await page.keyboard.press( 'ArrowLeft' ); + // Should move between the first and second format. + await page.keyboard.press( 'ArrowLeft' ); + + await page.keyboard.type( '-' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '1-2' }, + }, + ] ); + } ); + + test( 'should copy/paste heading', async ( { + page, + editor, + pageUtils, + } ) => { + await editor.insertBlock( { name: 'core/heading' } ); + await page.keyboard.type( 'Heading' ); + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+c' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'Enter' ); + await pageUtils.pressKeys( 'primary+v' ); + + expect( await editor.getBlocks() ).toMatchObject( + Array( 2 ).fill( { + name: 'core/heading', + attributes: { content: 'Heading' }, + } ) + ); + } ); +} );