From 9d18c70e6f9f13d3875f94c41263739a8ff5c48a Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 29 Dec 2023 16:13:06 +0100 Subject: [PATCH 01/20] Add support to pass the initialTitle to the E2E tests --- .../mobile/WPAndroidGlue/GutenbergProps.kt | 2 +- .../__device-tests__/pages/editor-page.js | 15 ++++++++++++--- .../src/main/java/com/gutenberg/MainActivity.java | 7 +++++++ .../GutenbergDemo/GutenbergViewController.swift | 5 ++++- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt index ce427be2ad09b0..ec847d71bf51c9 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt @@ -92,7 +92,6 @@ data class GutenbergProps @JvmOverloads constructor( content?.let { putString(PROP_INITIAL_DATA, it) } } - private const val PROP_INITIAL_TITLE = "initialTitle" private const val PROP_INITIAL_HTML_MODE_ENABLED = "initialHtmlModeEnabled" private const val PROP_POST_TYPE = "postType" private const val PROP_HOST_APP_NAMESPACE = "hostAppNamespace" @@ -105,6 +104,7 @@ data class GutenbergProps @JvmOverloads constructor( private const val PROP_QUOTE_BLOCK_V2 = "quoteBlockV2" private const val PROP_LIST_BLOCK_V2 = "listBlockV2" + const val PROP_INITIAL_TITLE = "initialTitle" const val PROP_INITIAL_DATA = "initialData" const val PROP_STYLES = "rawStyles" const val PROP_FEATURES = "rawFeatures" diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js index a19aaf5445d79f..cef5b8316a6922 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -44,9 +44,18 @@ class EditorPage { this.accessibilityIdKey = 'contentDescription'; } } - - async initializeEditor( { initialData, rawStyles, rawFeatures } = {} ) { - await launchApp( this.driver, { initialData, rawStyles, rawFeatures } ); + async initializeEditor( { + initialTitle, + initialData, + rawStyles, + rawFeatures, + } = {} ) { + await launchApp( this.driver, { + initialTitle, + initialData, + rawStyles, + rawFeatures, + } ); // Stores initial values from the editor for different helpers. const addButton = await this.driver.$$( `~${ ADD_BLOCK_ID }` ); diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java index 69985317ddad33..15dbf717893a47 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java @@ -166,6 +166,7 @@ private Bundle getAppOptions() { Bundle bundle = new Bundle(); // Parse initial props from launch arguments + String initialTitle = null; String initialData = null; String rawStyles = null; String rawFeatures = null; @@ -175,6 +176,9 @@ private Bundle getAppOptions() { String initialProps = extrasBundle.getString(EXTRAS_INITIAL_PROPS, "{}"); try { JSONObject jsonObject = new JSONObject(initialProps); + if (jsonObject.has(GutenbergProps.PROP_INITIAL_TITLE)) { + initialTitle = jsonObject.getString(GutenbergProps.PROP_INITIAL_TITLE); + } if (jsonObject.has(GutenbergProps.PROP_INITIAL_DATA)) { initialData = jsonObject.getString(GutenbergProps.PROP_INITIAL_DATA); } @@ -209,6 +213,9 @@ private Bundle getAppOptions() { capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_SMARTFRAME_EMBED_BLOCK, true); bundle.putBundle(GutenbergProps.PROP_CAPABILITIES, capabilities); + if(initialTitle != null) { + bundle.putString(GutenbergProps.PROP_INITIAL_TITLE, initialTitle); + } if(initialData != null) { bundle.putString(GutenbergProps.PROP_INITIAL_DATA, initialData); } diff --git a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift index ef95c7e65862f6..0c04308125df71 100644 --- a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift +++ b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift @@ -399,7 +399,10 @@ extension GutenbergViewController: GutenbergBridgeDataSource { } func gutenbergInitialTitle() -> String? { - return nil + guard isUITesting(), let initialProps = getInitialPropsFromArgs() else { + return nil + } + return initialProps["initialTitle"] } func gutenbergHostAppNamespace() -> String { From 55d500ed668c7d57128875447c6876e49d5f2152 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 29 Dec 2023 16:13:48 +0100 Subject: [PATCH 02/20] Editor Page - Add new E2E utils --- .../__device-tests__/pages/editor-page.js | 57 ++++++++++++++++--- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js index cef5b8316a6922..7ef744eb8c87a6 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -44,6 +44,7 @@ class EditorPage { this.accessibilityIdKey = 'contentDescription'; } } + async initializeEditor( { initialTitle, initialData, @@ -185,9 +186,18 @@ class EditorPage { await emptyAreaBelowLastBlock.click(); } - async getTitleElement( options = { autoscroll: false } ) { + async getDefaultBlockAppenderElement() { + const appenderElement = isAndroid() + ? `//android.widget.EditText[@text='Start writing…']` + : '(//XCUIElementTypeOther[contains(@name, "Start writing…")])[2]'; + return this.driver.$( appenderElement ); + } + + async getTitleElement( options = { autoscroll: false, isEmpty: false } ) { const titleElement = isAndroid() - ? 'Post title. Welcome to Gutenberg!' + ? `Post title. ${ + options.isEmpty ? 'Empty' : 'Welcome to Gutenberg!' + }` : 'post-title'; if ( options.autoscroll ) { @@ -209,6 +219,13 @@ class EditorPage { return elements[ 0 ]; } + async getEmptyTitleElement() { + const titleElement = isAndroid() + ? '//android.widget.EditText[@content-desc="Post title. Empty"]' + : '~Add title'; + return this.driver.$( titleElement ); + } + // iOS loads the block list more eagerly compared to Android. // This makes this function return elements without scrolling on iOS. // So we are keeping this Android only. @@ -379,10 +396,14 @@ class EditorPage { await settingsButton.click(); } - async removeBlock() { - const blockActionsButtonElement = isAndroid() + getBlockActionsMenuElement() { + return isAndroid() ? '//android.widget.Button[contains(@content-desc, "Open Block Actions Menu")]' : '//XCUIElementTypeButton[@name="Open Block Actions Menu"]'; + } + + async removeBlock() { + const blockActionsButtonElement = this.getBlockActionsMenuElement(); const blockActionsMenu = await this.swipeToolbarToElement( blockActionsButtonElement ); @@ -400,6 +421,12 @@ class EditorPage { return await swipeDown( this.driver ); } + async isBlockActionsMenuButtonDisplayed() { + const menuButtonElement = this.getBlockActionsMenuElement(); + const elementsFound = await this.driver.$$( menuButtonElement ); + return elementsFound.length !== 0; + } + // ========================= // Block toolbar functions // ========================= @@ -796,13 +823,25 @@ class EditorPage { await clickIfClickable( this.driver, mediaLibraryLocator ); } + async getImageBlockCaptionButton() { + const captionElement = isAndroid() + ? '//android.widget.Button[starts-with(@content-desc, "Image caption")]' + : '//XCUIElementTypeButton[starts-with(@name, "Image caption.")]'; + return this.driver.$( captionElement ); + } + + async getImageBlockCaptionInput( imageBlockCaptionButton ) { + const captionInputElement = isAndroid() + ? '//android.widget.EditText' + : '//XCUIElementTypeTextView'; + return imageBlockCaptionButton.$( captionInputElement ); + } + async enterCaptionToSelectedImageBlock( caption, clear = true ) { - const imageBlockCaptionButton = await this.driver.$( - '//XCUIElementTypeButton[starts-with(@name, "Image caption.")]' - ); + const imageBlockCaptionButton = await this.getImageBlockCaptionButton(); await imageBlockCaptionButton.click(); - const imageBlockCaptionField = await imageBlockCaptionButton.$( - '//XCUIElementTypeTextView' + const imageBlockCaptionField = await this.getImageBlockCaptionInput( + imageBlockCaptionButton ); await typeString( this.driver, imageBlockCaptionField, caption, clear ); } From b0550a152616cf8dada21ca543ef7ba9e1c959f5 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 29 Dec 2023 16:14:07 +0100 Subject: [PATCH 03/20] E2E - Paragraph block - Remove deprecated multiline prop test --- .../gutenberg-editor-paragraph.test.js | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js index 8f21ef04858fb6..e62761b1bdf720 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js @@ -116,28 +116,4 @@ describe( 'Gutenberg Editor tests for Paragraph Block', () => { await editorPage.getTextForParagraphBlockAtPosition( 1 ); expect( text0 + text1 ).toMatch( mergedBlockText ); } ); - - // Based on https://github.com/wordpress-mobile/gutenberg-mobile/pull/1507 - it( 'should handle multiline paragraphs from web', async () => { - await editorPage.initializeEditor( { - initialData: [ - testData.multiLinesParagraphBlock, - testData.paragraphBlockEmpty, - ].join( '\n\n' ), - } ); - - // Merge paragraphs. - const paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph, - 2 - ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - backspace - ); - - // Verify the editor has not crashed. - const text = await editorPage.getTextForParagraphBlockAtPosition( 1 ); - expect( text.length ).not.toEqual( 0 ); - } ); } ); From e9de7460c3e7fd1811a102d73a488415c8e8a5c4 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 29 Dec 2023 16:22:24 +0100 Subject: [PATCH 04/20] Add initial E2E Writing flow tests --- ...enberg-editor-writing-flow-@canary.test.js | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js new file mode 100644 index 00000000000000..26f4ff34ab3cff --- /dev/null +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js @@ -0,0 +1,195 @@ +/** + * Internal dependencies + */ +import { blockNames } from './pages/editor-page'; +import { backspace, isAndroid, waitForMediaLibrary } from './helpers/utils'; +import testData from './helpers/test-data'; + +async function isImageBlockSelected() { + // Since there isn't an easy way to see if a block is selected, + // it will check if the edit image button is visible + const editImageElement = isAndroid() + ? '(//android.widget.Button[@content-desc="Edit image"])' + : '(//XCUIElementTypeButton[@name="Edit image"])'; + + return await editorPage.driver.$( editImageElement ).isDisplayed(); +} + +describe( 'Gutenberg Editor Writing flow tests', () => { + it( 'should be able to write a post title', async () => { + await editorPage.initializeEditor( { initialTitle: '' } ); + + const titleElement = await editorPage.getTitleElement( { + isEmpty: true, + } ); + await titleElement.click(); + + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + + const titleInput = await editorPage.getEmptyTitleElement(); + await editorPage.typeTextToTextBlock( titleInput, testData.shortText ); + + // Trigger the return key to go to the first Paragraph + await editorPage.typeTextToTextBlock( titleInput, '\n' ); + + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph + ); + expect( paragraphBlockElement ).toBeTruthy(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + + // Trigger the return key to delete the Paragraph block + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + backspace + ); + // Expect to have an empty Paragraph block and the keyboard visible + expect( + await editorPage.getTextBlockAtPosition( blockNames.paragraph ) + ).toBeTruthy(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + } ); + + it( 'should be able to create a new Paragraph block when pressing the enter key', async () => { + await editorPage.initializeEditor( { initialTitle: '' } ); + + const defaultBlockAppenderElement = + await editorPage.getDefaultBlockAppenderElement(); + await defaultBlockAppenderElement.click(); + + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph + ); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + testData.shortText + ); + } ); + + it( 'should automatically dismiss the keyboard when selecting non-text-based-blocks', async () => { + await editorPage.initializeEditor( { initialTitle: '' } ); + + await editorPage.addNewBlock( blockNames.image ); + // Wait for the Media picker to show up + await waitForMediaLibrary( editorPage.driver ); + + // Select the WordPress Media Library option + await editorPage.chooseMediaLibrary(); + + // Wait until the media is added + await editorPage.driver.pause( 500 ); + + const captionElement = await editorPage.getImageBlockCaptionButton(); + await captionElement.click(); + const captionInput = + await editorPage.getImageBlockCaptionInput( captionElement ); + + expect( captionInput ).toBeTruthy(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + + // Sets a new caption + await editorPage.typeTextToTextBlock( + captionInput, + testData.listItem2, + true + ); + + // Trigger the return key to exit the caption and create a new Paragraph block + await editorPage.typeTextToTextBlock( captionInput, '\n' ); + + // Expect to have an empty Paragraph block and the keyboard visible + let paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph, + 2 + ); + expect( paragraphBlockElement ).toBeTruthy(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + backspace + ); + + // When deleting the Paragraph block, the keyboard should be hidden and + // the image block should be focused. + expect( + await editorPage.getBlockAtPosition( blockNames.image ) + ).toBeTruthy(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( false ); + expect( await isImageBlockSelected() ).toBe( true ); + + // Adding a new Paragraph block + await editorPage.addNewBlock( blockNames.paragraph ); + paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph, + 2 + ); + + // It should be focused and the keyboard should be visible + expect( paragraphBlockElement ).toBeTruthy(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + testData.shortText + ); + + const imageBlockElement = await editorPage.getBlockAtPosition( + blockNames.image + ); + await imageBlockElement.click(); + await editorPage.driver.pause( 1000 ); + + expect( await editorPage.driver.isKeyboardShown() ).toBe( false ); + expect( await isImageBlockSelected() ).toBe( true ); + } ); + + it( 'should manually dismiss the keyboard', async () => { + await editorPage.initializeEditor( { initialTitle: '' } ); + + const defaultBlockAppenderElement = + await editorPage.getDefaultBlockAppenderElement(); + await defaultBlockAppenderElement.click(); + + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph + ); + expect( paragraphBlockElement ).toBeTruthy(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + + await editorPage.dismissKeyboard(); + + // Checks that no block is select by looking for the block menu actions button + expect( await editorPage.isBlockActionsMenuButtonDisplayed() ).toBe( + false + ); + expect( await editorPage.driver.isKeyboardShown() ).toBe( false ); + } ); + + it( 'should dimiss the keyboard and show it back when opening modals', async () => { + await editorPage.initializeEditor( { initialTitle: '' } ); + + const defaultBlockAppenderElement = + await editorPage.getDefaultBlockAppenderElement(); + await defaultBlockAppenderElement.click(); + + await editorPage.openBlockSettings(); + await editorPage.driver.pause( 1000 ); + expect( await editorPage.driver.isKeyboardShown() ).toBe( false ); + + await editorPage.dismissBottomSheet(); + await editorPage.driver.pause( 1000 ); + + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph + ); + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + testData.listItem1 + ); + const typedText = await paragraphBlockElement.getText(); + expect( typedText ).toMatch( testData.listItem1 ); + } ); +} ); From 39471d9d083f9ffb6949e2398b9e0b0c0cb34641 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 29 Dec 2023 16:41:27 +0100 Subject: [PATCH 05/20] E2E Tests - Merge Paragraph block tests with the Writing flow tests --- .../gutenberg-editor-paragraph.test.js | 119 ----------------- ...enberg-editor-writing-flow-@canary.test.js | 126 +++++++++++++++++- 2 files changed, 125 insertions(+), 120 deletions(-) delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js deleted file mode 100644 index e62761b1bdf720..00000000000000 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Internal dependencies - */ -import { blockNames } from './pages/editor-page'; -import { - backspace, - clickMiddleOfElement, - clickBeginningOfElement, -} from './helpers/utils'; -import testData from './helpers/test-data'; - -describe( 'Gutenberg Editor tests for Paragraph Block', () => { - it( 'should be able to split one paragraph block into two', async () => { - await editorPage.initializeEditor(); - await editorPage.addNewBlock( blockNames.paragraph ); - const paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph - ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - testData.shortText - ); - await clickMiddleOfElement( editorPage.driver, paragraphBlockElement ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - '\n', - false - ); - const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); - const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 ); - expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 2 ); - expect( text0 ).not.toBe( '' ); - expect( text1 ).not.toBe( '' ); - expect( testData.shortText ).toMatch( - new RegExp( `${ text0 + text1 }|${ text0 } ${ text1 }` ) - ); - } ); - - it( 'should be able to merge 2 paragraph blocks into 1', async () => { - await editorPage.initializeEditor(); - await editorPage.addNewBlock( blockNames.paragraph ); - let paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph - ); - - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - testData.shortText - ); - await clickMiddleOfElement( editorPage.driver, paragraphBlockElement ); - await editorPage.typeTextToTextBlock( paragraphBlockElement, '\n' ); - - const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); - const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 ); - expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 2 ); - paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph, - 2 - ); - - await clickBeginningOfElement( - editorPage.driver, - paragraphBlockElement - ); - - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - backspace - ); - - const text = await editorPage.getTextForParagraphBlockAtPosition( 1 ); - expect( text0 + text1 ).toMatch( text ); - paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph, - 1 - ); - await paragraphBlockElement.click(); - expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 1 ); - } ); - - it( 'should be able to create a post with multiple paragraph blocks', async () => { - await editorPage.initializeEditor(); - await editorPage.addNewBlock( blockNames.paragraph ); - await editorPage.sendTextToParagraphBlock( 1, testData.longText ); - expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 3 ); - } ); - - it( 'should be able to merge blocks with unknown html elements', async () => { - await editorPage.initializeEditor( { - initialData: [ - testData.unknownElementParagraphBlock, - testData.lettersInParagraphBlock, - ].join( '\n\n' ), - } ); - - // Merge paragraphs. - const paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph, - 2 - ); - - const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); - const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 ); - - await clickBeginningOfElement( - editorPage.driver, - paragraphBlockElement - ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - backspace - ); - - // Verify the editor has not crashed. - const mergedBlockText = - await editorPage.getTextForParagraphBlockAtPosition( 1 ); - expect( text0 + text1 ).toMatch( mergedBlockText ); - } ); -} ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js index 26f4ff34ab3cff..cbabe343df5df5 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js @@ -2,7 +2,13 @@ * Internal dependencies */ import { blockNames } from './pages/editor-page'; -import { backspace, isAndroid, waitForMediaLibrary } from './helpers/utils'; +import { + backspace, + clickMiddleOfElement, + clickBeginningOfElement, + isAndroid, + waitForMediaLibrary, +} from './helpers/utils'; import testData from './helpers/test-data'; async function isImageBlockSelected() { @@ -192,4 +198,122 @@ describe( 'Gutenberg Editor Writing flow tests', () => { const typedText = await paragraphBlockElement.getText(); expect( typedText ).toMatch( testData.listItem1 ); } ); + + it( 'should be able to split one paragraph block into two', async () => { + await editorPage.initializeEditor(); + + const defaultBlockAppenderElement = + await editorPage.getDefaultBlockAppenderElement(); + await defaultBlockAppenderElement.click(); + + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph + ); + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + testData.shortText + ); + await clickMiddleOfElement( editorPage.driver, paragraphBlockElement ); + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + '\n', + false + ); + const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); + const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 ); + expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 2 ); + expect( text0 ).not.toBe( '' ); + expect( text1 ).not.toBe( '' ); + expect( testData.shortText ).toMatch( + new RegExp( `${ text0 + text1 }|${ text0 } ${ text1 }` ) + ); + } ); + + it( 'should be able to merge 2 paragraph blocks into 1', async () => { + await editorPage.initializeEditor(); + + const defaultBlockAppenderElement = + await editorPage.getDefaultBlockAppenderElement(); + await defaultBlockAppenderElement.click(); + + let paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph + ); + + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + testData.shortText + ); + await clickMiddleOfElement( editorPage.driver, paragraphBlockElement ); + await editorPage.typeTextToTextBlock( paragraphBlockElement, '\n' ); + + const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); + const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 ); + expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 2 ); + paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph, + 2 + ); + + await clickBeginningOfElement( + editorPage.driver, + paragraphBlockElement + ); + + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + backspace + ); + + const text = await editorPage.getTextForParagraphBlockAtPosition( 1 ); + expect( text0 + text1 ).toMatch( text ); + paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph, + 1 + ); + await paragraphBlockElement.click(); + expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 1 ); + } ); + + it( 'should be able to create a post with multiple paragraph blocks', async () => { + await editorPage.initializeEditor(); + const defaultBlockAppenderElement = + await editorPage.getDefaultBlockAppenderElement(); + await defaultBlockAppenderElement.click(); + + await editorPage.sendTextToParagraphBlock( 1, testData.longText ); + expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 3 ); + } ); + + it( 'should be able to merge blocks with unknown html elements', async () => { + await editorPage.initializeEditor( { + initialData: [ + testData.unknownElementParagraphBlock, + testData.lettersInParagraphBlock, + ].join( '\n\n' ), + } ); + + // Merge paragraphs. + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph, + 2 + ); + + const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); + const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 ); + + await clickBeginningOfElement( + editorPage.driver, + paragraphBlockElement + ); + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + backspace + ); + + // Verify the editor has not crashed. + const mergedBlockText = + await editorPage.getTextForParagraphBlockAtPosition( 1 ); + expect( text0 + text1 ).toMatch( mergedBlockText ); + } ); } ); From a7c219acfd4e44e55a6c8406dabb6c91c81fe0b8 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 29 Dec 2023 18:34:39 +0100 Subject: [PATCH 06/20] E2E Tests - Merge Heading block tests with the Writing flow tests --- .../gutenberg-editor-heading-@canary.test.js | 63 ------------------ ...enberg-editor-writing-flow-@canary.test.js | 66 ++++++++++++------- .../__device-tests__/pages/editor-page.js | 25 ++++--- 3 files changed, 59 insertions(+), 95 deletions(-) delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-heading-@canary.test.js diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-heading-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-heading-@canary.test.js deleted file mode 100644 index 50a2a3ee8fd640..00000000000000 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-heading-@canary.test.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Internal dependencies - */ -import { blockNames } from './pages/editor-page'; -import testData from './helpers/test-data'; - -describe( 'Gutenberg Editor tests', () => { - it( 'should be able to create a post with heading and paragraph blocks', async () => { - await editorPage.initializeEditor(); - await editorPage.addNewBlock( blockNames.heading ); - let headingBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.heading - ); - - await editorPage.typeTextToTextBlock( - headingBlockElement, - testData.heading - ); - - await editorPage.addNewBlock( blockNames.paragraph ); - let paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph, - 2 - ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - testData.mediumText - ); - - await editorPage.addNewBlock( blockNames.paragraph ); - paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph, - 3 - ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - testData.mediumText - ); - - await editorPage.addNewBlock( blockNames.heading ); - headingBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.heading, - 4 - ); - await editorPage.typeTextToTextBlock( - headingBlockElement, - testData.heading - ); - - await editorPage.addNewBlock( blockNames.paragraph ); - paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph, - 5 - ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - testData.mediumText - ); - - // Assert that even though there are 5 blocks, there should only be 3 paragraph blocks - expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 3 ); - } ); -} ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js index cbabe343df5df5..563f3bd186da4a 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js @@ -6,21 +6,10 @@ import { backspace, clickMiddleOfElement, clickBeginningOfElement, - isAndroid, waitForMediaLibrary, } from './helpers/utils'; import testData from './helpers/test-data'; -async function isImageBlockSelected() { - // Since there isn't an easy way to see if a block is selected, - // it will check if the edit image button is visible - const editImageElement = isAndroid() - ? '(//android.widget.Button[@content-desc="Edit image"])' - : '(//XCUIElementTypeButton[@name="Edit image"])'; - - return await editorPage.driver.$( editImageElement ).isDisplayed(); -} - describe( 'Gutenberg Editor Writing flow tests', () => { it( 'should be able to write a post title', async () => { await editorPage.initializeEditor( { initialTitle: '' } ); @@ -119,11 +108,9 @@ describe( 'Gutenberg Editor Writing flow tests', () => { // When deleting the Paragraph block, the keyboard should be hidden and // the image block should be focused. - expect( - await editorPage.getBlockAtPosition( blockNames.image ) - ).toBeTruthy(); + await editorPage.driver.pause( 1000 ); expect( await editorPage.driver.isKeyboardShown() ).toBe( false ); - expect( await isImageBlockSelected() ).toBe( true ); + expect( await editorPage.isImageBlockSelected() ).toBe( true ); // Adding a new Paragraph block await editorPage.addNewBlock( blockNames.paragraph ); @@ -148,7 +135,7 @@ describe( 'Gutenberg Editor Writing flow tests', () => { await editorPage.driver.pause( 1000 ); expect( await editorPage.driver.isKeyboardShown() ).toBe( false ); - expect( await isImageBlockSelected() ).toBe( true ); + expect( await editorPage.isImageBlockSelected() ).toBe( true ); } ); it( 'should manually dismiss the keyboard', async () => { @@ -214,19 +201,14 @@ describe( 'Gutenberg Editor Writing flow tests', () => { testData.shortText ); await clickMiddleOfElement( editorPage.driver, paragraphBlockElement ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - '\n', - false - ); + await editorPage.typeTextToTextBlock( paragraphBlockElement, '\n' ); + const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 ); + expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 2 ); expect( text0 ).not.toBe( '' ); expect( text1 ).not.toBe( '' ); - expect( testData.shortText ).toMatch( - new RegExp( `${ text0 + text1 }|${ text0 } ${ text1 }` ) - ); } ); it( 'should be able to merge 2 paragraph blocks into 1', async () => { @@ -316,4 +298,40 @@ describe( 'Gutenberg Editor Writing flow tests', () => { await editorPage.getTextForParagraphBlockAtPosition( 1 ); expect( text0 + text1 ).toMatch( mergedBlockText ); } ); + + it( 'should be able to create a post with heading and paragraph blocks', async () => { + await editorPage.initializeEditor(); + await editorPage.addNewBlock( blockNames.heading ); + const headingBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.heading + ); + + await editorPage.typeTextToTextBlock( + headingBlockElement, + testData.heading + ); + + await editorPage.addParagraphBlockByTappingEmptyAreaBelowLastBlock(); + let paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph, + 2 + ); + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + testData.mediumText + ); + + await editorPage.addParagraphBlockByTappingEmptyAreaBelowLastBlock(); + paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph, + 3 + ); + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + testData.mediumText + ); + + // Assert that even though there are 3 blocks, there should only be 2 paragraph blocks + expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 2 ); + } ); } ); diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js index 7ef744eb8c87a6..c36c32cd83363e 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -181,8 +181,10 @@ class EditorPage { } async addParagraphBlockByTappingEmptyAreaBelowLastBlock() { - const emptyAreaBelowLastBlock = - await this.driver.elementByAccessibilityId( 'Add paragraph block' ); + const element = isAndroid() + ? '~Add paragraph block' + : '(//XCUIElementTypeOther[@name="Add paragraph block"])'; + const emptyAreaBelowLastBlock = await this.driver.$( element ); await emptyAreaBelowLastBlock.click(); } @@ -442,8 +444,6 @@ class EditorPage { swipeRight: true, } ); await addButton[ 0 ].click(); - // Wait for Bottom sheet animation to finish - await this.driver.pause( 3000 ); } // Click on block of choice. @@ -461,10 +461,9 @@ class EditorPage { const inserterElement = isAndroid() ? 'Blocks menu' : 'InserterUI-Blocks'; - return await this.waitForElementToBeDisplayedById( - inserterElement, - 4000 - ); + await this.driver + .$( `~${ inserterElement }` ) + .waitForDisplayed( { timeout: 4000 } ); } static async isElementOutOfBounds( element, { width, height } = {} ) { @@ -862,6 +861,16 @@ class EditorPage { .perform(); } + async isImageBlockSelected() { + // Since there isn't an easy way to see if a block is selected, + // it will check if the edit image button is visible + const editImageElement = isAndroid() + ? '(//android.widget.Button[@content-desc="Edit image"])' + : '(//XCUIElementTypeButton[@name="Edit image"])'; + + return await this.driver.$( editImageElement ).isDisplayed(); + } + // ============================= // Search Block functions // ============================= From 9c53a3b87c55f372b87e51837e14bca70e525456 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 2 Jan 2024 10:35:55 +0100 Subject: [PATCH 07/20] Add missing part of the test --- .../gutenberg-editor-writing-flow-@canary.test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js index 563f3bd186da4a..677e084bcf6429 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js @@ -61,6 +61,13 @@ describe( 'Gutenberg Editor Writing flow tests', () => { paragraphBlockElement, testData.shortText ); + await editorPage.typeTextToTextBlock( paragraphBlockElement, '\n' ); + + // Expect to have a new Paragraph block and the keyboard visible + expect( + await editorPage.getTextBlockAtPosition( blockNames.paragraph, 2 ) + ).toBeTruthy(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); } ); it( 'should automatically dismiss the keyboard when selecting non-text-based-blocks', async () => { From f44a111d887d03d5b95bc02ff64f419e3e9785a2 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 2 Jan 2024 10:36:34 +0100 Subject: [PATCH 08/20] Fixes comment typo --- .../gutenberg-editor-writing-flow-@canary.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js index 677e084bcf6429..0aa7d769e195d7 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js @@ -160,7 +160,7 @@ describe( 'Gutenberg Editor Writing flow tests', () => { await editorPage.dismissKeyboard(); - // Checks that no block is select by looking for the block menu actions button + // Checks that no block is selected by looking for the block menu actions button expect( await editorPage.isBlockActionsMenuButtonDisplayed() ).toBe( false ); From b5f9fb7fa6bec789ceff4d51b9c0346b82908d72 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 2 Jan 2024 11:16:14 +0100 Subject: [PATCH 09/20] Fix typo --- .../gutenberg-editor-writing-flow-@canary.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js index 0aa7d769e195d7..ff63a12b4e1ddf 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js @@ -167,7 +167,7 @@ describe( 'Gutenberg Editor Writing flow tests', () => { expect( await editorPage.driver.isKeyboardShown() ).toBe( false ); } ); - it( 'should dimiss the keyboard and show it back when opening modals', async () => { + it( 'should dismiss the keyboard and show it back when opening modals', async () => { await editorPage.initializeEditor( { initialTitle: '' } ); const defaultBlockAppenderElement = From 119dcd63cd1ef49f775157497d0af2ee711fd5dd Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 2 Jan 2024 12:41:28 +0100 Subject: [PATCH 10/20] Increase pause value --- .../gutenberg-editor-writing-flow-@canary.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js index ff63a12b4e1ddf..3bb562d27addc8 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js @@ -115,7 +115,7 @@ describe( 'Gutenberg Editor Writing flow tests', () => { // When deleting the Paragraph block, the keyboard should be hidden and // the image block should be focused. - await editorPage.driver.pause( 1000 ); + await editorPage.driver.pause( 2000 ); expect( await editorPage.driver.isKeyboardShown() ).toBe( false ); expect( await editorPage.isImageBlockSelected() ).toBe( true ); From da9e54a691b88a2102a83a6c14f3e36d71a71640 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 2 Jan 2024 12:42:08 +0100 Subject: [PATCH 11/20] Add pause before dismissing the keyboard to avoid flakiness --- .../__device-tests__/gutenberg-editor-device-actions.test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js index e7ee4a20df03f2..66d498145917a7 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js @@ -27,6 +27,9 @@ describe( 'Gutenberg Editor Rotation tests', () => { await editorPage.addNewBlock( blockNames.paragraph ); if ( isAndroid() ) { + // We add a delay to avoid flakiness while the block is being created + // and the keyboard is shown. + await editorPage.driver.pause( 2000 ); await editorPage.dismissKeyboard(); } From 02e42d7a8006a168851dbe53f09170f49fdf34dd Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 2 Jan 2024 13:21:49 +0100 Subject: [PATCH 12/20] Remove pause in favor of waitUntil --- .../gutenberg-editor-device-actions.test.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js index 66d498145917a7..5ab7c18f6a4fc5 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js @@ -27,9 +27,10 @@ describe( 'Gutenberg Editor Rotation tests', () => { await editorPage.addNewBlock( blockNames.paragraph ); if ( isAndroid() ) { - // We add a delay to avoid flakiness while the block is being created - // and the keyboard is shown. - await editorPage.driver.pause( 2000 ); + // Waits until the keyboard is visible + await editorPage.driver.waitUntil( async function () { + return await editorPage.driver.isKeyboardShown(); + } ); await editorPage.dismissKeyboard(); } From 5b0823e7607a6be11546f5a54c58cf23011cb2f0 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 2 Jan 2024 13:36:55 +0100 Subject: [PATCH 13/20] Removes pause in favor of waitUntil when checking for the Keyboard state in some cases, which also expects the condition to be true. --- ...enberg-editor-writing-flow-@canary.test.js | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js index 3bb562d27addc8..6677cb5d240cc5 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js @@ -115,8 +115,9 @@ describe( 'Gutenberg Editor Writing flow tests', () => { // When deleting the Paragraph block, the keyboard should be hidden and // the image block should be focused. - await editorPage.driver.pause( 2000 ); - expect( await editorPage.driver.isKeyboardShown() ).toBe( false ); + await editorPage.driver.waitUntil( async function () { + return ! ( await editorPage.driver.isKeyboardShown() ); + } ); expect( await editorPage.isImageBlockSelected() ).toBe( true ); // Adding a new Paragraph block @@ -139,9 +140,10 @@ describe( 'Gutenberg Editor Writing flow tests', () => { blockNames.image ); await imageBlockElement.click(); - await editorPage.driver.pause( 1000 ); - expect( await editorPage.driver.isKeyboardShown() ).toBe( false ); + await editorPage.driver.waitUntil( async function () { + return ! ( await editorPage.driver.isKeyboardShown() ); + } ); expect( await editorPage.isImageBlockSelected() ).toBe( true ); } ); @@ -175,13 +177,15 @@ describe( 'Gutenberg Editor Writing flow tests', () => { await defaultBlockAppenderElement.click(); await editorPage.openBlockSettings(); - await editorPage.driver.pause( 1000 ); - expect( await editorPage.driver.isKeyboardShown() ).toBe( false ); + await editorPage.driver.waitUntil( async function () { + return ! ( await editorPage.driver.isKeyboardShown() ); + } ); await editorPage.dismissBottomSheet(); - await editorPage.driver.pause( 1000 ); - expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + await editorPage.driver.waitUntil( async function () { + return await editorPage.driver.isKeyboardShown(); + } ); const paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); From 7e6fa2d0646b078f3b2a2ba143b9d3d157caa0d6 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Wed, 3 Jan 2024 18:32:13 +0100 Subject: [PATCH 14/20] Simplify waitUntil to just pass the promise --- .../gutenberg-editor-device-actions.test.js | 6 +++--- .../gutenberg-editor-writing-flow-@canary.test.js | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js index 5ab7c18f6a4fc5..230c844491d282 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js @@ -28,9 +28,9 @@ describe( 'Gutenberg Editor Rotation tests', () => { if ( isAndroid() ) { // Waits until the keyboard is visible - await editorPage.driver.waitUntil( async function () { - return await editorPage.driver.isKeyboardShown(); - } ); + await editorPage.driver.waitUntil( + editorPage.driver.isKeyboardShown + ); await editorPage.dismissKeyboard(); } diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js index 6677cb5d240cc5..c91ef3f6b68c23 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js @@ -183,9 +183,7 @@ describe( 'Gutenberg Editor Writing flow tests', () => { await editorPage.dismissBottomSheet(); - await editorPage.driver.waitUntil( async function () { - return await editorPage.driver.isKeyboardShown(); - } ); + await editorPage.driver.waitUntil( editorPage.driver.isKeyboardShown ); const paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); From 0cdd36b93a0dcf87804ef301d6b96758cb7c69c4 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Wed, 3 Jan 2024 18:56:21 +0100 Subject: [PATCH 15/20] Rename getEmptyTitleElement to getEmptyTitleTextInputElement and move the logic to click on the title wrapper within the function --- .../gutenberg-editor-writing-flow-@canary.test.js | 8 +------- .../__device-tests__/pages/editor-page.js | 7 ++++++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js index c91ef3f6b68c23..2a22ad91926a07 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js @@ -14,14 +14,8 @@ describe( 'Gutenberg Editor Writing flow tests', () => { it( 'should be able to write a post title', async () => { await editorPage.initializeEditor( { initialTitle: '' } ); - const titleElement = await editorPage.getTitleElement( { - isEmpty: true, - } ); - await titleElement.click(); - + const titleInput = await editorPage.getEmptyTitleTextInputElement(); expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); - - const titleInput = await editorPage.getEmptyTitleElement(); await editorPage.typeTextToTextBlock( titleInput, testData.shortText ); // Trigger the return key to go to the first Paragraph diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js index c36c32cd83363e..a7e13e166a17e3 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -221,7 +221,12 @@ class EditorPage { return elements[ 0 ]; } - async getEmptyTitleElement() { + async getEmptyTitleTextInputElement() { + const titleWrapperElement = await this.getTitleElement( { + isEmpty: true, + } ); + await titleWrapperElement.click(); + const titleElement = isAndroid() ? '//android.widget.EditText[@content-desc="Post title. Empty"]' : '~Add title'; From 918be74ee6fb4aba99e194c151b0b4e76775716d Mon Sep 17 00:00:00 2001 From: Gerardo Date: Wed, 3 Jan 2024 19:32:51 +0100 Subject: [PATCH 16/20] Editor Page - Adds new param skipWrapperClick to getTextBlockAtPosition to avoid clicking on the wrapper on iOS if its set to true for cases where its already focused and the caret is at the desired position --- .../__device-tests__/pages/editor-page.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js index a7e13e166a17e3..b00be20458e802 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -82,9 +82,13 @@ class EditorPage { // Text blocks functions // E.g. Paragraph, Heading blocks // =============================== - async getTextBlockAtPosition( blockName, position = 1 ) { + async getTextBlockAtPosition( + blockName, + position = 1, + skipWrapperClick = false + ) { // iOS needs a click to get the text element - if ( ! isAndroid() ) { + if ( ! isAndroid() && ! skipWrapperClick ) { const textBlockLocator = `(//XCUIElementTypeButton[contains(@name, "${ blockName } Block. Row ${ position }")])`; await clickIfClickable( this.driver, textBlockLocator ); From b4522c309abab7bc0f23009a98ba72230575fa11 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Wed, 3 Jan 2024 19:40:29 +0100 Subject: [PATCH 17/20] Unify the split and merge tests into one --- ...enberg-editor-writing-flow-@canary.test.js | 89 +++++++++---------- 1 file changed, 41 insertions(+), 48 deletions(-) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js index 2a22ad91926a07..3a12bf5d13345b 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js @@ -4,7 +4,6 @@ import { blockNames } from './pages/editor-page'; import { backspace, - clickMiddleOfElement, clickBeginningOfElement, waitForMediaLibrary, } from './helpers/utils'; @@ -189,75 +188,69 @@ describe( 'Gutenberg Editor Writing flow tests', () => { expect( typedText ).toMatch( testData.listItem1 ); } ); - it( 'should be able to split one paragraph block into two', async () => { + it( 'should be able to split and merge paragraph blocks', async () => { await editorPage.initializeEditor(); + // Add the first Paragraph block using the default block appender const defaultBlockAppenderElement = await editorPage.getDefaultBlockAppenderElement(); await defaultBlockAppenderElement.click(); - const paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph - ); + // Type text into the first Paragraph block + const firstParagraphBlockElement = + await editorPage.getTextBlockAtPosition( blockNames.paragraph ); await editorPage.typeTextToTextBlock( - paragraphBlockElement, + firstParagraphBlockElement, testData.shortText ); - await clickMiddleOfElement( editorPage.driver, paragraphBlockElement ); - await editorPage.typeTextToTextBlock( paragraphBlockElement, '\n' ); - - const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); - const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 ); - - expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 2 ); - expect( text0 ).not.toBe( '' ); - expect( text1 ).not.toBe( '' ); - } ); - - it( 'should be able to merge 2 paragraph blocks into 1', async () => { - await editorPage.initializeEditor(); - - const defaultBlockAppenderElement = - await editorPage.getDefaultBlockAppenderElement(); - await defaultBlockAppenderElement.click(); - - let paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph - ); + // Add a second Paragraph block and type some text + await editorPage.addParagraphBlockByTappingEmptyAreaBelowLastBlock(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + const secondParagraphBlockElement = + await editorPage.getTextBlockAtPosition( blockNames.paragraph, 2 ); await editorPage.typeTextToTextBlock( - paragraphBlockElement, - testData.shortText - ); - await clickMiddleOfElement( editorPage.driver, paragraphBlockElement ); - await editorPage.typeTextToTextBlock( paragraphBlockElement, '\n' ); - - const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); - const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 ); - expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 2 ); - paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph, - 2 + secondParagraphBlockElement, + testData.mediumText ); + // Merge Paragraph blocks await clickBeginningOfElement( editorPage.driver, - paragraphBlockElement + secondParagraphBlockElement ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, + secondParagraphBlockElement, backspace ); - const text = await editorPage.getTextForParagraphBlockAtPosition( 1 ); - expect( text0 + text1 ).toMatch( text ); - paragraphBlockElement = await editorPage.getTextBlockAtPosition( + // Wait for blocks to be merged + await editorPage.driver.waitUntil( async function () { + return ( await editorPage.getNumberOfParagraphBlocks() ) === 1; + } ); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + + // Split the current Paragraph block right where the caret is positioned + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph, - 1 + 1, + true ); - await paragraphBlockElement.click(); - expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 1 ); + await editorPage.typeTextToTextBlock( paragraphBlockElement, '\n' ); + + // Wait for blocks to be split + await editorPage.driver.waitUntil( async function () { + return ( await editorPage.getNumberOfParagraphBlocks() ) === 2; + } ); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + + const firstParagraphText = + await editorPage.getTextForParagraphBlockAtPosition( 1 ); + const secondParagraphText = + await editorPage.getTextForParagraphBlockAtPosition( 2 ); + + expect( firstParagraphText ).toEqual( testData.shortText ); + expect( secondParagraphText ).toEqual( testData.mediumText ); } ); it( 'should be able to create a post with multiple paragraph blocks', async () => { From bbf277d8ad51f03aace008a134cadf48fa07e160 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 4 Jan 2024 13:03:16 +0100 Subject: [PATCH 18/20] Update `showSoftKeyboard` to wait for the text input to get focus --- .../ReactNativeAztec/ReactAztecText.java | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java index 71df8e0c2888a2..22c520758c06cb 100644 --- a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java @@ -1,5 +1,7 @@ package org.wordpress.mobile.ReactNativeAztec; +import static android.content.ClipData.Item; + import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; @@ -10,18 +12,19 @@ import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import android.text.Editable; import android.text.InputType; import android.text.Spannable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.View; +import android.view.ViewTreeObserver; import android.view.inputmethod.InputMethodManager; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReactContext; import com.facebook.react.uimanager.ThemedReactContext; @@ -41,12 +44,10 @@ import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.Set; -import java.util.HashSet; -import java.util.HashMap; - -import static android.content.ClipData.*; public class ReactAztecText extends AztecText { @@ -64,6 +65,7 @@ public class ReactAztecText extends AztecText { private @Nullable TextWatcherDelegator mTextWatcherDelegator; private @Nullable ContentSizeWatcher mContentSizeWatcher; private @Nullable ScrollWatcher mScrollWatcher; + private @Nullable Runnable mKeyboardRunnable; // FIXME: Used in `incrementAndGetEventCounter` but never read. I guess we can get rid of it, but before this // check when it's used in EditText in RN. (maybe tests?) @@ -264,14 +266,40 @@ public boolean requestFocus(int direction, Rect previouslyFocusedRect) { } private void showSoftKeyboard() { - new Handler(Looper.getMainLooper()).post(new Runnable() { + // If the text input is already focused we can show the keyboard. + if(hasWindowFocus()) { + showSoftKeyboardNow(); + } + // Otherwise, we'll wait until it gets focused. + else { + getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() { + @Override + public void onWindowFocusChanged(boolean hasFocus) { + if (hasFocus) { + showSoftKeyboardNow(); + getViewTreeObserver().removeOnWindowFocusChangeListener(this); + } + } + }); + } + } + + private void showSoftKeyboardNow() { + // Cancel any previously scheduled Runnable + if (mKeyboardRunnable != null) { + removeCallbacks(mKeyboardRunnable); + } + + mKeyboardRunnable = new Runnable() { @Override public void run() { if (mInputMethodManager != null) { - mInputMethodManager.showSoftInput(ReactAztecText.this, 0); + mInputMethodManager.showSoftInput(ReactAztecText.this, InputMethodManager.SHOW_IMPLICIT); } } - }); + }; + + post(mKeyboardRunnable); } private void hideSoftKeyboard() { From a3c7b7f205ce29701f8599eb7ff48791ebdf8b1f Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 4 Jan 2024 13:03:45 +0100 Subject: [PATCH 19/20] Check input method manager is defined before hiding keyboard --- .../org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java index 22c520758c06cb..380cdd1c5d6132 100644 --- a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java @@ -303,7 +303,9 @@ public void run() { } private void hideSoftKeyboard() { - mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + if (mInputMethodManager != null) { + mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + } } public void setScrollWatcher(ScrollWatcher scrollWatcher) { From 980cad0d37b827dab245797c8d67cf90d0505256 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 4 Jan 2024 13:04:45 +0100 Subject: [PATCH 20/20] Set wrapper view of the editor to be not focusable in the demo app This matches the behavior of the host app WP-Android. --- .../android/app/src/main/java/com/gutenberg/MainActivity.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java index 69985317ddad33..a938df715ec008 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java @@ -113,6 +113,8 @@ protected void onCreate(Bundle savedInstanceState) { LinearLayout linearLayout = new LinearLayout(this); linearLayout.setOrientation(LinearLayout.VERTICAL); linearLayout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + linearLayout.setFocusable(false); + linearLayout.setFocusableInTouchMode(true); // Create a Toolbar instance Toolbar toolbar = new Toolbar(this);