diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index 694a48f0266367..5fa1a82d5ad25b 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -36,10 +36,6 @@ jobs: run: | npm run wp-env start - - name: Running the tests - run: | - npx wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" - - name: Archive debug artifacts (screenshots, HTML snapshots) uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: always() diff --git a/packages/e2e-tests/specs/editor/various/autosave.test.js b/packages/e2e-tests/specs/editor/various/autosave.test.js deleted file mode 100644 index 528efc2d463167..00000000000000 --- a/packages/e2e-tests/specs/editor/various/autosave.test.js +++ /dev/null @@ -1,346 +0,0 @@ -/** - * WordPress dependencies - */ -import { - clickBlockAppender, - createNewPost, - getEditedPostContent, - pressKeyWithModifier, - publishPost, - saveDraft, - toggleOfflineMode, - canvas, -} from '@wordpress/e2e-test-utils'; - -// Constant to override editor preference -const AUTOSAVE_INTERVAL_SECONDS = 5; - -const AUTOSAVE_NOTICE_REMOTE = - 'There is an autosave of this post that is more recent than the version below.'; -const AUTOSAVE_NOTICE_LOCAL = - 'The backup of this post in your browser is different from the version below.'; - -// Save and wait for "Saved" to confirm save complete. Preserves focus in the -// editing area. -async function saveDraftWithKeyboard() { - await page.waitForSelector( '.editor-post-save-draft' ); - await Promise.all( [ - page.waitForSelector( '.editor-post-saved-state.is-saved' ), - pressKeyWithModifier( 'primary', 'S' ), - ] ); -} - -async function sleep( durationInSeconds ) { - // Rule `no-restricted-syntax` recommends `waitForSelector` against - // `waitFor`, which isn't apt for the use case, when provided an integer, - // of waiting for a given amount of time. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( durationInSeconds * 1000 ); -} - -async function clearSessionStorage() { - await page.evaluate( () => window.sessionStorage.clear() ); -} - -async function readSessionStorageAutosave( postId ) { - return page.evaluate( - ( key ) => window.sessionStorage.getItem( key ), - `wp-autosave-block-editor-post-${ postId ? postId : 'auto-draft' }` - ); -} - -async function getCurrentPostId() { - return page.evaluate( () => - window.wp.data.select( 'core/editor' ).getCurrentPostId() - ); -} - -async function setLocalAutosaveInterval( value ) { - return page.evaluate( ( _value ) => { - window.wp.data.dispatch( 'core/editor' ).updateEditorSettings( { - localAutosaveInterval: _value, - } ); - }, value ); -} - -function wrapParagraph( text ) { - return ` -
${ text }
-`; -} - -describe( 'autosave', () => { - beforeEach( async () => { - await clearSessionStorage(); - await createNewPost(); - await setLocalAutosaveInterval( AUTOSAVE_INTERVAL_SECONDS ); - } ); - - it( 'should save to sessionStorage', async () => { - // Wait for the original timeout to kick in, it will schedule - // another run using the updated interval length of AUTOSAVE_INTERVAL_SECONDS. - await sleep( 15 ); - - await clickBlockAppender(); - await page.keyboard.type( 'before save' ); - await saveDraftWithKeyboard(); - await sleep( 1 ); - await page.keyboard.type( ' after save' ); - - // Wait long enough for local autosave to kick in. - await sleep( AUTOSAVE_INTERVAL_SECONDS + 1 ); - - const id = await getCurrentPostId(); - const autosave = await readSessionStorageAutosave( id ); - const { content } = JSON.parse( autosave ); - expect( content ).toBe( wrapParagraph( 'before save after save' ) ); - } ); - - it( 'should recover from sessionStorage', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'before save' ); - await saveDraftWithKeyboard(); - await page.keyboard.type( ' after save' ); - - // Trigger local autosave. - await page.evaluate( () => - window.wp.data.dispatch( 'core/editor' ).autosave( { local: true } ) - ); - // Reload without saving on the server. - await page.reload(); - await page.waitForSelector( '.edit-post-layout' ); - - const notice = await page.$eval( - '.components-notice__content', - ( element ) => element.innerText - ); - expect( notice ).toContain( AUTOSAVE_NOTICE_LOCAL ); - - expect( await getEditedPostContent() ).toEqual( - wrapParagraph( 'before save' ) - ); - await page.click( '.components-notice__action' ); - expect( await getEditedPostContent() ).toEqual( - wrapParagraph( 'before save after save' ) - ); - } ); - - it( "shouldn't contaminate other posts", async () => { - await clickBlockAppender(); - await page.keyboard.type( 'before save' ); - await saveDraft(); - - // Fake local autosave. - await page.evaluate( - ( postId ) => - window.sessionStorage.setItem( - `wp-autosave-block-editor-post-${ postId }`, - JSON.stringify( { - post_title: 'A', - content: 'B', - excerpt: 'C', - } ) - ), - await getCurrentPostId() - ); - expect( - await page.evaluate( () => window.sessionStorage.length ) - ).toBe( 1 ); - - await page.reload(); - await page.waitForSelector( '.edit-post-layout' ); - const notice = await page.$eval( - '.components-notice__content', - ( element ) => element.innerText - ); - expect( notice ).toContain( - 'The backup of this post in your browser is different from the version below.' - ); - - await createNewPost(); - expect( await page.$( '.components-notice__content' ) ).toBe( null ); - } ); - - it( 'should clear local autosave after successful remote autosave', async () => { - // Edit, save draft, edit again. - await clickBlockAppender(); - await page.keyboard.type( 'before save' ); - await saveDraftWithKeyboard(); - await page.keyboard.type( ' after save' ); - - // Trigger local autosave. - await page.evaluate( () => - window.wp.data.dispatch( 'core/editor' ).autosave( { local: true } ) - ); - expect( - await page.evaluate( () => window.sessionStorage.length ) - ).toBeGreaterThanOrEqual( 1 ); - - // Trigger remote autosave. - await page.evaluate( () => - window.wp.data.dispatch( 'core/editor' ).autosave() - ); - expect( - await page.evaluate( () => window.sessionStorage.length ) - ).toBe( 0 ); - } ); - - it( "shouldn't clear local autosave if remote autosave fails", async () => { - // Edit, save draft, edit again. - await clickBlockAppender(); - await page.keyboard.type( 'before save' ); - await saveDraftWithKeyboard(); - await page.keyboard.type( ' after save' ); - - // Trigger local autosave. - await page.evaluate( () => - window.wp.data.dispatch( 'core/editor' ).autosave( { local: true } ) - ); - expect( - await page.evaluate( () => window.sessionStorage.length ) - ).toBe( 1 ); - - // Bring network down and attempt to autosave remotely. - toggleOfflineMode( true ); - await page.evaluate( () => - window.wp.data.dispatch( 'core/editor' ).autosave() - ); - expect( - await page.evaluate( () => window.sessionStorage.length ) - ).toBe( 1 ); - } ); - - it( 'should clear local autosave after successful save', async () => { - // Edit, save draft, edit again. - await clickBlockAppender(); - await page.keyboard.type( 'before save' ); - await saveDraftWithKeyboard(); - await page.keyboard.type( ' after save' ); - - // Trigger local autosave. - await page.evaluate( () => - window.wp.data.dispatch( 'core/editor' ).autosave( { local: true } ) - ); - expect( - await page.evaluate( () => window.sessionStorage.length ) - ).toBe( 1 ); - - await saveDraftWithKeyboard(); - expect( - await page.evaluate( () => window.sessionStorage.length ) - ).toBe( 0 ); - } ); - - it( "shouldn't clear local autosave if save fails", async () => { - // Edit, save draft, edit again. - await clickBlockAppender(); - await page.keyboard.type( 'before save' ); - await saveDraftWithKeyboard(); - await page.keyboard.type( ' after save' ); - - // Trigger local autosave. - await page.evaluate( () => - window.wp.data.dispatch( 'core/editor' ).autosave( { local: true } ) - ); - expect( - await page.evaluate( () => window.sessionStorage.length ) - ).toBeGreaterThanOrEqual( 1 ); - - // Bring network down and attempt to save. - toggleOfflineMode( true ); - saveDraftWithKeyboard(); - expect( - await page.evaluate( () => window.sessionStorage.length ) - ).toBeGreaterThanOrEqual( 1 ); - } ); - - it( "shouldn't conflict with server-side autosave", async () => { - await clickBlockAppender(); - await page.keyboard.type( 'before publish' ); - await publishPost(); - - await canvas().click( '[data-type="core/paragraph"]' ); - await page.keyboard.type( ' after publish' ); - - // Trigger remote autosave. - await page.evaluate( () => - window.wp.data.dispatch( 'core/editor' ).autosave() - ); - - // Force conflicting local autosave. - await page.evaluate( () => - window.wp.data.dispatch( 'core/editor' ).autosave( { local: true } ) - ); - expect( - await page.evaluate( () => window.sessionStorage.length ) - ).toBeGreaterThanOrEqual( 1 ); - - await page.reload(); - await page.waitForSelector( '.edit-post-layout' ); - - // FIXME: Occasionally, upon reload, there is no server-provided - // autosave value available, despite our having previously explicitly - // autosaved. The reasons for this are still unknown. Since this is - // unrelated to *local* autosave, until we can understand them, we'll - // drop this test's expectations if we don't have an autosave object - // available. - const stillHasRemoteAutosave = await page.evaluate( - () => - window.wp.data.select( 'core/editor' ).getEditorSettings() - .autosave - ); - if ( ! stillHasRemoteAutosave ) { - return; - } - - // Only one autosave notice should be displayed. - const notices = await page.$$( '.components-notice' ); - expect( notices.length ).toBe( 1 ); - const notice = await page.$eval( - '.components-notice__content', - ( element ) => element.innerText - ); - expect( notice ).toContain( AUTOSAVE_NOTICE_REMOTE ); - } ); - - it( 'should clear sessionStorage upon user logout', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'before save' ); - await saveDraft(); - - // Fake local autosave. - await page.evaluate( - ( postId ) => - window.sessionStorage.setItem( - `wp-autosave-block-editor-post-${ postId }`, - JSON.stringify( { - post_title: 'A', - content: 'B', - excerpt: 'C', - } ) - ), - await getCurrentPostId() - ); - expect( - await page.evaluate( () => window.sessionStorage.length ) - ).toBe( 1 ); - - await Promise.all( [ - page.waitForSelector( '#wp-admin-bar-logout', { visible: true } ), - page.hover( '#wp-admin-bar-my-account' ), - ] ); - await Promise.all( [ - page.waitForNavigation(), - page.click( '#wp-admin-bar-logout' ), - ] ); - - expect( - await page.evaluate( () => window.sessionStorage.length ) - ).toBe( 0 ); - } ); - - afterEach( async () => { - toggleOfflineMode( false ); - await clearSessionStorage(); - } ); -} ); diff --git a/test/e2e/specs/editor/various/autosave.spec.js b/test/e2e/specs/editor/various/autosave.spec.js new file mode 100644 index 00000000000000..01d53f0bf2d085 --- /dev/null +++ b/test/e2e/specs/editor/various/autosave.spec.js @@ -0,0 +1,378 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Autosave', () => { + test.beforeEach( async ( { admin, page } ) => { + await admin.createNewPost(); + await page.evaluate( () => window.sessionStorage.clear() ); + } ); + + test.afterEach( async ( { page } ) => { + await page.evaluate( () => window.sessionStorage.clear() ); + } ); + + test( 'should save to sessionStorage', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.canvas + .getByRole( 'button', { name: 'Add default block' } ) + .click(); + await page.keyboard.type( 'before save' ); + await pageUtils.pressKeys( 'primary+s' ); + await page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'Draft saved' } ) + .waitFor(); + await page.keyboard.type( ' after save' ); + + await page.evaluate( () => + window.wp.data.dispatch( 'core/editor' ).autosave( { local: true } ) + ); + + const autosave = await page.evaluate( () => { + const postId = window.wp.data + .select( 'core/editor' ) + .getCurrentPostId(); + + return window.sessionStorage.getItem( + `wp-autosave-block-editor-post-${ + postId ? postId : 'auto-draft' + }` + ); + } ); + + const { content } = JSON.parse( autosave ); + expect( content ).toBe( ` +before save after save
+` ); + } ); + + test( 'should recover from sessionStorage', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.canvas + .getByRole( 'button', { name: 'Add default block' } ) + .click(); + await page.keyboard.type( 'before save' ); + await pageUtils.pressKeys( 'primary+s' ); + await page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'Draft saved' } ) + .waitFor(); + await page.keyboard.type( ' after save' ); + + // Trigger local autosave. + await page.evaluate( () => + window.wp.data.dispatch( 'core/editor' ).autosave( { local: true } ) + ); + // Reload without saving on the server. + await page.reload(); + + await expect( + page.locator( '.components-notice__content' ) + ).toContainText( + 'The backup of this post in your browser is different from the version below.' + ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'before save' }, + }, + ] ); + + await page + .getByRole( 'button', { name: 'Restore the backup' } ) + .click(); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'before save after save' }, + }, + ] ); + } ); + + test( "shouldn't contaminate other posts", async ( { + admin, + editor, + page, + pageUtils, + } ) => { + await editor.canvas + .getByRole( 'button', { name: 'Add default block' } ) + .click(); + await page.keyboard.type( 'before save' ); + await pageUtils.pressKeys( 'primary+s' ); + await page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'Draft saved' } ) + .waitFor(); + await page.keyboard.type( ' after save' ); + + await page.evaluate( () => + window.wp.data.dispatch( 'core/editor' ).autosave( { local: true } ) + ); + expect( + await page.evaluate( () => window.sessionStorage.length ) + ).toBe( 1 ); + + await page.reload(); + await expect( + page.locator( '.components-notice__content' ) + ).toContainText( + 'The backup of this post in your browser is different from the version below.' + ); + + await admin.createNewPost(); + await expect( + page.locator( '.components-notice__content' ) + ).toBeHidden(); + } ); + + test( 'should clear local autosave after successful remote autosave', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.canvas + .getByRole( 'button', { name: 'Add default block' } ) + .click(); + await page.keyboard.type( 'before save' ); + await pageUtils.pressKeys( 'primary+s' ); + await page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'Draft saved' } ) + .waitFor(); + await page.keyboard.type( ' after save' ); + + // Trigger local autosave. + await page.evaluate( () => + window.wp.data.dispatch( 'core/editor' ).autosave( { local: true } ) + ); + + expect( + await page.evaluate( () => window.sessionStorage.length ) + ).toBeGreaterThanOrEqual( 1 ); + + // Trigger remote autosave. + await page.evaluate( () => + window.wp.data.dispatch( 'core/editor' ).autosave() + ); + expect( + await page.evaluate( () => window.sessionStorage.length ) + ).toBe( 0 ); + } ); + + test( "shouldn't clear local autosave if remote autosave fails", async ( { + editor, + context, + page, + pageUtils, + } ) => { + await editor.canvas + .getByRole( 'button', { name: 'Add default block' } ) + .click(); + await page.keyboard.type( 'before save' ); + await pageUtils.pressKeys( 'primary+s' ); + await page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'Draft saved' } ) + .waitFor(); + await page.keyboard.type( ' after save' ); + + // Trigger local autosave. + await page.evaluate( () => + window.wp.data.dispatch( 'core/editor' ).autosave( { local: true } ) + ); + + expect( + await page.evaluate( () => window.sessionStorage.length ) + ).toBeGreaterThanOrEqual( 1 ); + + // Intercept autosave request and abort it. + await context.setOffline( true ); + await page.evaluate( () => + window.wp.data.dispatch( 'core/editor' ).autosave() + ); + + expect( + await page.evaluate( () => window.sessionStorage.length ) + ).toBe( 1 ); + } ); + + test( 'should clear local autosave after successful save', async ( { + page, + pageUtils, + } ) => { + const notice = page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'Draft saved' } ); + + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'before save' ); + await pageUtils.pressKeys( 'primary+s' ); + await notice.waitFor(); + await page.keyboard.type( ' after save' ); + await notice.click(); + + // Trigger local autosave. + await page.evaluate( () => + window.wp.data.dispatch( 'core/editor' ).autosave( { local: true } ) + ); + + expect( + await page.evaluate( () => window.sessionStorage.length ) + ).toBeGreaterThanOrEqual( 1 ); + + await pageUtils.pressKeys( 'primary+s' ); + await notice.waitFor(); + + expect( + await page.evaluate( () => window.sessionStorage.length ) + ).toBe( 0 ); + } ); + + test( "shouldn't clear local autosave if save fails", async ( { + editor, + context, + page, + pageUtils, + } ) => { + const notice = page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'Draft saved' } ); + + await editor.canvas + .getByRole( 'button', { name: 'Add default block' } ) + .click(); + await page.keyboard.type( 'before save' ); + await pageUtils.pressKeys( 'primary+s' ); + await notice.waitFor(); + await page.keyboard.type( ' after save' ); + await notice.click(); + + // Trigger local autosave. + await page.evaluate( () => + window.wp.data.dispatch( 'core/editor' ).autosave( { local: true } ) + ); + + expect( + await page.evaluate( () => window.sessionStorage.length ) + ).toBeGreaterThanOrEqual( 1 ); + + await context.setOffline( true ); + await pageUtils.pressKeys( 'primary+s' ); + + await expect( + page.locator( '.components-notice__content' ) + ).toContainText( 'Updating failed. You are probably offline.' ); + expect( + await page.evaluate( () => window.sessionStorage.length ) + ).toBe( 1 ); + } ); + + // See https://github.com/WordPress/gutenberg/pull/17501. + test( "shouldn't conflict with server-side autosave", async ( { + editor, + page, + } ) => { + await editor.canvas + .getByRole( 'button', { name: 'Add default block' } ) + .click(); + await page.keyboard.type( 'before save' ); + await editor.publishPost(); + + const paragraph = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await paragraph.click(); + await page.keyboard.type( ' after save' ); + + // Trigger remote autosave. + await page.evaluate( () => + window.wp.data.dispatch( 'core/editor' ).autosave() + ); + + await expect + .poll( async () => { + return await page.evaluate( () => { + const postId = window.wp.data + .select( 'core/editor' ) + .getCurrentPostId(); + const autosaves = window.wp.data + .select( 'core' ) + .getAutosaves( 'post', postId ); + + return autosaves?.length ?? 0; + } ); + } ) + .toBeGreaterThanOrEqual( 1 ); + + // Force conflicting local autosave. + await page.evaluate( () => + window.wp.data.dispatch( 'core/editor' ).autosave( { local: true } ) + ); + + expect( + await page.evaluate( () => window.sessionStorage.length ) + ).toBeGreaterThanOrEqual( 1 ); + + await page.reload(); + await page.waitForFunction( () => window?.wp?.data ); + + // FIXME: Occasionally, upon reload, there is no server-provided + // autosave value available, despite our having previously explicitly + // autosaved. The reasons for this are still unknown. Since this is + // unrelated to *local* autosave, until we can understand them, we'll + // drop this test's expectations if we don't have an autosave object + // available. + const stillHasRemoteAutosave = await page.evaluate( + () => + window.wp.data.select( 'core/editor' ).getEditorSettings() + .autosave + ); + if ( ! stillHasRemoteAutosave ) { + return; + } + + // Only remote autosave notice should be applied. + await expect( + page.locator( '.components-notice__content' ) + ).toContainText( + 'There is an autosave of this post that is more recent than the version below.' + ); + } ); + + test.skip( 'should clear sessionStorage upon user logout', async ( { + page, + pageUtils, + } ) => { + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'before save' ); + await pageUtils.pressKeys( 'primary+s' ); + await page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'Draft saved' } ) + .waitFor(); + await page.keyboard.type( ' after save' ); + + await page.evaluate( () => + window.wp.data.dispatch( 'core/editor' ).autosave( { local: true } ) + ); + expect( + await page.evaluate( () => window.sessionStorage.length ) + ).toBe( 1 ); + + await page.locator( '#wp-admin-bar-my-account' ).hover(); + await page.locator( '#wp-admin-bar-logout' ).click(); + + expect( + await page.evaluate( () => window.sessionStorage.length ) + ).toBe( 0 ); + } ); +} );