diff --git a/editor/components/rich-text/index.js b/editor/components/rich-text/index.js index 3fcec36dcaba5..073e00e4fa24f 100644 --- a/editor/components/rich-text/index.js +++ b/editor/components/rich-text/index.js @@ -695,21 +695,22 @@ export class RichText extends Component { } } - updateContent() { - // Do not trigger a change event coming from the TinyMCE undo manager. - // Our global state is already up-to-date. - this.editor.undoManager.ignore( () => { - const bookmark = this.editor.selection.getBookmark( 2, true ); - - this.savedContent = this.props.value; - this.setContent( this.savedContent ); - this.editor.selection.moveToBookmark( bookmark ); - } ); - } - setContent( content ) { const { format } = this.props; + + // If editor has focus while content is being set, save the selection + // and restore caret position after content is set. + let bookmark; + if ( this.editor.hasFocus() ) { + bookmark = this.editor.selection.getBookmark( 2, true ); + } + + this.savedContent = content; this.editor.setContent( valueToString( content, format ) ); + + if ( bookmark ) { + this.editor.selection.moveToBookmark( bookmark ); + } } getContent() { @@ -739,7 +740,7 @@ export class RichText extends Component { ! isEqual( this.props.value, prevProps.value ) && ! isEqual( this.props.value, this.savedContent ) ) { - this.updateContent(); + this.setContent( this.props.value ); } if ( 'development' === process.env.NODE_ENV ) { @@ -829,7 +830,7 @@ export class RichText extends Component { * @param {?Array} blocks blocks to insert at the split position */ restoreContentAndSplit( before, after, blocks = [] ) { - this.updateContent(); + this.setContent( this.props.value ); this.props.onSplit( before, after, ...blocks ); } diff --git a/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap b/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap index 98d163a480fbd..6ca129d7e4123 100644 --- a/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap +++ b/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap @@ -12,6 +12,16 @@ exports[`splitting and merging blocks Should split and merge paragraph blocks us exports[`splitting and merging blocks Should split and merge paragraph blocks using Enter and Backspace 2`] = ` " -

FirstSecond

+

FirstBetweenSecond

+" +`; + +exports[`splitting and merging blocks Should split and merge paragraph blocks using Enter and Backspace 3`] = ` +" +

First

+ + + +

BeforeSecond:Second

" `; diff --git a/test/e2e/specs/splitting-merging.test.js b/test/e2e/specs/splitting-merging.test.js index 5c727cbf406d9..1d914fb65f9b5 100644 --- a/test/e2e/specs/splitting-merging.test.js +++ b/test/e2e/specs/splitting-merging.test.js @@ -2,7 +2,14 @@ * Internal dependencies */ import '../support/bootstrap'; -import { newPost, newDesktopBrowserPage, insertBlock } from '../support/utils'; +import { + newPost, + newDesktopBrowserPage, + insertBlock, + getHTMLFromCodeEditor, + pressTimes, + pressWithModifier, +} from '../support/utils'; describe( 'splitting and merging blocks', () => { beforeAll( async () => { @@ -11,42 +18,45 @@ describe( 'splitting and merging blocks', () => { } ); it( 'Should split and merge paragraph blocks using Enter and Backspace', async () => { - //Use regular inserter to add paragraph block and text + // Use regular inserter to add paragraph block and text await insertBlock( 'Paragraph' ); await page.keyboard.type( 'FirstSecond' ); - //Move caret between 'First' and 'Second' and press Enter to split paragraph blocks - for ( let i = 0; i < 6; i++ ) { - await page.keyboard.press( 'ArrowLeft' ); - } + // Move caret between 'First' and 'Second' and press Enter to split + // paragraph blocks + await pressTimes( 'ArrowLeft', 6 ); await page.keyboard.press( 'Enter' ); - //Switch to Code Editor to check HTML output - await page.click( '.edit-post-more-menu [aria-label="More"]' ); - let codeEditorButton = ( await page.$x( '//button[contains(text(), \'Code Editor\')]' ) )[ 0 ]; - await codeEditorButton.click( 'button' ); + // Assert that there are now two paragraph blocks with correct content + expect( await getHTMLFromCodeEditor() ).toMatchSnapshot(); - //Assert that there are now two paragraph blocks with correct content - let textEditorContent = await page.$eval( '.editor-post-text-editor', ( element ) => element.value ); - expect( textEditorContent ).toMatchSnapshot(); - - //Switch to Visual Editor to continue testing - await page.click( '.edit-post-more-menu [aria-label="More"]' ); - const visualEditorButton = ( await page.$x( '//button[contains(text(), \'Visual Editor\')]' ) )[ 0 ]; - await visualEditorButton.click( 'button' ); - - //Press Backspace to merge paragraph blocks + // Press Backspace to merge paragraph blocks await page.click( '.is-selected' ); await page.keyboard.press( 'Home' ); await page.keyboard.press( 'Backspace' ); - //Switch to Code Editor to check HTML output - await page.click( '.edit-post-more-menu [aria-label="More"]' ); - codeEditorButton = ( await page.$x( '//button[contains(text(), \'Code Editor\')]' ) )[ 0 ]; - await codeEditorButton.click( 'button' ); + // Ensure that caret position is correctly placed at the between point. + await page.keyboard.type( 'Between' ); + expect( await getHTMLFromCodeEditor() ).toMatchSnapshot(); + // Workaround: When transitioning back from Code to Visual, the caret + // is placed at the beginning of the selected paragraph. Ideally this + // should persist selection between modes. + await pressTimes( 'ArrowRight', 5 ); // After "First" + await pressTimes( 'Delete', 7 ); // Delete "Between" + + // Edge case: Without ensuring that the editor still has focus when + // restoring a bookmark, the caret may be inadvertently moved back to + // an inline boundary after a split occurs. + await page.keyboard.press( 'Home' ); + await page.keyboard.down( 'Shift' ); + await pressTimes( 'ArrowRight', 5 ); + await page.keyboard.up( 'Shift' ); + await pressWithModifier( 'mod', 'b' ); + // Collapse selection, still within inline boundary. + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'BeforeSecond:' ); - //Assert that there is now one paragraph with correct content - textEditorContent = await page.$eval( '.editor-post-text-editor', ( element ) => element.value ); - expect( textEditorContent ).toMatchSnapshot(); + expect( await getHTMLFromCodeEditor() ).toMatchSnapshot(); } ); } ); diff --git a/test/e2e/support/utils.js b/test/e2e/support/utils.js index a9fb2d9984efd..eb347ef3d67e5 100644 --- a/test/e2e/support/utils.js +++ b/test/e2e/support/utils.js @@ -4,6 +4,11 @@ import { join } from 'path'; import { URL } from 'url'; +/** + * External dependencies + */ +import { times } from 'lodash'; + const { WP_BASE_URL = 'http://localhost:8888', WP_USERNAME = 'admin', @@ -26,6 +31,21 @@ const MOD_KEY = process.platform === 'darwin' ? 'Meta' : 'Control'; */ const REGEXP_ZWSP = /[\u200B\u200C\u200D\uFEFF]/; +/** + * Given an array of functions, each returning a promise, performs all + * promises in sequence (waterfall) order. + * + * @param {Function[]} sequence Array of promise creators. + * + * @return {Promise} Promise resolving once all in the sequence complete. + */ +async function promiseSequence( sequence ) { + return sequence.reduce( + ( current, next ) => current.then( next ), + Promise.resolve() + ); +} + export function getUrl( WPPath, query = '' ) { const url = new URL( WP_BASE_URL ); @@ -232,6 +252,18 @@ export async function openDocumentSettingsSidebar() { } } +/** + * Presses the given keyboard key a number of times in sequence. + * + * @param {string} key Key to press. + * @param {number} count Number of times to press. + * + * @return {Promise} Promise resolving when key presses complete. + */ +export async function pressTimes( key, count ) { + return promiseSequence( times( count, () => () => page.keyboard.press( key ) ) ); +} + export async function clearLocalStorage() { await page.evaluate( () => window.localStorage.clear() ); }