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 9929ebf19d01a1..3b187625fd47cf 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 7705ff11cbff9d..00000000000000 --- 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`] = ` -" --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.
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`] = ` -" -1
2
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
a
- - - -1
- - - -2
- - - -b
-" -`; - -exports[`RichText should transform backtick to code 1`] = ` -" -A backtick
A \`backtick\`
-" -`; - -exports[`RichText should transform when typing backtick over selection 1`] = ` -" -A selection
test.
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 ff651e61d52ea9..00000000000000 --- 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/test/e2e/specs/editor/various/rich-text.spec.js b/test/e2e/specs/editor/various/rich-text.spec.js new file mode 100644 index 00000000000000..75c8d40e191979 --- /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 collapse', 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: 'Abacktick
' },
+ },
+ ] );
+
+ 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: '1a
' },
+ },
+ ] );
+ } );
+
+ 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' },
+ } )
+ );
+ } );
+} );