diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion.test.js index ead25e7bee9a2e..4dfbf56f52a5a5 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion.test.js @@ -39,9 +39,6 @@ describe( 'Gutenberg Editor tests for Block insertion', () => { testData.blockInsertionHtml.toLowerCase() ); - // wait for the block editor to load and for accessibility ids to update - await editorPage.driver.sleep( 3000 ); - // Workaround for now since deleting the first element causes a crash on CI for Android if ( isAndroid() ) { paragraphBlockElement = await editorPage.getTextBlockAtPosition( @@ -55,8 +52,6 @@ describe( 'Gutenberg Editor tests for Block insertion', () => { await paragraphBlockElement.click(); await editorPage.removeBlockAtPosition( blockNames.paragraph, 3 ); for ( let i = 3; i > 0; i-- ) { - // wait for accessibility ids to update - await editorPage.driver.sleep( 1000 ); paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph, i, @@ -72,8 +67,6 @@ describe( 'Gutenberg Editor tests for Block insertion', () => { } } else { for ( let i = 4; i > 0; i-- ) { - // wait for accessibility ids to update - await editorPage.driver.sleep( 1000 ); paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-cover.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-cover.test.js index 8d6eaa429a9e80..4905936f7f8f3c 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-cover.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-cover.test.js @@ -2,95 +2,49 @@ * Internal dependencies */ import { blockNames } from './pages/editor-page'; -import { isAndroid, waitForVisible } from './helpers/utils'; +import { isAndroid } from './helpers/utils'; import testData from './helpers/test-data'; describe( 'Gutenberg Editor Cover Block test', () => { it( 'should displayed properly and have properly converted height (ios only)', async () => { - await editorPage.setHtmlContent( testData.coverHeightWithRemUnit ); - - const coverBlock = await editorPage.getBlockAtPosition( - blockNames.cover, - 1, - { useWaitForVisible: true } - ); - // Temporarily this test is skipped on Android,due to the inconsistency of the results, // which are related to getting values in raw pixels instead of density pixels on Android. /* eslint-disable jest/no-conditional-expect */ if ( ! isAndroid() ) { + await editorPage.setHtmlContent( testData.coverHeightWithRemUnit ); + + const coverBlock = await editorPage.getBlockAtPosition( + blockNames.cover + ); + const { height } = await coverBlock.getSize(); // Height is set to 20rem, where 1rem is 16. // There is also block's vertical padding equal 32. // Finally, the total height should be 20 * 16 + 32 = 352. expect( height ).toBe( 352 ); - } - /* eslint-enable jest/no-conditional-expect */ + /* eslint-enable jest/no-conditional-expect */ - await coverBlock.click(); - expect( coverBlock ).toBeTruthy(); - await editorPage.removeBlockAtPosition( blockNames.cover ); + await coverBlock.click(); + expect( coverBlock ).toBeTruthy(); + await editorPage.removeBlockAtPosition( blockNames.cover ); + } } ); // Testing this for iOS on a device is valuable to ensure that it properly // handles opening multiple modals, as only one can be open at a time. it( 'allows modifying media from within block settings', async () => { - await editorPage.setHtmlContent( testData.coverHeightWithRemUnit ); - - const coverBlock = await editorPage.getBlockAtPosition( - blockNames.cover, - 1, - { useWaitForVisible: true } - ); - await coverBlock.click(); - // Can only add image from media library on iOS if ( ! isAndroid() ) { - // Open block settings. - const settingsButton = await editorPage.driver.elementByAccessibilityId( - 'Open Settings' - ); - await settingsButton.click(); + await editorPage.setHtmlContent( testData.coverHeightWithRemUnit ); - // Add initial media via button within bottom sheet. - const mediaSection = await editorPage.driver.elementByAccessibilityId( - 'Media Add image or video' - ); - const addMediaButton = await mediaSection.elementByAccessibilityId( - 'Add image or video' + const coverBlock = await editorPage.getBlockAtPosition( + blockNames.cover ); - await addMediaButton.click(); - await editorPage.chooseMediaLibrary(); - await editorPage.driver.sleep( 2000 ); // Await media load. - // Get Edit image button of block - const editImageButtonLocator = - '//XCUIElementTypeButton[@name="Edit image"][@enabled="true"]'; - const blockEditImageButton = await waitForVisible( - editorPage.driver, - editImageButtonLocator - ); - - // Edit media within block settings. - await settingsButton.click(); - await editorPage.driver.sleep( 2000 ); // Await media load. - - // Get Edit image button of block settings. - // NOTE: Since we have multiple Edit image buttons at this - // point, we have to filter them to obtain the correct one. - const settingsEditImageButtons = await editorPage.driver.elementsByXPath( - editImageButtonLocator - ); - const settingsEditImageButton = settingsEditImageButtons.find( - ( element ) => element.value !== blockEditImageButton.value - ); - await settingsEditImageButton.click(); - - // Replace image. - const replaceButton = await editorPage.driver.elementByAccessibilityId( - 'Replace' - ); - await replaceButton.click(); + await editorPage.openBlockSettings( coverBlock ); + await editorPage.clickAddMediaFromCoverBlock(); + await editorPage.chooseMediaLibrary(); + await editorPage.replaceMediaImage(); // First modal should no longer be presented. const replaceButtons = await editorPage.driver.elementsByAccessibilityId( @@ -101,9 +55,9 @@ describe( 'Gutenberg Editor Cover Block test', () => { // Select different media. await editorPage.chooseMediaLibrary(); - } - expect( coverBlock ).toBeTruthy(); - await editorPage.removeBlockAtPosition( blockNames.cover ); + expect( coverBlock ).toBeTruthy(); + await editorPage.removeBlockAtPosition( blockNames.cover ); + } } ); } ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-image-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-image-@canary.test.js index 03a929d5c12094..d07e8a3ea2cb26 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-image-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-image-@canary.test.js @@ -11,11 +11,7 @@ describe( 'Gutenberg Editor Image Block tests', () => { await editorPage.closePicker(); const imageBlock = await editorPage.getBlockAtPosition( - blockNames.image, - 1, - { - useWaitForVisible: true, - } + blockNames.image ); // Can only add image from media library on iOS diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-search.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-search.test.js index 7387e4b7125bc1..2233b56f6c37da 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-search.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-search.test.js @@ -124,6 +124,7 @@ describe( 'Gutenberg Editor Search Block tests.', () => { searchBlock, 'Button inside' ); + await editorPage.isSearchSettingsVisible(); await editorPage.dismissBottomSheet(); // Switch to html and verify. @@ -141,6 +142,7 @@ describe( 'Gutenberg Editor Search Block tests.', () => { searchBlock, 'No button' ); + await editorPage.isSearchSettingsVisible(); await editorPage.dismissBottomSheet(); // Switch to html and verify. diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-spacer.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-spacer.test.js index d234e9ae1dcae5..721ae86ea7eee4 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-spacer.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-spacer.test.js @@ -7,9 +7,7 @@ describe( 'Gutenberg Editor Spacer Block test', () => { it( 'should be able to add a spacer block', async () => { await editorPage.addNewBlock( blockNames.spacer ); const spacerBlock = await editorPage.getBlockAtPosition( - blockNames.spacer, - 1, - { useWaitForVisible: true } + blockNames.spacer ); expect( spacerBlock ).toBeTruthy(); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-unsupported-blocks.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-unsupported-blocks.test.js index 6e1be2a13dddb6..e2ae927d4f9449 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-unsupported-blocks.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-unsupported-blocks.test.js @@ -16,8 +16,7 @@ describe( 'Gutenberg Editor Unsupported Block Editor Tests', () => { const editButton = await editorPage.getUnsupportedBlockBottomSheetEditButton(); await editButton.click(); - await expect( - editorPage.getUnsupportedBlockWebView() - ).resolves.toBeTruthy(); + const webView = await editorPage.getUnsupportedBlockWebView(); + await expect( webView ).toBeTruthy(); } ); } ); diff --git a/packages/react-native-editor/__device-tests__/helpers/utils.js b/packages/react-native-editor/__device-tests__/helpers/utils.js index 1658d553c9c7a2..b3225a03cfbae0 100644 --- a/packages/react-native-editor/__device-tests__/helpers/utils.js +++ b/packages/react-native-editor/__device-tests__/helpers/utils.js @@ -310,7 +310,8 @@ const longPressMiddleOfElement = async ( driver, element ) => { const x = location.x + size.width / 2; const y = location.y + size.height / 2; action.press( { x, y } ); - action.wait( 2000 ); + // Setting to wait a bit longer because this is failing more frequently on the CI + action.wait( 5000 ); action.release(); await action.perform(); }; @@ -419,24 +420,28 @@ const toggleHtmlMode = async ( driver, toggleOn ) => { const showHtmlButtonXpath = '/hierarchy/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.ListView/android.widget.TextView[9]'; - const showHtmlButton = await driver.elementByXPath( - showHtmlButtonXpath + + await clickIfClickable( driver, showHtmlButtonXpath ); + } else if ( toggleOn ) { + await clickIfClickable( + driver, + '//XCUIElementTypeButton[@name="..."]' + ); + await clickIfClickable( + driver, + '//XCUIElementTypeButton[@name="Switch to HTML"]' ); - await showHtmlButton.click(); } else { - const menuButton = await driver.elementByAccessibilityId( '...' ); - await menuButton.click(); - let toggleHtmlButton; - if ( toggleOn ) { - toggleHtmlButton = await driver.elementByAccessibilityId( - 'Switch to HTML' - ); - } else { - toggleHtmlButton = await driver.elementByAccessibilityId( - 'Switch To Visual' - ); - } - await toggleHtmlButton.click(); + // This is to wait for the clipboard paste notification to disappear, currently it overlaps with the menu button + await driver.sleep( 3000 ); + await clickIfClickable( + driver, + '//XCUIElementTypeButton[@name="..."]' + ); + await clickIfClickable( + driver, + '//XCUIElementTypeButton[@name="Switch To Visual"]' + ); } }; @@ -492,7 +497,7 @@ const waitForVisible = async ( } const element = await driver.elementsByXPath( elementLocator ); - if ( element.length !== 1 ) { + if ( element.length === 0 ) { // if locator is not visible, try again return waitForVisible( driver, @@ -530,6 +535,39 @@ const isElementVisible = async ( return true; }; +const clickIfClickable = async ( + driver, + elementLocator, + maxIteration = 25, + iteration = 0 +) => { + const element = await waitForVisible( + driver, + elementLocator, + maxIteration, + iteration + ); + + try { + return await element.click(); + } catch ( error ) { + if ( iteration >= maxIteration ) { + // eslint-disable-next-line no-console + console.error( + `"${ elementLocator }" still not clickable after "${ iteration }" retries` + ); + return ''; + } + + return clickIfClickable( + driver, + elementLocator, + maxIteration, + iteration + 1 + ); + } +}; + // Only for Android const waitIfAndroid = async () => { if ( isAndroid() ) { @@ -540,6 +578,7 @@ const waitIfAndroid = async () => { module.exports = { backspace, clickBeginningOfElement, + clickIfClickable, clickMiddleOfElement, doubleTap, isAndroid, 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 440e256566ef91..f7e69057ece2a2 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -15,6 +15,7 @@ const { toggleHtmlMode, typeString, waitForVisible, + clickIfClickable, } = require( '../helpers/utils' ); const initializeEditorPage = async () => { @@ -79,31 +80,24 @@ class EditorPage { async getBlockAtPosition( blockName, position = 1, - options = { autoscroll: false, useWaitForVisible: false } + options = { autoscroll: false } ) { - let blockLocator; - - // Make it optional to use waitForVisible() so we can handle this test by test. - // This condition can be removed once we have gone through all test cases. - if ( options.useWaitForVisible ) { - let elementType; - switch ( blockName ) { - case blockNames.cover: - elementType = 'XCUIElementTypeButton'; - break; - default: - elementType = 'XCUIElementTypeOther'; - break; - } + let elementType; + switch ( blockName ) { + case blockNames.cover: + elementType = 'XCUIElementTypeButton'; + break; + default: + elementType = 'XCUIElementTypeOther'; + break; + } - blockLocator = isAndroid() - ? `//android.view.ViewGroup[contains(@${ this.accessibilityIdXPathAttrib }, "${ blockName } Block. Row ${ position }")]` - : `(//${ elementType }[contains(@${ this.accessibilityIdXPathAttrib }, "${ blockName } Block. Row ${ position }")])[1]`; + const blockLocator = isAndroid() + ? `//android.view.ViewGroup[contains(@${ this.accessibilityIdXPathAttrib }, "${ blockName } Block. Row ${ position }")]` + : `(//${ elementType }[contains(@${ this.accessibilityIdXPathAttrib }, "${ blockName } Block. Row ${ position }")])[1]`; + + await waitForVisible( this.driver, blockLocator ); - await waitForVisible( this.driver, blockLocator ); - } else { - blockLocator = `//*[contains(@${ this.accessibilityIdXPathAttrib }, "${ blockName } Block. Row ${ position }")]`; - } const elements = await this.driver.elementsByXPath( blockLocator ); const lastElementFound = elements[ elements.length - 1 ]; if ( elements.length === 0 && options.autoscroll ) { @@ -151,12 +145,12 @@ class EditorPage { async getFirstBlockVisible() { const firstBlockLocator = `//*[contains(@${ this.accessibilityIdXPathAttrib }, " Block. Row ")]`; - const elements = await this.driver.elementsByXPath( firstBlockLocator ); - return elements[ 0 ]; + return await waitForVisible( this.driver, firstBlockLocator ); } async getLastBlockVisible() { const firstBlockLocator = `//*[contains(@${ this.accessibilityIdXPathAttrib }, " Block. Row ")]`; + await waitForVisible( this.driver, firstBlockLocator ); const elements = await this.driver.elementsByXPath( firstBlockLocator ); return elements[ elements.length - 1 ]; } @@ -164,9 +158,7 @@ class EditorPage { async hasBlockAtPosition( position = 1, blockName = '' ) { return ( undefined !== - ( await this.getBlockAtPosition( blockName, position, { - useWaitForVisible: true, - } ) ) + ( await this.getBlockAtPosition( blockName, position ) ) ); } @@ -252,19 +244,18 @@ class EditorPage { // Sometimes double tap is not enough for paste menu to appear, so we also long press. await longPressMiddleOfElement( this.driver, htmlContentView ); - const pasteButton = this.driver.elementByXPath( + const pasteButton = await waitForVisible( + this.driver, '//XCUIElementTypeMenuItem[@name="Paste"]' ); await pasteButton.click(); - await this.driver.sleep( 3000 ); // Wait for paste notification to disappear. } await toggleHtmlMode( this.driver, false ); } async dismissKeyboard() { - await this.driver.sleep( 1000 ); // Wait for any keyboard animations. const keyboardShown = await this.driver.isKeyboardShown(); if ( ! keyboardShown ) { return; @@ -325,11 +316,7 @@ class EditorPage { ? '//android.widget.Button[@content-desc="Add Block Before"]' : '//XCUIElementTypeButton[@name="Add Block Before"]'; - const addBlockBeforeButton = await waitForVisible( - this.driver, - addBlockBeforeButtonLocator - ); - await addBlockBeforeButton.click(); + await clickIfClickable( this.driver, addBlockBeforeButtonLocator ); } else { await addButton.click(); } @@ -486,9 +473,7 @@ class EditorPage { blockActionsMenuButtonLocator ); if ( isAndroid() ) { - const block = await this.getBlockAtPosition( blockName, position, { - useWaitForVisible: true, - } ); + const block = await this.getBlockAtPosition( blockName, position ); let checkList = await this.driver.elementsByXPath( blockActionsMenuButtonLocator ); @@ -572,20 +557,17 @@ class EditorPage { ) { // iOS needs a few extra steps to get the text element if ( ! isAndroid() ) { - // Wait for and click the list in the correct position - let listBlock = await waitForVisible( + // Click the list in the correct position + await clickIfClickable( this.driver, `(//XCUIElementTypeOther[contains(@name, "List Block. Row ${ position }")])[1]` ); - await listBlock.click(); const listBlockLocator = options.isEmptyBlock ? `(//XCUIElementTypeStaticText[contains(@name, "List")])` : `//XCUIElementTypeButton[contains(@name, "List")]`; - // Wait for and click the list to get the text element - listBlock = await waitForVisible( this.driver, listBlockLocator ); - await listBlock.click(); + await clickIfClickable( this.driver, listBlockLocator ); } const listBlockTextLocatorIOS = options.isEmptyBlock @@ -608,6 +590,33 @@ class EditorPage { await this.clickToolBarButton( this.orderedListButtonName ); } + // ========================= + // Cover Block functions + // For iOS only + // ========================= + + async clickAddMediaFromCoverBlock() { + const mediaSection = await waitForVisible( + this.driver, + '//XCUIElementTypeOther[@name="Media Add image or video"]' + ); + const addMediaButton = await mediaSection.elementByAccessibilityId( + 'Add image or video' + ); + await addMediaButton.click(); + } + + async replaceMediaImage() { + await clickIfClickable( + this.driver, + '(//XCUIElementTypeButton[@name="Edit image"])[1]' + ); + await clickIfClickable( + this.driver, + '//XCUIElementTypeButton[@name="Replace"]' + ); + } + // ========================= // Image Block functions // ========================= @@ -687,10 +696,12 @@ class EditorPage { const elementName = isAndroid() ? '//*' : '//XCUIElementTypeOther'; const locator = `${ elementName }[starts-with(@${ this.accessibilityIdXPathAttrib }, "Hide search heading")]`; - return await this.driver - .elementByXPath( locator ) - .click() - .sleep( isAndroid() ? 200 : 0 ); + const hideSearchHeadingToggle = await waitForVisible( + this.driver, + locator + ); + + return await hideSearchHeadingToggle.click(); } async changeSearchButtonPositionSetting( block, buttonPosition ) { @@ -699,13 +710,16 @@ class EditorPage { const elementName = isAndroid() ? '//*' : '//XCUIElementTypeButton'; const locator = `${ elementName }[starts-with(@${ this.accessibilityIdXPathAttrib }, "Button position")]`; - await this.driver.elementByXPath( locator ).click(); + let optionMenuButton = await waitForVisible( this.driver, locator ); + await optionMenuButton.click(); const optionMenuButtonLocator = `${ elementName }[contains(@${ this.accessibilityIdXPathAttrib }, "${ buttonPosition }")]`; - return await this.driver - .elementByXPath( optionMenuButtonLocator ) - .click() - .sleep( isAndroid() ? 600 : 200 ); // sleep a little longer due to multiple menus. + optionMenuButton = await waitForVisible( + this.driver, + optionMenuButtonLocator + ); + + return await optionMenuButton.click(); } async toggleSearchIconOnlySetting( block ) { @@ -714,10 +728,16 @@ class EditorPage { const elementName = isAndroid() ? '//*' : '//XCUIElementTypeOther'; const locator = `${ elementName }[starts-with(@${ this.accessibilityIdXPathAttrib }, "Use icon button")]`; - return await this.driver - .elementByXPath( locator ) - .click() - .sleep( isAndroid() ? 200 : 0 ); + const useIconButton = await waitForVisible( this.driver, locator ); + + return await useIconButton.click(); + } + + async isSearchSettingsVisible() { + const elementName = isAndroid() ? '//*' : '//XCUIElementTypeButton'; + const buttonPositionLocator = `${ elementName }[starts-with(@${ this.accessibilityIdXPathAttrib }, "Button position")]`; + + return await waitForVisible( this.driver, buttonPositionLocator ); } // ============================= @@ -726,37 +746,28 @@ class EditorPage { async getUnsupportedBlockHelpButton() { const accessibilityId = 'Help button'; - let blockLocator = - '//android.widget.Button[@content-desc="Help button, Tap here to show help"]'; + const blockLocator = isAndroid() + ? `//android.widget.Button[starts-with(@content-desc, "${ accessibilityId }")]` + : `//XCUIElementTypeButton[@name="${ accessibilityId }"]`; - if ( ! isAndroid() ) { - blockLocator = `//XCUIElementTypeButton[@name="${ accessibilityId }"]`; - } - return await this.driver.elementByXPath( blockLocator ); + return await waitForVisible( this.driver, blockLocator ); } async getUnsupportedBlockBottomSheetEditButton() { const accessibilityId = 'Edit using web editor'; - let blockLocator = - '//android.widget.Button[@content-desc="Edit using web editor"]'; + const blockLocator = isAndroid() + ? `//android.widget.Button[@content-desc="${ accessibilityId }"]` + : `//XCUIElementTypeButton[@name="${ accessibilityId }"]`; - if ( ! isAndroid() ) { - blockLocator = `//XCUIElementTypeButton[@name="${ accessibilityId }"]`; - } - return await this.driver.elementByXPath( blockLocator ); + return await waitForVisible( this.driver, blockLocator ); } async getUnsupportedBlockWebView() { - let blockLocator = '//android.webkit.WebView'; - - if ( ! isAndroid() ) { - blockLocator = '//XCUIElementTypeWebView'; - } + const blockLocator = isAndroid() + ? '//android.webkit.WebView' + : '//XCUIElementTypeWebView'; - this.driver.setImplicitWaitTimeout( 20000 ); - const element = await this.driver.elementByXPath( blockLocator ); - this.driver.setImplicitWaitTimeout( 5000 ); - return element; + return await waitForVisible( this.driver, blockLocator ); } async stopDriver() {