diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/undo.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/undo.test.js.snap deleted file mode 100644 index 5d1601b9f0d9a..0000000000000 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/undo.test.js.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`undo should immediately create an undo level on typing 1`] = ` -" -

1

-" -`; - -exports[`undo should undo typing after a pause 1`] = ` -" -

before pause after pause

-" -`; - -exports[`undo should undo typing after a pause 2`] = ` -" -

before pause

-" -`; - -exports[`undo should undo typing after non input change 1`] = ` -" -

before keyboard after keyboard

-" -`; - -exports[`undo should undo typing after non input change 2`] = ` -" -

before keyboard

-" -`; diff --git a/packages/e2e-tests/specs/editor/various/undo.test.js b/packages/e2e-tests/specs/editor/various/undo.test.js deleted file mode 100644 index 9402ce8624c1a..0000000000000 --- a/packages/e2e-tests/specs/editor/various/undo.test.js +++ /dev/null @@ -1,444 +0,0 @@ -/** - * WordPress dependencies - */ -import { - clickBlockAppender, - getEditedPostContent, - createNewPost, - pressKeyWithModifier, - selectBlockByClientId, - getAllBlocks, - saveDraft, - publishPost, -} from '@wordpress/e2e-test-utils'; - -const getSelection = async () => { - return await page.evaluate( () => { - const selectedBlock = document.activeElement.closest( '.wp-block' ); - const blocks = Array.from( document.querySelectorAll( '.wp-block' ) ); - const blockIndex = blocks.indexOf( selectedBlock ); - - if ( blockIndex === -1 ) { - return {}; - } - - let editables; - - if ( selectedBlock.getAttribute( 'contenteditable' ) ) { - editables = [ selectedBlock ]; - } else { - editables = Array.from( - selectedBlock.querySelectorAll( '[contenteditable]' ) - ); - } - - const editableIndex = editables.indexOf( document.activeElement ); - const selection = window.getSelection(); - - if ( editableIndex === -1 || ! selection.rangeCount ) { - return { blockIndex }; - } - - const range = selection.getRangeAt( 0 ); - const cloneStart = range.cloneRange(); - const cloneEnd = range.cloneRange(); - - cloneStart.setStart( document.activeElement, 0 ); - cloneEnd.setStart( document.activeElement, 0 ); - - /** - * Zero width non-breaking space, used as padding in the editable DOM - * tree when it is empty otherwise. - */ - const ZWNBSP = '\ufeff'; - - return { - blockIndex, - editableIndex, - startOffset: cloneStart.toString().replace( ZWNBSP, '' ).length, - endOffset: cloneEnd.toString().replace( ZWNBSP, '' ).length, - }; - } ); -}; - -describe( 'undo', () => { - beforeEach( async () => { - await createNewPost(); - } ); - - it( 'should undo typing after a pause', async () => { - await clickBlockAppender(); - - await page.keyboard.type( 'before pause' ); - await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); - await page.keyboard.type( ' after pause' ); - - const after = await getEditedPostContent(); - - expect( after ).toMatchSnapshot(); - - await pressKeyWithModifier( 'primary', 'z' ); - - const before = await getEditedPostContent(); - - expect( before ).toMatchSnapshot(); - expect( await getSelection() ).toEqual( { - blockIndex: 1, - editableIndex: 0, - startOffset: 'before pause'.length, - endOffset: 'before pause'.length, - } ); - - await pressKeyWithModifier( 'primary', 'z' ); - - expect( await getEditedPostContent() ).toBe( '' ); - expect( await getSelection() ).toEqual( { - blockIndex: 1, - editableIndex: 0, - startOffset: 0, - endOffset: 0, - } ); - - await pressKeyWithModifier( 'primaryShift', 'z' ); - - expect( await getEditedPostContent() ).toBe( before ); - expect( await getSelection() ).toEqual( { - blockIndex: 1, - editableIndex: 0, - startOffset: 'before pause'.length, - endOffset: 'before pause'.length, - } ); - - await pressKeyWithModifier( 'primaryShift', 'z' ); - - expect( await getEditedPostContent() ).toBe( after ); - expect( await getSelection() ).toEqual( { - blockIndex: 1, - editableIndex: 0, - startOffset: 'before pause after pause'.length, - endOffset: 'before pause after pause'.length, - } ); - } ); - - it( 'should undo typing after non input change', async () => { - await clickBlockAppender(); - - await page.keyboard.type( 'before keyboard ' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( 'after keyboard' ); - - const after = await getEditedPostContent(); - - expect( after ).toMatchSnapshot(); - - await pressKeyWithModifier( 'primary', 'z' ); - - const before = await getEditedPostContent(); - - expect( before ).toMatchSnapshot(); - expect( await getSelection() ).toEqual( { - blockIndex: 1, - editableIndex: 0, - startOffset: 'before keyboard '.length, - endOffset: 'before keyboard '.length, - } ); - - await pressKeyWithModifier( 'primary', 'z' ); - - expect( await getEditedPostContent() ).toBe( '' ); - expect( await getSelection() ).toEqual( { - blockIndex: 1, - editableIndex: 0, - startOffset: 0, - endOffset: 0, - } ); - - await pressKeyWithModifier( 'primaryShift', 'z' ); - - expect( await getEditedPostContent() ).toBe( before ); - expect( await getSelection() ).toEqual( { - blockIndex: 1, - editableIndex: 0, - startOffset: 'before keyboard '.length, - endOffset: 'before keyboard '.length, - } ); - - await pressKeyWithModifier( 'primaryShift', 'z' ); - - expect( await getEditedPostContent() ).toBe( after ); - expect( await getSelection() ).toEqual( { - blockIndex: 1, - editableIndex: 0, - startOffset: 'before keyboard after keyboard'.length, - endOffset: 'before keyboard after keyboard'.length, - } ); - } ); - - it( 'should undo bold', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'test' ); - await saveDraft(); - await page.reload(); - await page.waitForSelector( '.edit-post-layout' ); - await page.click( '[data-type="core/paragraph"]' ); - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'b' ); - await pressKeyWithModifier( 'primary', 'z' ); - - const visibleResult = await page.evaluate( - () => document.activeElement.innerHTML - ); - expect( visibleResult ).toBe( 'test' ); - } ); - - it( 'Should undo/redo to expected level intervals', async () => { - await clickBlockAppender(); - - const firstBlock = await getEditedPostContent(); - - await page.keyboard.type( 'This' ); - - const firstText = await getEditedPostContent(); - - await page.keyboard.press( 'Enter' ); - - const secondBlock = await getEditedPostContent(); - - await page.keyboard.type( 'is' ); - - const secondText = await getEditedPostContent(); - - await page.keyboard.press( 'Enter' ); - - const thirdBlock = await getEditedPostContent(); - - await page.keyboard.type( 'test' ); - - const thirdText = await getEditedPostContent(); - - await pressKeyWithModifier( 'primary', 'z' ); // Undo 3rd paragraph text. - - expect( await getEditedPostContent() ).toBe( thirdBlock ); - expect( await getSelection() ).toEqual( { - blockIndex: 3, - editableIndex: 0, - startOffset: 0, - endOffset: 0, - } ); - - await pressKeyWithModifier( 'primary', 'z' ); // Undo 3rd block. - - expect( await getEditedPostContent() ).toBe( secondText ); - expect( await getSelection() ).toEqual( { - blockIndex: 2, - editableIndex: 0, - startOffset: 'is'.length, - endOffset: 'is'.length, - } ); - - await pressKeyWithModifier( 'primary', 'z' ); // Undo 2nd paragraph text. - - expect( await getEditedPostContent() ).toBe( secondBlock ); - expect( await getSelection() ).toEqual( { - blockIndex: 2, - editableIndex: 0, - startOffset: 0, - endOffset: 0, - } ); - - await pressKeyWithModifier( 'primary', 'z' ); // Undo 2nd block. - - expect( await getEditedPostContent() ).toBe( firstText ); - expect( await getSelection() ).toEqual( { - blockIndex: 1, - editableIndex: 0, - startOffset: 'This'.length, - endOffset: 'This'.length, - } ); - - await pressKeyWithModifier( 'primary', 'z' ); // Undo 1st paragraph text. - - expect( await getEditedPostContent() ).toBe( firstBlock ); - expect( await getSelection() ).toEqual( { - blockIndex: 1, - editableIndex: 0, - startOffset: 0, - endOffset: 0, - } ); - - await pressKeyWithModifier( 'primary', 'z' ); // Undo 1st block. - - expect( await getEditedPostContent() ).toBe( '' ); - expect( await getSelection() ).toEqual( {} ); - // After undoing every action, there should be no more undo history. - expect( - await page.$( '.editor-history__undo[aria-disabled="true"]' ) - ).not.toBeNull(); - - await pressKeyWithModifier( 'primaryShift', 'z' ); // Redo 1st block. - - expect( await getEditedPostContent() ).toBe( firstBlock ); - expect( await getSelection() ).toEqual( { - blockIndex: 1, - editableIndex: 0, - startOffset: 0, - endOffset: 0, - } ); - // After redoing one change, the undo button should be enabled again. - expect( - await page.$( '.editor-history__undo[aria-disabled="true"]' ) - ).toBeNull(); - - await pressKeyWithModifier( 'primaryShift', 'z' ); // Redo 1st paragraph text. - - expect( await getEditedPostContent() ).toBe( firstText ); - expect( await getSelection() ).toEqual( { - blockIndex: 1, - editableIndex: 0, - startOffset: 'This'.length, - endOffset: 'This'.length, - } ); - - await pressKeyWithModifier( 'primaryShift', 'z' ); // Redo 2nd block. - - expect( await getEditedPostContent() ).toBe( secondBlock ); - expect( await getSelection() ).toEqual( { - blockIndex: 2, - editableIndex: 0, - startOffset: 0, - endOffset: 0, - } ); - - await pressKeyWithModifier( 'primaryShift', 'z' ); // Redo 2nd paragraph text. - - expect( await getEditedPostContent() ).toBe( secondText ); - expect( await getSelection() ).toEqual( { - blockIndex: 2, - editableIndex: 0, - startOffset: 'is'.length, - endOffset: 'is'.length, - } ); - - await pressKeyWithModifier( 'primaryShift', 'z' ); // Redo 3rd block. - - expect( await getEditedPostContent() ).toBe( thirdBlock ); - expect( await getSelection() ).toEqual( { - blockIndex: 3, - editableIndex: 0, - startOffset: 0, - endOffset: 0, - } ); - - await pressKeyWithModifier( 'primaryShift', 'z' ); // Redo 3rd paragraph text. - - expect( await getEditedPostContent() ).toBe( thirdText ); - expect( await getSelection() ).toEqual( { - blockIndex: 3, - editableIndex: 0, - startOffset: 'test'.length, - endOffset: 'test'.length, - } ); - } ); - - it( 'should undo for explicit persistence editing post', async () => { - // Regression test: An issue had occurred where the creation of an - // explicit undo level would interfere with blocks values being synced - // correctly to the block editor. - // - // See: https://github.com/WordPress/gutenberg/issues/14950 - - // Issue is demonstrated from an edited post: create, save, and reload. - await clickBlockAppender(); - await page.keyboard.type( 'original' ); - await saveDraft(); - await page.reload(); - await page.waitForSelector( '.edit-post-layout' ); - - // Issue is demonstrated by forcing state merges (multiple inputs) on - // an existing text after a fresh reload. - await selectBlockByClientId( ( await getAllBlocks() )[ 0 ].clientId ); - await page.keyboard.type( 'modified' ); - - // The issue is demonstrated after the one second delay to trigger the - // creation of an explicit undo persistence level. - await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); - - await pressKeyWithModifier( 'primary', 'z' ); - - // Assert against the _visible_ content. Since editor state with the - // regression present was accurate, it would produce the correct - // content. The issue had manifested in the form of what was shown to - // the user since the blocks state failed to sync to block editor. - const visibleContent = await page.evaluate( - () => document.activeElement.textContent - ); - expect( visibleContent ).toBe( 'original' ); - } ); - - it( 'should not create undo levels when saving', async () => { - await clickBlockAppender(); - await page.keyboard.type( '1' ); - await saveDraft(); - await pressKeyWithModifier( 'primary', 'z' ); - - expect( await getEditedPostContent() ).toBe( '' ); - } ); - - it( 'should not create undo levels when publishing', async () => { - await clickBlockAppender(); - await page.keyboard.type( '1' ); - await publishPost(); - await pressKeyWithModifier( 'primary', 'z' ); - - expect( await getEditedPostContent() ).toBe( '' ); - } ); - - it( 'should immediately create an undo level on typing', async () => { - await clickBlockAppender(); - - await page.keyboard.type( '1' ); - await saveDraft(); - await page.reload(); - await page.waitForSelector( '.edit-post-layout' ); - - // Expect undo button to be disabled. - expect( - await page.$( '.editor-history__undo[aria-disabled="true"]' ) - ).not.toBeNull(); - - await page.click( '[data-type="core/paragraph"]' ); - - await page.keyboard.type( '2' ); - - // Expect undo button to be enabled. - expect( - await page.$( '.editor-history__undo[aria-disabled="true"]' ) - ).toBeNull(); - - await pressKeyWithModifier( 'primary', 'z' ); - - // Expect "1". - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should be able to undo and redo when transient changes have been made and we update/publish', async () => { - // Typing consecutive characters in a `Paragraph` block updates the same - // block attribute as in the previous action and results in transient edits - // and skipping `undo` history steps. - const text = 'tonis'; - await clickBlockAppender(); - await page.keyboard.type( text ); - await publishPost(); - await pressKeyWithModifier( 'primary', 'z' ); - expect( await getEditedPostContent() ).toBe( '' ); - await page.waitForSelector( - '.editor-history__redo[aria-disabled="false"]' - ); - await page.click( '.editor-history__redo[aria-disabled="false"]' ); - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

tonis

- " - ` ); - } ); -} ); diff --git a/test/e2e/specs/editor/various/undo.spec.js b/test/e2e/specs/editor/various/undo.spec.js new file mode 100644 index 0000000000000..29b34ea416ff2 --- /dev/null +++ b/test/e2e/specs/editor/various/undo.spec.js @@ -0,0 +1,491 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.use( { + undoUtils: async ( { page }, use ) => { + await use( new UndoUtils( { page } ) ); + }, +} ); + +test.describe( 'undo', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'should undo typing after a pause', async ( { + editor, + page, + pageUtils, + undoUtils, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'before pause' ); + await editor.page.waitForTimeout( 1000 ); + await page.keyboard.type( ' after pause' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'before pause after pause' }, + }, + ] ); + + await pageUtils.pressKeys( 'primary+z' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'before pause' }, + }, + ] ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 0, + startOffset: 'before pause'.length, + endOffset: 'before pause'.length, + } ); + + await pageUtils.pressKeys( 'primary+z' ); + + await expect.poll( editor.getEditedPostContent ).toBe( '' ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 0, + } ); + + await pageUtils.pressKeys( 'primaryShift+z' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'before pause' }, + }, + ] ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 0, + startOffset: 'before pause'.length, + endOffset: 'before pause'.length, + } ); + + await pageUtils.pressKeys( 'primaryShift+z' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'before pause after pause' }, + }, + ] ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 0, + startOffset: 'before pause after pause'.length, + endOffset: 'before pause after pause'.length, + } ); + } ); + + test( 'should undo typing after non input change', async ( { + editor, + page, + pageUtils, + undoUtils, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + + await page.keyboard.type( 'before keyboard ' ); + await pageUtils.pressKeys( 'primary+b' ); + await page.keyboard.type( 'after keyboard' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: 'before keyboard after keyboard', + }, + }, + ] ); + + await pageUtils.pressKeys( 'primary+z' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: 'before keyboard ', + }, + }, + ] ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 0, + startOffset: 'before keyboard '.length, + endOffset: 'before keyboard '.length, + } ); + + await pageUtils.pressKeys( 'primary+z' ); + + await expect.poll( editor.getEditedPostContent ).toBe( '' ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 0, + } ); + + await pageUtils.pressKeys( 'primaryShift+z' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: 'before keyboard ', + }, + }, + ] ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 0, + startOffset: 'before keyboard '.length, + endOffset: 'before keyboard '.length, + } ); + + await pageUtils.pressKeys( 'primaryShift+z' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: 'before keyboard after keyboard', + }, + }, + ] ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 0, + startOffset: 'before keyboard after keyboard'.length, + endOffset: 'before keyboard after keyboard'.length, + } ); + } ); + + test( 'should undo bold', async ( { page, pageUtils } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'test' ); + await page.click( 'role=button[name="Save draft"i]' ); + await expect( + page.locator( + 'role=button[name="Dismiss this notice"i] >> text=Draft saved' + ) + ).toBeVisible(); + await page.reload(); + await page.click( '[data-type="core/paragraph"]' ); + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+b' ); + await pageUtils.pressKeys( 'primary+z' ); + const activeElementLocator = page.locator( ':focus' ); + await expect( activeElementLocator ).toHaveText( 'test' ); + } ); + + test( 'Should undo/redo to expected level intervals', async ( { + editor, + page, + pageUtils, + undoUtils, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + + const firstBlock = await editor.getEditedPostContent(); + + await page.keyboard.type( 'This' ); + + const firstText = await editor.getEditedPostContent(); + + await page.keyboard.press( 'Enter' ); + + const secondBlock = await editor.getEditedPostContent(); + + await page.keyboard.type( 'is' ); + + const secondText = await editor.getEditedPostContent(); + + await page.keyboard.press( 'Enter' ); + + const thirdBlock = await editor.getEditedPostContent(); + + await page.keyboard.type( 'test' ); + + const thirdText = await editor.getEditedPostContent(); + + await pageUtils.pressKeys( 'primary+z' ); // Undo 3rd paragraph text. + + await expect.poll( editor.getEditedPostContent ).toBe( thirdBlock ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 2, + } ); + + await pageUtils.pressKeys( 'primary+z' ); // Undo 3rd block. + + await expect.poll( editor.getEditedPostContent ).toBe( secondText ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 1, + startOffset: 'is'.length, + endOffset: 'is'.length, + } ); + + await pageUtils.pressKeys( 'primary+z' ); // Undo 2nd paragraph text. + + await expect.poll( editor.getEditedPostContent ).toBe( secondBlock ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 1, + } ); + + await pageUtils.pressKeys( 'primary+z' ); // Undo 2nd block. + + await expect.poll( editor.getEditedPostContent ).toBe( firstText ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 0, + startOffset: 'This'.length, + endOffset: 'This'.length, + } ); + + await pageUtils.pressKeys( 'primary+z' ); // Undo 1st paragraph text. + + await expect.poll( editor.getEditedPostContent ).toBe( firstBlock ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 0, + } ); + + await pageUtils.pressKeys( 'primary+z' ); // Undo 1st block. + + await expect.poll( editor.getEditedPostContent ).toBe( '' ); + await expect.poll( undoUtils.getSelection ).toEqual( {} ); + // After undoing every action, there should be no more undo history. + await expect( + page.locator( 'role=button[name="Undo"]' ) + ).toBeDisabled(); + + await pageUtils.pressKeys( 'primaryShift+z' ); // Redo 1st block. + + await expect.poll( editor.getEditedPostContent ).toBe( firstBlock ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 0, + startOffset: 0, + endOffset: 0, + } ); + // After redoing one change, the undo button should be enabled again. + await expect( + page.locator( 'role=button[name="Undo"]' ) + ).toBeEnabled(); + + await pageUtils.pressKeys( 'primaryShift+z' ); // Redo 1st paragraph text. + + await expect.poll( editor.getEditedPostContent ).toBe( firstText ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 0, + startOffset: 'This'.length, + endOffset: 'This'.length, + } ); + + await pageUtils.pressKeys( 'primaryShift+z' ); // Redo 2nd block. + + await expect.poll( editor.getEditedPostContent ).toBe( secondBlock ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 1, + startOffset: 0, + endOffset: 0, + } ); + + await pageUtils.pressKeys( 'primaryShift+z' ); // Redo 2nd paragraph text. + + await expect.poll( editor.getEditedPostContent ).toBe( secondText ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 1, + startOffset: 'is'.length, + endOffset: 'is'.length, + } ); + + await pageUtils.pressKeys( 'primaryShift+z' ); // Redo 3rd block. + + await expect.poll( editor.getEditedPostContent ).toBe( thirdBlock ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 2, + startOffset: 0, + endOffset: 0, + } ); + + await pageUtils.pressKeys( 'primaryShift+z' ); // Redo 3rd paragraph text. + + await expect.poll( editor.getEditedPostContent ).toBe( thirdText ); + await expect.poll( undoUtils.getSelection ).toEqual( { + blockIndex: 2, + startOffset: 'test'.length, + endOffset: 'test'.length, + } ); + } ); + + test( 'should undo for explicit persistence editing post', async ( { + page, + pageUtils, + editor, + } ) => { + // Regression test: An issue had occurred where the creation of an + // explicit undo level would interfere with blocks values being synced + // correctly to the block editor. + // + // See: https://github.com/WordPress/gutenberg/issues/14950 + + // Issue is demonstrated from an edited post: create, save, and reload. + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'original' ); + await page.click( 'role=button[name="Save draft"i]' ); + await expect( + page.locator( + 'role=button[name="Dismiss this notice"i] >> text=Draft saved' + ) + ).toBeVisible(); + await page.reload(); + + // Issue is demonstrated by forcing state merges (multiple inputs) on + // an existing text after a fresh reload. + await page.click( '[data-type="core/paragraph"] >> nth=0' ); + await page.keyboard.type( 'modified' ); + + // The issue is demonstrated after the one second delay to trigger the + // creation of an explicit undo persistence level. + await editor.page.waitForTimeout( 1000 ); + + await pageUtils.pressKeys( 'primary+z' ); + + // Assert against the _visible_ content. Since editor state with the + // regression present was accurate, it would produce the correct + // content. The issue had manifested in the form of what was shown to + // the user since the blocks state failed to sync to block editor. + const activeElementLocator = page.locator( ':focus' ); + await expect( activeElementLocator ).toHaveText( 'original' ); + } ); + + test( 'should not create undo levels when saving', async ( { + editor, + page, + pageUtils, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '1' ); + await page.click( 'role=button[name="Save draft"i]' ); + await expect( + page.locator( + 'role=button[name="Dismiss this notice"i] >> text=Draft saved' + ) + ).toBeVisible(); + await pageUtils.pressKeys( 'primary+z' ); + + await expect.poll( editor.getEditedPostContent ).toBe( '' ); + } ); + + test( 'should not create undo levels when publishing', async ( { + editor, + page, + pageUtils, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '1' ); + await editor.publishPost(); + await pageUtils.pressKeys( 'primary+z' ); + + await expect.poll( editor.getEditedPostContent ).toBe( '' ); + } ); + + test( 'should immediately create an undo level on typing', async ( { + editor, + page, + pageUtils, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + + await page.keyboard.type( '1' ); + await page.click( 'role=button[name="Save draft"i]' ); + await expect( + page.locator( + 'role=button[name="Dismiss this notice"i] >> text=Draft saved' + ) + ).toBeVisible(); + await page.reload(); + + // Expect undo button to be disabled. + await expect( + page.locator( 'role=button[name="Undo"]' ) + ).toBeDisabled(); + await page.click( '[data-type="core/paragraph"]' ); + + await page.keyboard.type( '2' ); + + // Expect undo button to be enabled. + await expect( + page.locator( 'role=button[name="Undo"]' ) + ).toBeEnabled(); + + await pageUtils.pressKeys( 'primary+z' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: '1', + }, + }, + ] ); + } ); + + test( 'should be able to undo and redo when transient changes have been made and we update/publish', async ( { + editor, + page, + pageUtils, + } ) => { + // Typing consecutive characters in a `Paragraph` block updates the same + // block attribute as in the previous action and results in transient edits + // and skipping `undo` history steps. + const text = 'tonis'; + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( text ); + await editor.publishPost(); + await pageUtils.pressKeys( 'primary+z' ); + await expect.poll( editor.getEditedPostContent ).toBe( '' ); + await expect( + page.locator( 'role=button[name="Redo"]' ) + ).not.toBeDisabled(); + await page.click( 'role=button[name="Redo"]' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: 'tonis', + }, + }, + ] ); + } ); +} ); + +class UndoUtils { + constructor( { page } ) { + this.page = page; + + this.getSelection = this.getSelection.bind( this ); + } + + async getSelection() { + return await this.page.evaluate( () => { + const selectedBlockId = window.wp.data + .select( 'core/block-editor' ) + .getSelectedBlockClientId(); + const blockIndex = window.wp.data + .select( 'core/block-editor' ) + .getBlockIndex( selectedBlockId ); + + if ( blockIndex === -1 ) { + return {}; + } + + return { + blockIndex, + startOffset: window.wp.data + .select( 'core/block-editor' ) + .getSelectionStart().offset, + endOffset: window.wp.data + .select( 'core/block-editor' ) + .getSelectionEnd().offset, + }; + } ); + } +}