diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-drag-and-drop.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-drag-and-drop.test.js new file mode 100644 index 00000000000000..5d8e62cc7b664f --- /dev/null +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-drag-and-drop.test.js @@ -0,0 +1,153 @@ +/** + * Internal dependencies + */ +import { blockNames } from './pages/editor-page'; +import { + clearClipboard, + clickElementOutsideOfTextInput, + dragAndDropAfterElement, + isAndroid, + setClipboard, + tapPasteAboveElement, +} from './helpers/utils'; +import testData from './helpers/test-data'; + +describe( 'Gutenberg Editor Drag & Drop blocks tests', () => { + beforeEach( async () => { + await clearClipboard( editorPage.driver ); + } ); + + it( 'should be able to drag & drop a block', async () => { + // Initialize the editor with a Spacer and Paragraph block + await editorPage.setHtmlContent( + [ testData.spacerBlock, testData.paragraphBlockShortText ].join( + '\n\n' + ) + ); + + // Get elements for both blocks + const spacerBlock = await editorPage.getBlockAtPosition( + blockNames.spacer + ); + const paragraphBlock = await editorPage.getParagraphBlockWrapperAtPosition( + 2 + ); + + // Drag & drop the Spacer block after the Paragraph block + await dragAndDropAfterElement( + editorPage.driver, + spacerBlock, + paragraphBlock + ); + + // Get the first block, in this case the Paragraph block + // and check the text value is the expected one + const firstBlockText = await editorPage.getTextForParagraphBlockAtPosition( + 1 + ); + expect( firstBlockText ).toMatch( testData.shortText ); + + // Remove the blocks + await spacerBlock.click(); + await editorPage.removeBlockAtPosition( blockNames.spacer, 2 ); + await editorPage.removeBlockAtPosition( blockNames.paragraph, 1 ); + } ); + + it( 'should be able to long-press on a text-based block to paste a text in a focused textinput', async () => { + // Add a Paragraph block + await editorPage.addNewBlock( blockNames.paragraph ); + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph + ); + + // Set clipboard text + await setClipboard( editorPage.driver, testData.shortText ); + + // Dismiss auto-suggestion popup + if ( isAndroid() ) { + // On Andrdoid 10 a new auto-suggestion popup is appearing to let the user paste text recently put in the clipboard. Let's dismiss it. + await editorPage.dismissAndroidClipboardSmartSuggestion(); + } + + // Paste into the Paragraph block + await tapPasteAboveElement( editorPage.driver, paragraphBlockElement ); + const paragraphText = await editorPage.getTextForParagraphBlockAtPosition( + 1 + ); + + // Expect to have the pasted text in the Paragraph block + expect( paragraphText ).toMatch( testData.shortText ); + + // Remove the block + await editorPage.removeBlockAtPosition( blockNames.paragraph ); + } ); + + it( 'should be able to long-press on a text-based block using the PlainText component to paste a text in a focused textinput', async () => { + // Add a Shortcode block + await editorPage.addNewBlock( blockNames.shortcode ); + const shortcodeBlockElement = await editorPage.getShortBlockTextInputAtPosition( + blockNames.shortcode + ); + + // Set clipboard text + await setClipboard( editorPage.driver, testData.shortText ); + + // Dismiss auto-suggestion popup + if ( isAndroid() ) { + // On Andrdoid 10 a new auto-suggestion popup is appearing to let the user paste text recently put in the clipboard. Let's dismiss it. + await editorPage.dismissAndroidClipboardSmartSuggestion(); + } + + // Paste into the Shortcode block + await tapPasteAboveElement( editorPage.driver, shortcodeBlockElement ); + const shortcodeText = await shortcodeBlockElement.text(); + + // Expect to have the pasted text in the Shortcode block + expect( shortcodeText ).toMatch( testData.shortText ); + + // Remove the block + await editorPage.removeBlockAtPosition( blockNames.shortcode ); + } ); + + it( 'should be able to drag & drop a text-based block when the textinput is not focused', async () => { + // Initialize the editor with two Paragraph blocks + await editorPage.setHtmlContent( + [ + testData.paragraphBlockShortText, + testData.paragraphBlockEmpty, + ].join( '\n\n' ) + ); + + // Get elements for both blocks + const firstParagraphBlock = await editorPage.getParagraphBlockWrapperAtPosition( + 1 + ); + const secondParagraphBlock = await editorPage.getParagraphBlockWrapperAtPosition( + 2 + ); + + // Tap on the first Paragraph block outside of the textinput + await clickElementOutsideOfTextInput( + editorPage.driver, + firstParagraphBlock + ); + + // Drag & drop the first Paragraph block after the second Paragraph block + await dragAndDropAfterElement( + editorPage.driver, + firstParagraphBlock, + secondParagraphBlock + ); + + // Get the current second Paragraph block in the editor after dragging & dropping + const secondBlockText = await editorPage.getTextForParagraphBlockAtPosition( + 2 + ); + + // Expect the second Paragraph block to have the expected content + expect( secondBlockText ).toMatch( testData.shortText ); + + // Remove the block + await editorPage.removeBlockAtPosition( blockNames.paragraph ); + } ); +} ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-paste.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-paste.test.js index 7404b0969d6adb..63be583561e4e0 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-paste.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-paste.test.js @@ -3,6 +3,7 @@ */ import { blockNames } from './pages/editor-page'; import { + clearClipboard, longPressMiddleOfElement, tapSelectAllAboveElement, tapCopyAboveElement, @@ -21,7 +22,7 @@ describe( 'Gutenberg Editor paste tests', () => { } beforeAll( async () => { - await editorPage.driver.setClipboard( '', 'plaintext' ); + await clearClipboard( editorPage.driver ); } ); it( 'copies plain text from one paragraph block and pastes in another', async () => { @@ -58,10 +59,6 @@ describe( 'Gutenberg Editor paste tests', () => { ); // Paste into second paragraph block. - await longPressMiddleOfElement( - editorPage.driver, - paragraphBlockElement2 - ); await tapPasteAboveElement( editorPage.driver, paragraphBlockElement2 ); const text = await editorPage.getTextForParagraphBlockAtPosition( 2 ); @@ -101,10 +98,6 @@ describe( 'Gutenberg Editor paste tests', () => { ); // Paste into second paragraph block. - await longPressMiddleOfElement( - editorPage.driver, - paragraphBlockElement2 - ); await tapPasteAboveElement( editorPage.driver, paragraphBlockElement2 ); // Check styled text by verifying html contents. diff --git a/packages/react-native-editor/__device-tests__/helpers/test-data.js b/packages/react-native-editor/__device-tests__/helpers/test-data.js index 99fea16d643fce..ac225a02403215 100644 --- a/packages/react-native-editor/__device-tests__/helpers/test-data.js +++ b/packages/react-native-editor/__device-tests__/helpers/test-data.js @@ -166,6 +166,10 @@ exports.paragraphBlockEmpty = `

`; +exports.paragraphBlockShortText = ` +

Rock music approaches at high velocity.

+`; + exports.multiLinesParagraphBlock = `

multiple lines
multiple lines
multiple lines

`; @@ -177,3 +181,7 @@ exports.unknownElementParagraphBlock = ` exports.lettersInParagraphBlock = `

ABCD

`; + +exports.spacerBlock = ` + +`; diff --git a/packages/react-native-editor/__device-tests__/helpers/utils.js b/packages/react-native-editor/__device-tests__/helpers/utils.js index b3225a03cfbae0..cc1585892dfb96 100644 --- a/packages/react-native-editor/__device-tests__/helpers/utils.js +++ b/packages/react-native-editor/__device-tests__/helpers/utils.js @@ -46,6 +46,9 @@ const strToKeycode = { [ backspace ]: 67, }; +// $block-edge-to-content value +const blockEdgeToContent = 16; + const timer = ( ms ) => new Promise( ( res ) => setTimeout( res, ms ) ); const isAndroid = () => { @@ -301,18 +304,27 @@ const clickBeginningOfElement = async ( driver, element ) => { await action.perform(); }; +// Clicks in the top left of a text-based element outside of the TextInput +const clickElementOutsideOfTextInput = async ( driver, element ) => { + const location = await element.getLocation(); + const y = isAndroid() ? location.y - blockEdgeToContent : location.y; + const x = isAndroid() ? location.x - blockEdgeToContent : location.x; + + const action = new wd.TouchAction( driver ).press( { x, y } ).release(); + await action.perform(); +}; + // Long press to activate context menu. const longPressMiddleOfElement = async ( driver, element ) => { const location = await element.getLocation(); const size = await element.getSize(); - const action = await new wd.TouchAction( driver ); const x = location.x + size.width / 2; const y = location.y + size.height / 2; - action.press( { x, y } ); - // Setting to wait a bit longer because this is failing more frequently on the CI - action.wait( 5000 ); - action.release(); + const action = new wd.TouchAction( driver ) + .longPress( { x, y } ) + .wait( 5000 ) // Setting to wait a bit longer because this is failing more frequently on the CI + .release(); await action.perform(); }; @@ -342,13 +354,21 @@ const tapCopyAboveElement = async ( driver, element ) => { // Press "Paste" in floating context menu. const tapPasteAboveElement = async ( driver, element ) => { - const location = await element.getLocation(); - const action = await new wd.TouchAction( driver ); - action.wait( 2000 ); - action.press( { x: location.x + 100, y: location.y - 50 } ); - action.wait( 2000 ); - action.release(); - await action.perform(); + await longPressMiddleOfElement( driver, element ); + + if ( isAndroid() ) { + const location = await element.getLocation(); + const action = await new wd.TouchAction( driver ); + action.wait( 2000 ); + action.press( { x: location.x + 100, y: location.y - 50 } ); + action.wait( 2000 ); + action.release(); + await action.perform(); + } else { + const pasteButtonLocator = '//XCUIElementTypeMenuItem[@name="Paste"]'; + await clickIfClickable( driver, pasteButtonLocator ); + await driver.sleep( 3000 ); // Wait for paste notification to disappear. + } }; // Starts from the middle of the screen or the element(if specified) @@ -413,6 +433,29 @@ const swipeDown = async ( driver, delay = 3000 ) => { ); }; +// Drag & Drop after element +const dragAndDropAfterElement = async ( driver, element, nextElement ) => { + // Element to drag & drop + const elementLocation = await element.getLocation(); + const elementSize = await element.getSize(); + const x = elementLocation.x + elementSize.width / 2; + const y = elementLocation.y + elementSize.height / 2; + + // Element to drag & drop to + const nextElementLocation = await nextElement.getLocation(); + const nextElementSize = await nextElement.getSize(); + const nextYPosition = isAndroid() + ? elementLocation.y + nextElementLocation.y + nextElementSize.height + : nextElementLocation.y + nextElementSize.height; + + const action = new wd.TouchAction( driver ) + .press( { x, y } ) + .wait( 5000 ) + .moveTo( { x, y: nextYPosition } ) + .release(); + await action.perform(); +}; + const toggleHtmlMode = async ( driver, toggleOn ) => { if ( isAndroid() ) { // Hit the "Menu" key. @@ -575,17 +618,50 @@ const waitIfAndroid = async () => { } }; +/** + * Content type definitions. + * Note: Android only supports plaintext. + * + * @typedef {"plaintext" | "image" | "url"} ClipboardContentType + */ + +/** + * Helper to set content in the clipboard. + * + * @param {Object} driver Driver + * @param {string} content Content to set in the clipboard + * @param {ClipboardContentType} contentType Type of the content + */ +const setClipboard = async ( driver, content, contentType = 'plaintext' ) => { + const base64String = Buffer.from( content ).toString( 'base64' ); + await driver.setClipboard( base64String, contentType ); +}; + +/** + * Helper to clear the clipboard + * + * @param {Object} driver Driver + * @param {ClipboardContentType} contentType Type of the content + */ +const clearClipboard = async ( driver, contentType = 'plaintext' ) => { + await driver.setClipboard( '', contentType ); +}; + module.exports = { backspace, + clearClipboard, clickBeginningOfElement, + clickElementOutsideOfTextInput, clickIfClickable, clickMiddleOfElement, doubleTap, + dragAndDropAfterElement, isAndroid, isEditorVisible, isElementVisible, isLocalEnvironment, longPressMiddleOfElement, + setClipboard, setupDriver, stopDriver, swipeDown, 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 f7e69057ece2a2..92829551843ec4 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -7,6 +7,7 @@ const { isEditorVisible, isElementVisible, longPressMiddleOfElement, + setClipboard, setupDriver, stopDriver, swipeDown, @@ -227,9 +228,7 @@ class EditorPage { async setHtmlContent( html ) { await toggleHtmlMode( this.driver, true ); - const base64String = Buffer.from( html ).toString( 'base64' ); - - await this.driver.setClipboard( base64String, 'plaintext' ); + await setClipboard( this.driver, html ); const htmlContentView = await this.getTextViewForHtmlViewContent(); @@ -500,6 +499,15 @@ class EditorPage { // Paragraph Block functions // ========================= + async getParagraphBlockWrapperAtPosition( position = 1 ) { + // iOS needs a click to get the text element + const blockLocator = isAndroid() + ? `//android.view.ViewGroup[contains(@content-desc, "Paragraph Block. Row ${ position }")]` + : `(//XCUIElementTypeButton[contains(@name, "Paragraph Block. Row ${ position }")])`; + + return await waitForVisible( this.driver, blockLocator ); + } + async sendTextToParagraphBlock( position, text, clear ) { const paragraphs = text.split( '\n' ); for ( let i = 0; i < paragraphs.length; i++ ) { @@ -777,6 +785,29 @@ class EditorPage { async sauceJobStatus( allPassed ) { await this.driver.sauceJobStatus( allPassed ); } + + // ========================= + // Shortcode Block functions + // ========================= + + async getShortBlockTextInputAtPosition( blockName, position = 1 ) { + // iOS needs a click to get the text element + if ( ! isAndroid() ) { + const textBlockLocator = `(//XCUIElementTypeButton[contains(@name, "Shortcode Block. Row ${ position }")])`; + + const textBlock = await waitForVisible( + this.driver, + textBlockLocator + ); + await textBlock.click(); + } + + const blockLocator = isAndroid() + ? `//android.view.ViewGroup[@content-desc="Shortcode Block. Row ${ position }"]/android.view.ViewGroup/android.view.ViewGroup/android.widget.EditText` + : `//XCUIElementTypeButton[contains(@name, "Shortcode Block. Row ${ position }")]//XCUIElementTypeTextView`; + + return await waitForVisible( this.driver, blockLocator ); + } } const blockNames = { @@ -796,6 +827,7 @@ const blockNames = { separator: 'Separator', spacer: 'Spacer', verse: 'Verse', + shortcode: 'Shortcode', }; module.exports = { initializeEditorPage, blockNames };