diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/block-deletion.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/block-deletion.test.js.snap deleted file mode 100644 index 8b0dcaec4067f7..00000000000000 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/block-deletion.test.js.snap +++ /dev/null @@ -1,111 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`block deletion - deleting the third and fourth blocks using backspace with multi-block selection results in two remaining blocks and positions the caret at the end of the second block 1`] = ` -" -

First paragraph

- - - -

Second paragraph

- - - -

-" -`; - -exports[`block deletion - deleting the third and fourth blocks using backspace with multi-block selection results in two remaining blocks and positions the caret at the end of the second block 2`] = ` -" -

First paragraph

- - - -

Second paragraph

- - - - -" -`; - -exports[`block deletion - deleting the third block using backspace in an empty block results in two remaining blocks and positions the caret at the end of the second block 1`] = ` -" -

First paragraph

- - - -

Second paragraph

-" -`; - -exports[`block deletion - deleting the third block using backspace in an empty block results in two remaining blocks and positions the caret at the end of the second block 2`] = ` -" -

First paragraph

- - - -

Second paragraph - caret was here

-" -`; - -exports[`block deletion - deleting the third block using backspace with block wrapper selection results in three remaining blocks and positions the caret at the end of the third block 1`] = ` -" -

First paragraph

- - - -

Second paragraph

-" -`; - -exports[`block deletion - deleting the third block using backspace with block wrapper selection results in three remaining blocks and positions the caret at the end of the third block 2`] = ` -" -

First paragraph

- - - -

Second paragraph - caret was here

-" -`; - -exports[`block deletion - deleting the third block using the Remove Block menu item results in two remaining blocks and positions the caret at the end of the second block 1`] = ` -" -

First paragraph

- - - -

Second paragraph

-" -`; - -exports[`block deletion - deleting the third block using the Remove Block menu item results in two remaining blocks and positions the caret at the end of the second block 2`] = ` -" -

First paragraph

- - - -

Second paragraph - caret was here

-" -`; - -exports[`block deletion - deleting the third block using the Remove Block shortcut results in two remaining blocks and positions the caret at the end of the second block 1`] = ` -" -

First paragraph

- - - -

Second paragraph

-" -`; - -exports[`block deletion - deleting the third block using the Remove Block shortcut results in two remaining blocks and positions the caret at the end of the second block 2`] = ` -" -

First paragraph

- - - -

Second paragraph - caret was here

-" -`; diff --git a/packages/e2e-tests/specs/editor/various/block-deletion.test.js b/packages/e2e-tests/specs/editor/various/block-deletion.test.js deleted file mode 100644 index e4497d45fbfaf8..00000000000000 --- a/packages/e2e-tests/specs/editor/various/block-deletion.test.js +++ /dev/null @@ -1,209 +0,0 @@ -/** - * WordPress dependencies - */ -import { - clickBlockAppender, - clickBlockToolbarButton, - getEditedPostContent, - createNewPost, - isInDefaultBlock, - pressKeyWithModifier, - pressKeyTimes, - insertBlock, -} from '@wordpress/e2e-test-utils'; - -const addThreeParagraphsToNewPost = async () => { - await createNewPost(); - - // Add demo content. - await clickBlockAppender(); - await page.keyboard.type( 'First paragraph' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( 'Second paragraph' ); - await page.keyboard.press( 'Enter' ); -}; - -/** - * Due to an issue with the Popover component not being scrollable - * under certain conditions, Pupeteer cannot "see" the "Remove Block" - * button. This is a workaround until that issue is resolved. - * - * see: https://github.com/WordPress/gutenberg/pull/14908#discussion_r284725956 - */ -const clickOnBlockSettingsMenuRemoveBlockButton = async () => { - await clickBlockToolbarButton( 'Options' ); - - let isRemoveButton = false; - - let numButtons = await page.$$eval( - '.block-editor-block-settings-menu__content button', - ( btns ) => btns.length - ); - - // Limit by the number of buttons available. - while ( --numButtons ) { - await page.keyboard.press( 'Tab' ); - - isRemoveButton = await page.evaluate( () => { - return document.activeElement.innerText.includes( - 'Remove Paragraph' - ); - } ); - - // Stop looping once we find the button. - if ( isRemoveButton ) { - await pressKeyTimes( 'Enter', 1 ); - break; - } - } - - // Makes failures more explicit. - await expect( isRemoveButton ).toBe( true ); -}; - -describe( 'block deletion -', () => { - beforeEach( addThreeParagraphsToNewPost ); - - describe( 'deleting the third block using the Remove Block menu item', () => { - it( 'results in two remaining blocks and positions the caret at the end of the second block', async () => { - // The blocks can't be empty to trigger the toolbar. - await page.keyboard.type( 'Paragraph to remove' ); - await clickOnBlockSettingsMenuRemoveBlockButton(); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // Type additional text and assert that caret position is correct by comparing to snapshot. - await page.keyboard.type( ' - caret was here' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - } ); - - describe( 'deleting the third block using the Remove Block shortcut', () => { - it( 'results in two remaining blocks and positions the caret at the end of the second block', async () => { - // Type some text to assert that the shortcut also deletes block content. - await page.keyboard.type( 'this is block 2' ); - await pressKeyWithModifier( 'access', 'z' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // Type additional text and assert that caret position is correct by comparing to snapshot. - await page.keyboard.type( ' - caret was here' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - } ); - - describe( 'deleting the third block using backspace in an empty block', () => { - it( 'results in two remaining blocks and positions the caret at the end of the second block', async () => { - await page.keyboard.press( 'Backspace' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // Type additional text and assert that caret position is correct by comparing to snapshot. - await page.keyboard.type( ' - caret was here' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - } ); - - describe( 'deleting the third block using backspace with block wrapper selection', () => { - it( 'results in three remaining blocks and positions the caret at the end of the third block', async () => { - // Add an image block since it's easier to click the wrapper on non-textual blocks. - await page.keyboard.type( '/image' ); - await page.waitForXPath( - `//*[contains(@class, "components-autocomplete__result") and contains(@class, "is-selected") and contains(text(), 'Image')]` - ); - await page.keyboard.press( 'Enter' ); - - // Click on something that's not a block. - await page.click( '.editor-post-title' ); - - // Click on the image block so that its wrapper is selected and backspace to delete it. - await page.click( - '.wp-block[data-type="core/image"] .components-placeholder__label' - ); - await page.keyboard.press( 'Backspace' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // Type additional text and assert that caret position is correct by comparing to snapshot. - await page.keyboard.type( ' - caret was here' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - } ); - - describe( 'deleting the third and fourth blocks using backspace with multi-block selection', () => { - it( 'results in two remaining blocks and positions the caret at the end of the second block', async () => { - // Add a third paragraph for this test. - await page.keyboard.type( 'Third paragraph' ); - await page.keyboard.press( 'Enter' ); - - // Press the up arrow once to select the third and fourth blocks. - await pressKeyWithModifier( 'shift', 'ArrowUp' ); - - // Now that the block wrapper is selected, press backspace to delete it. - await page.keyboard.press( 'Backspace' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // Type additional text and assert that caret position is correct by comparing to snapshot. - await page.keyboard.type( ' - caret was here' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - } ); -} ); - -describe( 'deleting all blocks', () => { - beforeEach( async () => { - await createNewPost(); - } ); - - it( 'results in the default block getting selected', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'Paragraph' ); - await clickOnBlockSettingsMenuRemoveBlockButton(); - - // There is a default block and post title: - expect( - await page.$$( '.block-editor-block-list__block' ) - ).toHaveLength( 2 ); - - // But the effective saved content is still empty: - expect( await getEditedPostContent() ).toBe( '' ); - - // And focus is retained: - expect( await isInDefaultBlock() ).toBe( true ); - } ); - - it( 'gracefully removes blocks when the default block is not available', async () => { - // Regression Test: Previously, removing a block would not clear - // selection state when there were no other blocks to select. - // - // See: https://github.com/WordPress/gutenberg/issues/15458 - // See: https://github.com/WordPress/gutenberg/pull/15543 - - // Unregister default block type. This may happen if the editor is - // configured to not allow the default (paragraph) block type, either - // by plugin editor settings filtering or user block preferences. - await page.evaluate( () => { - const defaultBlockName = wp.data - .select( 'core/blocks' ) - .getDefaultBlockName(); - wp.data - .dispatch( 'core/blocks' ) - .removeBlockTypes( defaultBlockName ); - } ); - - // Add and remove a block. - await insertBlock( 'Image' ); - await page.waitForSelector( 'figure[data-type="core/image"]' ); - await page.keyboard.press( 'ArrowUp' ); - await page.keyboard.press( 'Backspace' ); - - // Verify there is no selected block. - // TODO: There should be expectations around where focus is placed in - // this scenario. Currently, a focus loss occurs (not acceptable). - const selectedBlocksCount = await page.evaluate( () => { - return wp.data - .select( 'core/block-editor' ) - .getSelectedBlockClientIds().length; - } ); - - expect( selectedBlocksCount ).toBe( 0 ); - } ); -} ); diff --git a/test/e2e/specs/editor/various/block-deletion.spec.js b/test/e2e/specs/editor/various/block-deletion.spec.js new file mode 100644 index 00000000000000..efcf7b7d5a2825 --- /dev/null +++ b/test/e2e/specs/editor/various/block-deletion.spec.js @@ -0,0 +1,349 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Block deletion', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'deleting the last block via its options menu', async ( { + editor, + page, + } ) => { + // Add a couple of paragraphs. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'First' }, + } ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Second' }, + } ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Third' }, + } ); + + // Ensure the last paragraph is focused. + await expect( + editor.canvas + .getByRole( 'document', { + name: 'Paragraph block', + } ) + .last() + ).toBeFocused(); + + // Remove the current paragraph via the Block Toolbar options menu. + await editor.showBlockToolbar(); + await editor.canvas + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Options' } ) + .click(); + await page + .getByRole( 'menuitem', { name: 'Remove Paragraph' } ) + .click(); + + // Ensure the last block was removed. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { name: 'core/paragraph', attributes: { content: 'First' } }, + { name: 'core/paragraph', attributes: { content: 'Second' } }, + ] ); + + // Ensure the caret is in a correct position. + await page.keyboard.type( '| ← caret was here' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { name: 'core/paragraph', attributes: { content: 'First' } }, + { + name: 'core/paragraph', + attributes: { content: 'Second| ← caret was here' }, + }, + ] ); + } ); + + test( 'deleting the last block via the keyboard shortcut', async ( { + editor, + page, + pageUtils, + } ) => { + // Add a couple of paragraphs. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'First' }, + } ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Second' }, + } ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Third' }, + } ); + + // Ensure the last paragraph is focused. + await expect( + editor.canvas + .getByRole( 'document', { + name: 'Paragraph block', + } ) + .last() + ).toBeFocused(); + + // Remove the current paragraph via dedicated keyboard shortcut. + await pageUtils.pressKeyWithModifier( 'access', 'z' ); + + // Ensure the last block was removed. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { name: 'core/paragraph', attributes: { content: 'First' } }, + { name: 'core/paragraph', attributes: { content: 'Second' } }, + ] ); + + // Ensure the caret is in a correct position. + await page.keyboard.type( '| ← caret was here' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { name: 'core/paragraph', attributes: { content: 'First' } }, + { + name: 'core/paragraph', + attributes: { content: 'Second| ← caret was here' }, + }, + ] ); + } ); + + test( 'deleting the last block via backspace from an empty paragraph', async ( { + editor, + page, + } ) => { + // Add a couple of paragraphs. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'First' }, + } ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Second' }, + } ); + + // Leave last paragraph empty and ensure it's focused. + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await expect( + editor.canvas.getByRole( 'document', { name: 'Empty block' } ) + ).toBeFocused(); + + // Hit backspace to remove the empty paragraph. + await page.keyboard.press( 'Backspace' ); + + // Ensure the last block was removed. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { name: 'core/paragraph', attributes: { content: 'First' } }, + { name: 'core/paragraph', attributes: { content: 'Second' } }, + ] ); + + // Ensure the caret is in a correct position. + await page.keyboard.type( '| ← caret was here' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { name: 'core/paragraph', attributes: { content: 'First' } }, + { + name: 'core/paragraph', + attributes: { content: 'Second| ← caret was here' }, + }, + ] ); + } ); + + test( 'deleting last selected block via backspace', async ( { + editor, + page, + } ) => { + // Add a couple of paragraphs. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'First' }, + } ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Second' }, + } ); + + // Add the Image block and focus it. + await editor.insertBlock( { + name: 'core/image', + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await expect( + imageBlock.getByRole( 'button', { name: 'Upload' } ) + ).toBeFocused(); + await page.keyboard.press( 'ArrowUp' ); + await expect( imageBlock ).toBeFocused(); + + // Hit backspace and ensure the Image block was removed. + await page.keyboard.press( 'Backspace' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { name: 'core/paragraph', attributes: { content: 'First' } }, + { name: 'core/paragraph', attributes: { content: 'Second' } }, + ] ); + + // Ensure the caret is in a correct position. + await page.keyboard.type( '| ← caret was here' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { name: 'core/paragraph', attributes: { content: 'First' } }, + { + name: 'core/paragraph', + attributes: { content: 'Second| ← caret was here' }, + }, + ] ); + } ); + + test( 'deleting the last two selected blocks via backspace', async ( { + editor, + page, + pageUtils, + } ) => { + // Add a couple of paragraphs. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'First' }, + } ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Second' }, + } ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Third' }, + } ); + await editor.insertBlock( { + name: 'core/paragraph', + } ); + + // Ensure the empty paragraph is focused. + await expect( + editor.canvas.getByRole( 'document', { + name: 'Empty block', + } ) + ).toBeFocused(); + + // Select the last two paragraphs. + await pageUtils.pressKeyWithModifier( 'shift', 'ArrowUp' ); + await expect + .poll( () => + page.evaluate( () => + window.wp.data + .select( 'core/block-editor' ) + .getMultiSelectedBlocks() + ) + ) + .toHaveLength( 2 ); + + // Hit backspace and ensure the last two paragraphs were deleted, and an + // empty block was created. + await page.keyboard.press( 'Backspace' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { name: 'core/paragraph', attributes: { content: 'First' } }, + { name: 'core/paragraph', attributes: { content: 'Second' } }, + { name: 'core/paragraph', attributes: { content: '' } }, + ] ); + + // Ensure that the newly created empty block is focused. + await expect.poll( editor.getBlocks ).toHaveLength( 3 ); + await expect( + editor.canvas.getByRole( 'document', { + name: 'Empty block', + } ) + ).toBeFocused(); + } ); + + test( 'deleting all blocks', async ( { editor, page } ) => { + // Add one paragraph with content and ensure it's focused. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Test' }, + } ); + await expect( + editor.canvas.getByRole( 'document', { + name: 'Paragraph block', + } ) + ).toBeFocused(); + + // Remove that paragraph via its options menu. + await editor.showBlockToolbar(); + await editor.canvas + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Options' } ) + .click(); + await page + .getByRole( 'menuitem', { name: 'Remove Paragraph' } ) + .click(); + + // Ensure an empty block was created and focused. + await expect( + editor.canvas.getByRole( 'document', { + name: 'Empty block', + } ) + ).toBeFocused(); + await expect.poll( editor.getBlocks ).toEqual( [] ); + } ); + + /** + * Regression Test: Previously, removing a block would not clear + * selection state when there were no other blocks to select. + * + * See: https://github.com/WordPress/gutenberg/issues/15458 + * See: https://github.com/WordPress/gutenberg/pull/15543 + */ + test( 'deleting all blocks when the default block is unavailable', async ( { + editor, + page, + } ) => { + // Unregister default block type. This may happen if the editor is + // configured to not allow the default (paragraph) block type, either + // by plugin editor settings filtering or user block preferences. + await page.waitForFunction( () => { + try { + const defaultBlockName = window.wp.data + .select( 'core/blocks' ) + .getDefaultBlockName(); + window.wp.data + .dispatch( 'core/blocks' ) + .removeBlockTypes( defaultBlockName ); + return true; + } catch { + return false; + } + } ); + + // Add the Image block. + await editor.insertBlock( { name: 'core/image' } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + + // Initially, the "Upload" button is focused, so we need to move focus + // up to the block wrapper. + await expect( + imageBlock.getByRole( 'button', { name: 'Upload' } ) + ).toBeFocused(); + await page.keyboard.press( 'ArrowUp' ); + await expect( imageBlock ).toBeFocused(); + + // Hit backspace to remove the Image block. + await page.keyboard.press( 'Backspace' ); + + // Ensure that there's no blocks. + await expect.poll( editor.getBlocks ).toHaveLength( 0 ); + await expect( + editor.canvas.getByRole( 'document', { name: 'Empty block' } ) + ).not.toBeVisible(); + + // Ensure that the block appender button is visible. + await expect( + editor.canvas.getByRole( 'button', { name: 'Add block' } ) + ).toBeVisible(); + + // TODO: There should be expectations around where focus is placed in + // this scenario. Currently, a focus loss occurs, which is unacceptable. + } ); +} );