diff --git a/.github/workflows/rnmobile-android-runner.yml b/.github/workflows/rnmobile-android-runner.yml new file mode 100644 index 00000000000000..dca51aa0749de8 --- /dev/null +++ b/.github/workflows/rnmobile-android-runner.yml @@ -0,0 +1,45 @@ +name: React Native E2E Tests (Android) +on: push + +jobs: + test: + runs-on: macos-latest + strategy: + matrix: + native-test-name: [ + gutenberg-editor-heading, + gutenberg-editor-image, + ] + + steps: + - name: checkout + uses: actions/checkout@v2 + + - name: Restore npm cache + uses: actions/cache@v1 + with: + path: ~/.npm + key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm- + + - run: npm ci + + - name: Restore Gradle cache + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + + - uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 28 + profile: pixel_xl + script: npm run native test:e2e:android:local ${{ matrix.native-test-name }} + + - uses: actions/upload-artifact@v2 + if: always() + with: + name: android-screen-recordings + path: packages/react-native-editor/android-screen-recordings diff --git a/.github/workflows/rnmobile-ios-runner.yml b/.github/workflows/rnmobile-ios-runner.yml new file mode 100644 index 00000000000000..77129db4199360 --- /dev/null +++ b/.github/workflows/rnmobile-ios-runner.yml @@ -0,0 +1,71 @@ +name: React Native E2E Tests (iOS) +on: push + +jobs: + test: + runs-on: macos-latest + strategy: + matrix: + native-test-name: [ + gutenberg-editor-heading, + gutenberg-editor-image, + ] + + steps: + - uses: actions/checkout@v2 + + - name: Restore npm cache + uses: actions/cache@v1 + with: + path: ~/.npm + key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm- + + - name: Install dependencies + run: | + source ~/.nvm/nvm.sh + nvm install + npm ci + + - name: Prepare build cache key + run: find package-lock.json packages/react-native-editor/ios packages/react-native-aztec/ios packages/react-native-bridge/ios -type f -print0 | sort -z | xargs -0 shasum | tee ios-checksums.txt + + - name: Restore build cache + uses: actions/cache@v1 + with: + path: packages/react-native-editor/ios/build/gutenberg/Build/Products/Release-iphonesimulator/GutenbergDemo.app + key: ${{ runner.os }}-ios-build-${{ hashFiles('ios-checksums.txt') }} + + - name: Restore pods cache + uses: actions/cache@v1 + with: + path: | + packages/react-native-editor/ios/Pods + ~/Library/Caches/CocoaPods + ~/.cocoapods/repos/trunk + packages/react-native-editor/ios/vendor + key: ${{ runner.os }}-pods-${{ hashFiles('packages/react-native-editor/ios/Gemfile.lock') }}-${{ hashFiles('packages/react-native-editor/ios/Podfile.lock') }}-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-pods-${{ hashFiles('packages/react-native-editor/ios/Gemfile.lock') }}-${{ hashFiles('packages/react-native-editor/ios/Podfile.lock') }}-${{ hashFiles('package-lock.json') }} + ${{ runner.os }}-pods-${{ hashFiles('packages/react-native-editor/ios/Gemfile.lock') }}-${{ hashFiles('packages/react-native-editor/ios/Podfile.lock') }}- + ${{ runner.os }}-pods-${{ hashFiles('packages/react-native-editor/ios/Gemfile.lock') }}- + ${{ runner.os }}-pods- + + - name: Bundle iOS + run: npm run native test:e2e:bundle:ios + + - name: Build (if needed) + run: test -e packages/react-native-editor/ios/build/gutenberg/Build/Products/Release-iphonesimulator/GutenbergDemo.app/GutenbergDemo || SKIP_BUNDLING=true npm run native test:e2e:build-app:ios + + - name: Run iOS Device Tests + run: TEST_RN_PLATFORM=ios npm run native device-tests:local ${{ matrix.native-test-name }} + + - name: Prepare build cache + run: rm packages/react-native-editor/ios/build/gutenberg/Build/Products/Release-iphonesimulator/GutenbergDemo.app/main.jsbundle + + - uses: actions/upload-artifact@v2 + if: always() + with: + name: ios-screen-recordings + path: packages/react-native-editor/ios-screen-recordings diff --git a/packages/react-native-editor/.gitignore b/packages/react-native-editor/.gitignore index b7e17bf8a14fb6..5ec8884e0d751d 100644 --- a/packages/react-native-editor/.gitignore +++ b/packages/react-native-editor/.gitignore @@ -27,6 +27,7 @@ bundle/ *.ap_ .gradle/ android/app/src/main/assets/ +android/app/src/main/res/raw/ # iOS builds *.app.zip @@ -105,6 +106,8 @@ buck-out/ # e2e output log appium-out.log +ios-screen-recordings/ +android-screen-recordings/ bin/wp-cli.phar 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 553b02a99ea424..2d8ce89ffda3ef 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 @@ -12,12 +12,13 @@ import { } from './helpers/utils'; import testData from './helpers/test-data'; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000000; +jest.setTimeout( 1000000 ); describe( 'Gutenberg Editor tests for Block insertion', () => { let driver; let editorPage; let allPassed = true; + const paragraphBlockName = 'Paragraph'; // Use reporter for setting status for saucelabs Job if ( ! isLocalEnvironment() ) { @@ -41,33 +42,31 @@ describe( 'Gutenberg Editor tests for Block insertion', () => { } ); it( 'should be able to insert block into post', async () => { - await editorPage.addNewParagraphBlock(); - let paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( - 1 + await editorPage.addNewBlock( paragraphBlockName ); + let paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName ); if ( isAndroid() ) { await paragraphBlockElement.click(); } - await editorPage.sendTextToParagraphBlockAtPosition( - 1, - testData.longText - ); + + await editorPage.sendTextToParagraphBlock( 1, testData.longText ); // Should have 3 paragraph blocks at this point - paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( + paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName, 2 ); await paragraphBlockElement.click(); - await editorPage.addNewParagraphBlock(); - paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( + await editorPage.addNewBlock( paragraphBlockName ); + + paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName, 3 ); await paragraphBlockElement.click(); - await editorPage.sendTextToParagraphBlockAtPosition( - 3, - testData.mediumText - ); + await editorPage.sendTextToParagraphBlock( 3, testData.mediumText ); await editorPage.verifyHtmlContent( testData.blockInsertionHtml ); @@ -76,47 +75,52 @@ describe( 'Gutenberg Editor tests for Block insertion', () => { // Workaround for now since deleting the first element causes a crash on CI for Android if ( isAndroid() ) { - paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( + paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName, 3, - { autoscroll: true } + { + autoscroll: true, + } ); + await paragraphBlockElement.click(); - await editorPage.removeParagraphBlockAtPosition( 3 ); + await editorPage.removeBlockAtPosition( paragraphBlockName, 3 ); for ( let i = 3; i > 0; i-- ) { // wait for accessibility ids to update await driver.sleep( 1000 ); - paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( + paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName, i, - { autoscroll: true } + { + autoscroll: true, + } ); await paragraphBlockElement.click(); - await editorPage.removeParagraphBlockAtPosition( i ); + await editorPage.removeBlockAtPosition( paragraphBlockName, i ); } } else { for ( let i = 4; i > 0; i-- ) { // wait for accessibility ids to update await driver.sleep( 1000 ); - paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( - 1 + paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName ); await clickMiddleOfElement( driver, paragraphBlockElement ); - await editorPage.removeParagraphBlockAtPosition( 1 ); + await editorPage.removeBlockAtPosition( paragraphBlockName ); } } } ); it( 'should be able to insert block at the beginning of post from the title', async () => { - await editorPage.addNewParagraphBlock(); - let paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( - 1 + await editorPage.addNewBlock( paragraphBlockName ); + let paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName ); if ( isAndroid() ) { await paragraphBlockElement.click(); } - await editorPage.sendTextToParagraphBlockAtPosition( - 1, - testData.longText - ); + + await editorPage.sendTextToParagraphBlock( 1, testData.longText ); // Should have 3 paragraph blocks at this point if ( isAndroid() ) { @@ -130,15 +134,12 @@ describe( 'Gutenberg Editor tests for Block insertion', () => { await titleElement.click(); await titleElement.click(); - await editorPage.addNewParagraphBlock(); - paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( - 1 + await editorPage.addNewBlock( paragraphBlockName ); + paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName ); await clickMiddleOfElement( driver, paragraphBlockElement ); - await editorPage.sendTextToParagraphBlockAtPosition( - 1, - testData.mediumText - ); + await editorPage.sendTextToParagraphBlock( 1, testData.mediumText ); await paragraphBlockElement.click(); await editorPage.verifyHtmlContent( testData.blockInsertionHtmlFromTitle diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-gallery.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-gallery.test.js index d90542ce1ff09d..5872ad69803d0f 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-gallery.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-gallery.test.js @@ -4,12 +4,13 @@ import EditorPage from './pages/editor-page'; import { setupDriver, isLocalEnvironment, stopDriver } from './helpers/utils'; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000000; +jest.setTimeout( 1000000 ); describe( 'Gutenberg Editor Gallery Block tests', () => { let driver; let editorPage; let allPassed = true; + const galleryBlockName = 'Gallery'; // Use reporter for setting status for saucelabs Job if ( ! isLocalEnvironment() ) { @@ -32,11 +33,13 @@ describe( 'Gutenberg Editor Gallery Block tests', () => { } ); it( 'should be able to add a gallery block', async () => { - await editorPage.addNewGalleryBlock(); - const galleryBlock = await editorPage.getGalleryBlockAtPosition( 1 ); + await editorPage.addNewBlock( galleryBlockName ); + const galleryBlock = await editorPage.getBlockAtPosition( + galleryBlockName + ); expect( galleryBlock ).toBeTruthy(); - await editorPage.removeGalleryBlockAtPosition( 1 ); + await editorPage.removeBlockAtPosition( galleryBlockName ); } ); afterAll( async () => { diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-heading.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-heading.test.js index 8fec3cdeb4b0b2..83bc5184e78ae0 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-heading.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-heading.test.js @@ -10,12 +10,14 @@ import { } from './helpers/utils'; import testData from './helpers/test-data'; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000000; +jest.setTimeout( 1000000 ); describe( 'Gutenberg Editor tests', () => { let driver; let editorPage; let allPassed = true; + const paragraphBlockName = 'Paragraph'; + const headingBlockName = 'Heading'; // Use reporter for setting status for saucelabs Job if ( ! isLocalEnvironment() ) { @@ -38,49 +40,55 @@ describe( 'Gutenberg Editor tests', () => { } ); it( 'should be able to create a post with heading and paragraph blocks', async () => { - await editorPage.addNewHeadingBlock(); - let headingBlockElement = await editorPage.getHeadingBlockAtPosition( - 1 + await editorPage.addNewBlock( headingBlockName ); + let headingBlockElement = await editorPage.getBlockAtPosition( + headingBlockName ); - if ( isAndroid() ) { await headingBlockElement.click(); } await editorPage.sendTextToHeadingBlock( headingBlockElement, - testData.heading + testData.heading, + false ); - await editorPage.addNewParagraphBlock(); - let paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( + await editorPage.addNewBlock( paragraphBlockName ); + let paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName, 2 ); - await editorPage.sendTextToParagraphBlock( + await editorPage.typeTextToParagraphBlock( paragraphBlockElement, testData.mediumText ); - await editorPage.addNewParagraphBlock(); - paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( + await editorPage.addNewBlock( paragraphBlockName ); + paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName, 3 ); - await editorPage.sendTextToParagraphBlock( + await editorPage.typeTextToParagraphBlock( paragraphBlockElement, testData.mediumText ); - await editorPage.addNewHeadingBlock(); - headingBlockElement = await editorPage.getHeadingBlockAtPosition( 4 ); - await editorPage.sendTextToHeadingBlock( + await editorPage.addNewBlock( headingBlockName ); + headingBlockElement = await editorPage.getBlockAtPosition( + headingBlockName, + 4 + ); + await editorPage.typeTextToParagraphBlock( headingBlockElement, testData.heading ); - await editorPage.addNewParagraphBlock(); - paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( + await editorPage.addNewBlock( paragraphBlockName ); + paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName, 5 ); - await editorPage.sendTextToParagraphBlock( + await editorPage.typeTextToParagraphBlock( paragraphBlockElement, testData.mediumText ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-image.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-image.test.js index 56accc92b66297..d54eb9113f148d 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-image.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-image.test.js @@ -12,12 +12,14 @@ import { } from './helpers/utils'; import testData from './helpers/test-data'; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000000; +jest.setTimeout( 1000000 ); describe( 'Gutenberg Editor Image Block tests', () => { let driver; let editorPage; let allPassed = true; + const imageBlockName = 'Image'; + const paragraphBlockName = 'Paragraph'; // Use reporter for setting status for saucelabs Job if ( ! isLocalEnvironment() ) { @@ -40,8 +42,8 @@ describe( 'Gutenberg Editor Image Block tests', () => { } ); it( 'should be able to add an image block', async () => { - await editorPage.addNewImageBlock(); - let imageBlock = await editorPage.getImageBlockAtPosition( 1 ); + await editorPage.addNewBlock( imageBlockName ); + let imageBlock = await editorPage.getBlockAtPosition( imageBlockName ); // Can only add image from media library on iOS if ( ! isAndroid() ) { @@ -54,52 +56,28 @@ describe( 'Gutenberg Editor Image Block tests', () => { await editorPage.dismissKeyboard(); // end workaround - imageBlock = await editorPage.getImageBlockAtPosition( 1 ); - await imageBlock.click(); + imageBlock = await editorPage.getBlockAtPosition( imageBlock ); await swipeUp( driver, imageBlock ); await editorPage.enterCaptionToSelectedImageBlock( - testData.imageCaption + testData.imageCaption, + true ); await editorPage.dismissKeyboard(); - imageBlock = await editorPage.getImageBlockAtPosition( 1 ); - await imageBlock.click(); } - await editorPage.removeImageBlockAtPosition( 1 ); - } ); - - it( 'should be able to add an image block with multiple paragraph blocks', async () => { - await editorPage.addNewImageBlock(); - let imageBlock = await editorPage.getImageBlockAtPosition( 1 ); - - // Can only add image from media library on iOS - if ( ! isAndroid() ) { - await editorPage.selectEmptyImageBlock( imageBlock ); - await editorPage.chooseMediaLibrary(); - - imageBlock = await editorPage.getImageBlockAtPosition( 1 ); - await imageBlock.click(); - await swipeUp( driver, imageBlock ); - await editorPage.enterCaptionToSelectedImageBlock( - testData.imageCaption - ); - await editorPage.dismissKeyboard(); - } - - await editorPage.addNewParagraphBlock(); - const paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( + await editorPage.addNewBlock( paragraphBlockName ); + const paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName, 2 ); if ( isAndroid() ) { await paragraphBlockElement.click(); } - await editorPage.sendTextToParagraphBlockAtPosition( - 2, - testData.longText - ); + + await editorPage.sendTextToParagraphBlock( 2, testData.shortText ); // skip HTML check for Android since we couldn't add image from media library if ( ! isAndroid() ) { - await editorPage.verifyHtmlContent( testData.imageCompletehtml ); + await editorPage.verifyHtmlContent( testData.imageShorteHtml ); } } ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-latest-posts.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-latest-posts.test.js index 15074eba1ce094..160965d1afe967 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-latest-posts.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-latest-posts.test.js @@ -33,9 +33,9 @@ describe( 'Gutenberg Editor Latest Post Block tests', () => { } ); it( 'should be able to add a Latests-Posts block', async () => { - await editorPage.addNewLatestPostsBlock(); - const latestPostsBlock = await editorPage.getLatestPostsBlockAtPosition( - 1 + await editorPage.addNewBlock( lastPostBlockName ); + const latestPostsBlock = await editorPage.getBlockAtPosition( + lastPostBlockName ); expect( latestPostsBlock ).toBeTruthy(); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-end.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-end.test.js index 208ca36bfe9ef7..a39edd83c9fc29 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-end.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-end.test.js @@ -10,12 +10,13 @@ import { } from './helpers/utils'; import testData from './helpers/test-data'; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000000; +jest.setTimeout( 1000000 ); describe( 'Gutenberg Editor tests for List block (end)', () => { let driver; let editorPage; let allPassed = true; + const listBlockName = 'List'; // Use reporter for setting status for saucelabs Job if ( ! isLocalEnvironment() ) { @@ -38,8 +39,10 @@ describe( 'Gutenberg Editor tests for List block (end)', () => { } ); it( 'should be able to end a List block', async () => { - await editorPage.addNewListBlock(); - const listBlockElement = await editorPage.getListBlockAtPosition( 1 ); + await editorPage.addNewBlock( listBlockName ); + const listBlockElement = await editorPage.getBlockAtPosition( + listBlockName + ); // Click List block on Android to force EditText focus if ( isAndroid() ) { diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-lists.test.js index 7120997fbdda3f..d837d87bea5e80 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-lists.test.js @@ -10,12 +10,13 @@ import { } from './helpers/utils'; import testData from './helpers/test-data'; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000000; +jest.setTimeout( 1000000 ); describe( 'Gutenberg Editor tests for List block', () => { let driver; let editorPage; let allPassed = true; + const listBlockName = 'List'; // Use reporter for setting status for saucelabs Job if ( ! isLocalEnvironment() ) { @@ -38,9 +39,10 @@ describe( 'Gutenberg Editor tests for List block', () => { } ); it( 'should be able to add a new List block', async () => { - await editorPage.addNewListBlock(); - const listBlockElement = await editorPage.getListBlockAtPosition( 1 ); - + await editorPage.addNewBlock( listBlockName ); + const listBlockElement = await editorPage.getBlockAtPosition( + listBlockName + ); // Click List block on Android to force EditText focus if ( isAndroid() ) { await listBlockElement.click(); @@ -65,6 +67,21 @@ describe( 'Gutenberg Editor tests for List block', () => { await editorPage.verifyHtmlContent( testData.listHtml ); } ); + it( 'should update format to ordered list, using toolbar button', async () => { + const listBlockElement = await editorPage.getBlockAtPosition( + listBlockName + ); + + // Click List block to force EditText focus + await listBlockElement.click(); + + // Send a click on the order list format button + await editorPage.clickOrderedListToolBarButton(); + + // switch to html and verify html + await editorPage.verifyHtmlContent( testData.listHtmlOrdered ); + } ); + afterAll( async () => { if ( ! isLocalEnvironment() ) { driver.sauceJobStatus( allPassed ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-more.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-more.test.js index 1e8e4f2b2ba4ce..7b6aed2b2266c3 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-more.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-more.test.js @@ -4,7 +4,7 @@ import EditorPage from './pages/editor-page'; import { setupDriver, isLocalEnvironment, stopDriver } from './helpers/utils'; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000000; +jest.setTimeout( 1000000 ); describe( 'Gutenberg Editor Spacer Block test', () => { let driver; 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 86a9d66cc6813c..00017552cd329c 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 @@ -8,16 +8,18 @@ import { clickMiddleOfElement, clickBeginningOfElement, stopDriver, + swipeUp, isAndroid, } from './helpers/utils'; import testData from './helpers/test-data'; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000000; +jest.setTimeout( 1000000 ); describe( 'Gutenberg Editor tests for Paragraph Block', () => { let driver; let editorPage; let allPassed = true; + const paragraphBlockName = 'Paragraph'; // Use reporter for setting status for saucelabs Job if ( ! isLocalEnvironment() ) { @@ -40,29 +42,31 @@ describe( 'Gutenberg Editor tests for Paragraph Block', () => { } ); it( 'should be able to add a new Paragraph block', async () => { - await editorPage.addNewParagraphBlock(); - const paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( - 1 + await editorPage.addNewBlock( paragraphBlockName ); + const paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName ); if ( isAndroid() ) { await paragraphBlockElement.click(); } - await editorPage.sendTextToParagraphBlock( + + await editorPage.typeTextToParagraphBlock( paragraphBlockElement, testData.shortText ); - await editorPage.removeParagraphBlockAtPosition( 1 ); + await editorPage.removeBlockAtPosition( paragraphBlockName ); } ); it( 'should be able to split one paragraph block into two', async () => { - await editorPage.addNewParagraphBlock(); - const paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( - 1 + await editorPage.addNewBlock( paragraphBlockName ); + const paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName ); if ( isAndroid() ) { await paragraphBlockElement.click(); } - await editorPage.sendTextToParagraphBlock( + + await editorPage.typeTextToParagraphBlock( paragraphBlockElement, testData.shortText ); @@ -70,13 +74,14 @@ describe( 'Gutenberg Editor tests for Paragraph Block', () => { paragraphBlockElement ); await clickMiddleOfElement( driver, textViewElement ); - await editorPage.sendTextToParagraphBlock( + await editorPage.typeTextToParagraphBlock( paragraphBlockElement, - '\n' + '\n', + false ); expect( - ( await editorPage.hasParagraphBlockAtPosition( 1 ) ) && - ( await editorPage.hasParagraphBlockAtPosition( 2 ) ) + ( await editorPage.hasBlockAtPosition( 1, paragraphBlockName ) ) && + ( await editorPage.hasBlockAtPosition( 2, paragraphBlockName ) ) ).toBe( true ); const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); @@ -87,19 +92,20 @@ describe( 'Gutenberg Editor tests for Paragraph Block', () => { new RegExp( `${ text0 + text1 }|${ text0 } ${ text1 }` ) ); - await editorPage.removeParagraphBlockAtPosition( 2 ); - await editorPage.removeParagraphBlockAtPosition( 1 ); + await editorPage.removeBlockAtPosition( paragraphBlockName, 2 ); + await editorPage.removeBlockAtPosition( paragraphBlockName ); } ); it( 'should be able to merge 2 paragraph blocks into 1', async () => { - await editorPage.addNewParagraphBlock(); - let paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( - 1 + await editorPage.addNewBlock( paragraphBlockName ); + let paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName ); if ( isAndroid() ) { await paragraphBlockElement.click(); } - await editorPage.sendTextToParagraphBlock( + + await editorPage.typeTextToParagraphBlock( paragraphBlockElement, testData.shortText ); @@ -107,28 +113,30 @@ describe( 'Gutenberg Editor tests for Paragraph Block', () => { paragraphBlockElement ); await clickMiddleOfElement( driver, textViewElement ); - await editorPage.sendTextToParagraphBlock( + await editorPage.typeTextToParagraphBlock( paragraphBlockElement, '\n' ); expect( - ( await editorPage.hasParagraphBlockAtPosition( 1 ) ) && - ( await editorPage.hasParagraphBlockAtPosition( 2 ) ) + ( await editorPage.hasBlockAtPosition( 1, paragraphBlockName ) ) && + ( await editorPage.hasBlockAtPosition( 2, paragraphBlockName ) ) ).toBe( true ); const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 ); - paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( + paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName, 2 ); if ( isAndroid() ) { await paragraphBlockElement.click(); } + textViewElement = await editorPage.getTextViewForParagraphBlock( paragraphBlockElement ); await clickBeginningOfElement( driver, textViewElement ); - await editorPage.sendTextToParagraphBlock( + await editorPage.typeTextToParagraphBlock( paragraphBlockElement, '\u0008' ); @@ -136,27 +144,26 @@ describe( 'Gutenberg Editor tests for Paragraph Block', () => { const text = await editorPage.getTextForParagraphBlockAtPosition( 1 ); expect( text0 + text1 ).toMatch( text ); - expect( await editorPage.hasParagraphBlockAtPosition( 2 ) ).toBe( - false - ); - await editorPage.removeParagraphBlockAtPosition( 1 ); + expect( + await editorPage.hasBlockAtPosition( 2, paragraphBlockName ) + ).toBe( false ); + await editorPage.removeBlockAtPosition( paragraphBlockName ); } ); it( 'should be able to create a post with multiple paragraph blocks', async () => { - await editorPage.addNewParagraphBlock(); - const paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( - 1 + await editorPage.addNewBlock( paragraphBlockName ); + const paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName ); if ( isAndroid() ) { await paragraphBlockElement.click(); } - await editorPage.sendTextToParagraphBlockAtPosition( - 1, - testData.longText - ); + + await editorPage.sendTextToParagraphBlock( 1, testData.longText ); for ( let i = 3; i > 0; i-- ) { - await editorPage.removeParagraphBlockAtPosition( i ); + await swipeUp( driver ); + await editorPage.removeBlockAtPosition( paragraphBlockName, i ); } } ); 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 96a734e4c7bc30..75f18abd3c2511 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 @@ -14,7 +14,7 @@ import { } from './helpers/utils'; import testData from './helpers/test-data'; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000000; +jest.setTimeout( 1000000 ); describe( 'Gutenberg Editor paste tests', () => { // skip iOS for now @@ -26,6 +26,7 @@ describe( 'Gutenberg Editor paste tests', () => { let driver; let editorPage; let allPassed = true; + const paragraphBlockName = 'Paragraph'; // Use reporter for setting status for saucelabs Job if ( ! isLocalEnvironment() ) { @@ -45,16 +46,15 @@ describe( 'Gutenberg Editor paste tests', () => { } ); it( 'copies plain text from one paragraph block and pastes in another', async () => { - await editorPage.addNewParagraphBlock(); - const paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( - 1 + await editorPage.addNewBlock( paragraphBlockName ); + const paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName ); - if ( isAndroid() ) { await paragraphBlockElement.click(); } - await editorPage.sendTextToParagraphBlock( + await editorPage.typeTextToParagraphBlock( paragraphBlockElement, testData.pastePlainText ); @@ -68,11 +68,11 @@ describe( 'Gutenberg Editor paste tests', () => { await tapCopyAboveElement( driver, textViewElement ); // create another paragraph block - await editorPage.addNewParagraphBlock(); - const paragraphBlockElement2 = await editorPage.getParagraphBlockAtPosition( + await editorPage.addNewBlock( paragraphBlockName ); + const paragraphBlockElement2 = await editorPage.getBlockAtPosition( + paragraphBlockName, 2 ); - if ( isAndroid() ) { await paragraphBlockElement2.click(); } @@ -92,10 +92,9 @@ describe( 'Gutenberg Editor paste tests', () => { it( 'copies styled text from one paragraph block and pastes in another', async () => { // create paragraph block with styled text by editing html await editorPage.setHtmlContentAndroid( testData.pasteHtmlText ); - const paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( - 1 + const paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName ); - if ( isAndroid() ) { await paragraphBlockElement.click(); } @@ -110,11 +109,11 @@ describe( 'Gutenberg Editor paste tests', () => { await tapCopyAboveElement( driver, textViewElement ); // create another paragraph block - await editorPage.addNewParagraphBlock(); - const paragraphBlockElement2 = await editorPage.getParagraphBlockAtPosition( + await editorPage.addNewBlock( paragraphBlockName ); + const paragraphBlockElement2 = await editorPage.getBlockAtPosition( + paragraphBlockName, 2 ); - if ( isAndroid() ) { await paragraphBlockElement2.click(); } diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-rotation.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-rotation.test.js index 9c82ab7392c8ce..85e426aa5dff3f 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-rotation.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-rotation.test.js @@ -11,12 +11,13 @@ import { } from './helpers/utils'; import testData from './helpers/test-data'; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000000; +jest.setTimeout( 1000000 ); describe( 'Gutenberg Editor tests', () => { let driver; let editorPage; let allPassed = true; + const paragraphBlockName = 'Paragraph'; // Use reporter for setting status for saucelabs Job if ( ! isLocalEnvironment() ) { @@ -39,15 +40,15 @@ describe( 'Gutenberg Editor tests', () => { } ); it( 'should be able to add blocks , rotate device and continue adding blocks', async () => { - await editorPage.addNewParagraphBlock(); - let paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( - 1 + await editorPage.addNewBlock( paragraphBlockName ); + let paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName ); if ( isAndroid() ) { await paragraphBlockElement.click(); } - await editorPage.sendTextToParagraphBlock( + await editorPage.typeTextToParagraphBlock( paragraphBlockElement, testData.mediumText ); @@ -58,22 +59,24 @@ describe( 'Gutenberg Editor tests', () => { await driver.hideDeviceKeyboard(); } - await editorPage.addNewParagraphBlock(); + await editorPage.addNewBlock( paragraphBlockName ); if ( isAndroid() ) { await driver.hideDeviceKeyboard(); } - paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( + paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName, 2 ); while ( ! paragraphBlockElement ) { await driver.hideDeviceKeyboard(); - paragraphBlockElement = await editorPage.getParagraphBlockAtPosition( + paragraphBlockElement = await editorPage.getBlockAtPosition( + paragraphBlockName, 2 ); } - await editorPage.sendTextToParagraphBlock( + await editorPage.typeTextToParagraphBlock( paragraphBlockElement, testData.mediumText ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-separator.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-separator.test.js index 7c669e6c574fa5..438e1a5c1c9c5b 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-separator.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-separator.test.js @@ -4,7 +4,7 @@ import EditorPage from './pages/editor-page'; import { setupDriver, isLocalEnvironment, stopDriver } from './helpers/utils'; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000000; +jest.setTimeout( 1000000 ); describe( 'Gutenberg Editor Separator Block test', () => { let driver; 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 056abacedc6052..2aa09cc523bb15 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 @@ -4,7 +4,7 @@ import EditorPage from './pages/editor-page'; import { setupDriver, isLocalEnvironment, stopDriver } from './helpers/utils'; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000000; +jest.setTimeout( 1000000 ); describe( 'Gutenberg Editor Spacer Block test', () => { let driver; diff --git a/packages/react-native-editor/__device-tests__/helpers/caps.js b/packages/react-native-editor/__device-tests__/helpers/caps.js index c0c9ddda2fe309..89ad3d8b2dd92b 100644 --- a/packages/react-native-editor/__device-tests__/helpers/caps.js +++ b/packages/react-native-editor/__device-tests__/helpers/caps.js @@ -1,7 +1,7 @@ const ios = { browserName: '', platformName: 'iOS', - platformVersion: '13.3', + platformVersion: '13.4', deviceName: 'iPhone 11', os: 'iOS', deviceOrientation: 'portrait', diff --git a/packages/react-native-editor/__device-tests__/helpers/utils.js b/packages/react-native-editor/__device-tests__/helpers/utils.js index 4f95ceb68d362b..761bfe325d58de 100644 --- a/packages/react-native-editor/__device-tests__/helpers/utils.js +++ b/packages/react-native-editor/__device-tests__/helpers/utils.js @@ -6,6 +6,7 @@ import childProcess from 'child_process'; import wd from 'wd'; import crypto from 'crypto'; import path from 'path'; +import fs from 'fs'; /** * Internal dependencies @@ -36,6 +37,8 @@ const localIOSAppPath = process.env.IOS_APP_PATH || defaultIOSAppPath; const localAppiumPort = serverConfigs.local.port; // Port to spawn appium process for local runs let appiumProcess; +let iOSScreenRecordingProcess; +let androidScreenRecordingProcess; // Used to map unicode and special values to keycodes on Android // Docs for keycode values: https://developer.android.com/reference/android/view/KeyEvent.html @@ -54,6 +57,90 @@ const isLocalEnvironment = () => { return testEnvironment.toLowerCase() === 'local'; }; +const IOS_RECORDINGS_DIR = './ios-screen-recordings'; +const ANDROID_RECORDINGS_DIR = './android-screen-recordings'; + +const getScreenRecordingFileNameBase = ( testPath, id ) => { + const suiteName = path.basename( testPath, '.test.js' ); + return `${ suiteName }.${ id }`; +}; + +jasmine.getEnv().addReporter( { + specStarted: ( { testPath, id } ) => { + const fileName = + getScreenRecordingFileNameBase( testPath, id ) + '.mp4'; + + if ( isAndroid() ) { + if ( ! fs.existsSync( ANDROID_RECORDINGS_DIR ) ) { + fs.mkdirSync( ANDROID_RECORDINGS_DIR ); + } + + androidScreenRecordingProcess = childProcess.spawn( 'adb', [ + 'shell', + 'screenrecord', + '--verbose', + '--bit-rate', + '1M', + '--size', + '720x1280', + `/sdcard/${ fileName }`, + ] ); + + return; + } + + if ( ! fs.existsSync( IOS_RECORDINGS_DIR ) ) { + fs.mkdirSync( IOS_RECORDINGS_DIR ); + } + + iOSScreenRecordingProcess = childProcess.spawn( + 'xcrun', + [ + 'simctl', + 'io', + 'booted', + 'recordVideo', + '--mask=black', + '--force', + fileName, + ], + { + cwd: IOS_RECORDINGS_DIR, + } + ); + }, + specDone: ( { testPath, id, status } ) => { + const fileNameBase = getScreenRecordingFileNameBase( testPath, id ); + + if ( isAndroid() ) { + androidScreenRecordingProcess.kill( 'SIGINT' ); + // wait for kill + childProcess.execSync( 'sleep 20' ); + + childProcess.execSync( + `adb pull /sdcard/${ fileNameBase }.mp4 ${ ANDROID_RECORDINGS_DIR }` + ); + + const oldPath = `${ ANDROID_RECORDINGS_DIR }/${ fileNameBase }.mp4`; + const newPath = `${ ANDROID_RECORDINGS_DIR }/${ fileNameBase }.${ status }.mp4`; + + if ( fs.existsSync( oldPath ) ) { + fs.renameSync( oldPath, newPath ); + } + return; + } + + iOSScreenRecordingProcess.kill( 'SIGINT' ); + + const oldPath = `${ IOS_RECORDINGS_DIR }/${ fileNameBase }.mp4`; + const newPath = `${ IOS_RECORDINGS_DIR }/${ fileNameBase }.${ status }.mp4`; + + if ( fs.existsSync( oldPath ) ) { + fs.renameSync( oldPath, newPath ); + } + }, +} ); + // Initialises the driver and desired capabilities for appium const setupDriver = async () => { const branch = process.env.CIRCLE_BRANCH || ''; @@ -120,8 +207,8 @@ const setupDriver = async () => { // eslint-disable-next-line no-console console.log( status ); - await driver.setImplicitWaitTimeout( 2000 ); - await timer( 3000 ); + await driver.setImplicitWaitTimeout( 5000 ); + await timer( 5000 ); await driver.setOrientation( 'PORTRAIT' ); return driver; 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 99af32e7aa32db..5ba58f56f9dd29 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -1,9 +1,3 @@ -/** - * External dependencies - */ -// eslint-disable-next-line import/no-extraneous-dependencies -import wd from 'wd'; - /** * Internal dependencies */ @@ -16,22 +10,13 @@ import { } from '../helpers/utils'; export default class EditorPage { - driver: wd.PromiseChainWebdriver; - accessibilityIdKey: string; - accessibilityIdXPathAttrib: string; + driver; + accessibilityIdKey; + accessibilityIdXPathAttrib; paragraphBlockName = 'Paragraph'; - listBlockName = 'List'; - headingBlockName = 'Heading'; - imageBlockName = 'Image'; - galleryBlockName = 'Gallery'; - - // This is needed to adapt to changes in the way accessibility ids are being - // assigned after migrating to AndroidX and React Native 0.60. See: - // https://github.com/wordpress-mobile/gutenberg-mobile/pull/1112#issuecomment-501165250 - // for more details. - accessibilityIdSuffix = ''; - - constructor( driver: wd.PromiseChainWebdriver ) { + orderedListButtonName = 'Convert to ordered list'; + + constructor( driver ) { this.driver = driver; this.accessibilityIdKey = 'name'; this.accessibilityIdXPathAttrib = 'name'; @@ -39,7 +24,6 @@ export default class EditorPage { if ( isAndroid() ) { this.accessibilityIdXPathAttrib = 'content-desc'; this.accessibilityIdKey = 'contentDescription'; - this.accessibilityIdSuffix = ', '; } } @@ -51,9 +35,9 @@ export default class EditorPage { // and accessibilityId attributes on this object and selects the block // position uses one based numbering async getBlockAtPosition( - position: number, - blockName: string, - options: { autoscroll: boolean } = { autoscroll: false } + blockName, + position = 1, + options = { autoscroll: false } ) { const blockLocator = `//*[contains(@${ this.accessibilityIdXPathAttrib }, "${ blockName } Block. Row ${ position }")]`; const elements = await this.driver.elementsByXPath( blockLocator ); @@ -92,7 +76,11 @@ export default class EditorPage { // scroll down await swipeUp( this.driver ); } - return this.getBlockAtPosition( position, blockName, options ); + return await this.getBlockAtPosition( + blockName, + position, + options + ); } return lastElementFound; } @@ -109,16 +97,14 @@ export default class EditorPage { return elements[ elements.length - 1 ]; } - async hasBlockAtPosition( position: number, blockName: string = '' ) { + async hasBlockAtPosition( position = 1, blockName = '' ) { return ( undefined !== - ( await this.getBlockAtPosition( position, blockName ) ) + ( await this.getBlockAtPosition( blockName, position ) ) ); } - async getTitleElement( - options: { autoscroll: boolean } = { autoscroll: false } - ) { + async getTitleElement( options = { autoscroll: false } ) { //TODO: Improve the identifier for this element const elements = await this.driver.elementsByXPath( `//*[contains(@${ this.accessibilityIdXPathAttrib }, "Post title.")]` @@ -131,7 +117,7 @@ export default class EditorPage { } async getTextViewForHtmlViewContent() { - const accessibilityId = `html-view-content${ this.accessibilityIdSuffix }`; + const accessibilityId = 'html-view-content'; let blockLocator = `//*[@${ this.accessibilityIdXPathAttrib }="${ accessibilityId }"]`; if ( ! isAndroid() ) { @@ -142,7 +128,7 @@ export default class EditorPage { // Converts to lower case and checks for a match to lowercased html content // Ensure to take additional steps to handle text being changed by auto correct - async verifyHtmlContent( html: string ) { + async verifyHtmlContent( html ) { await toggleHtmlMode( this.driver, true ); const htmlContentView = await this.getTextViewForHtmlViewContent(); @@ -153,7 +139,7 @@ export default class EditorPage { } // set html editor content explicitly - async setHtmlContentAndroid( html: string ) { + async setHtmlContentAndroid( html ) { await toggleHtmlMode( this.driver, true ); const htmlContentView = await this.getTextViewForHtmlViewContent(); @@ -181,7 +167,7 @@ export default class EditorPage { // Block toolbar functions // ========================= - async addNewBlock( blockName: string ) { + async addNewBlock( blockName ) { // Click add button let identifier = 'Add block'; if ( isAndroid() ) { @@ -193,10 +179,55 @@ export default class EditorPage { await addButton.click(); // Click on block of choice + const blockButton = await this.findBlockButton( blockName ); + if ( isAndroid() ) { + await blockButton.click(); + } else { + await this.driver.execute( 'mobile: tap', { + element: blockButton, + x: 10, + y: 10, + } ); + } + } + + // Attempts to find the given block button in the block inserter control. + async findBlockButton( blockName ) { + if ( isAndroid() ) { + // Checks if the Block Button is available, and if not will scroll to the second half of the available buttons. + while ( + ! ( await this.driver.hasElementByAccessibilityId( blockName ) ) + ) { + await this.driver.pressKeycode( 20 ); // Press the Down arrow to force a scroll. + } + + return await this.driver.elementByAccessibilityId( blockName ); + } + const blockButton = await this.driver.elementByAccessibilityId( blockName ); - await blockButton.click(); + const size = await this.driver.getWindowSize(); + const height = size.height - 5; + + while ( ! ( await blockButton.isDisplayed() ) ) { + await this.driver.execute( 'mobile: dragFromToForDuration', { + fromX: 50, + fromY: height, + toX: 50, + toY: height - 450, + duration: 0.5, + } ); + } + + return blockButton; + } + + async clickToolBarButton( buttonName ) { + const toolBarButton = await this.driver.elementByAccessibilityId( + buttonName + ); + await toolBarButton.click(); } // ========================= @@ -204,43 +235,40 @@ export default class EditorPage { // ========================= // position of the block to move up - async moveBlockUpAtPosition( position: number, blockName: string = '' ) { + async moveBlockUpAtPosition( position, blockName = '' ) { if ( ! ( await this.hasBlockAtPosition( position, blockName ) ) ) { throw Error( `No Block at position ${ position }` ); } - const parentId = `${ blockName } Block. Row ${ position }.${ this.accessibilityIdSuffix }`; - const parentLocator = `//*[@${ this.accessibilityIdXPathAttrib }="${ parentId }"]`; - const blockId = `Move block up from row ${ position } to row ${ position - - 1 }${ this.accessibilityIdSuffix }`; + const parentLocator = `//*[@${ this.accessibilityIdXPathAttrib }="${ blockName } Block. Row ${ position }."]`; let blockLocator = `${ parentLocator }/following-sibling::*`; blockLocator += isAndroid() ? '' : '//*'; - blockLocator += `[@${ this.accessibilityIdXPathAttrib }="${ blockId }"]`; + blockLocator += `[@${ + this.accessibilityIdXPathAttrib + }="Move block up from row ${ position } to row ${ position - 1 }"]`; const moveUpButton = await this.driver.elementByXPath( blockLocator ); await moveUpButton.click(); } // position of the block to move down - async moveBlockDownAtPosition( position: number, blockName: string = '' ) { + async moveBlockDownAtPosition( position, blockName = '' ) { if ( ! ( await this.hasBlockAtPosition( position, blockName ) ) ) { throw Error( `No Block at position ${ position }` ); } - const parentId = `${ blockName } Block. Row ${ position }.`; - const parentLocator = `//*[contains(@${ this.accessibilityIdXPathAttrib }, "${ parentId }")]`; - - const blockId = `Move block down from row ${ position } to row ${ position + - 1 }${ this.accessibilityIdSuffix }`; + const parentLocator = `//*[contains(@${ this.accessibilityIdXPathAttrib }, "${ blockName } Block. Row ${ position }.")]`; let blockLocator = `${ parentLocator }/following-sibling::*`; blockLocator += isAndroid() ? '' : '//*'; - blockLocator += `[@${ this.accessibilityIdXPathAttrib }="${ blockId }"]`; + blockLocator += `[@${ + this.accessibilityIdXPathAttrib + }="Move block down from row ${ position } to row ${ position + 1 }"]`; const moveDownButton = await this.driver.elementByXPath( blockLocator ); await moveDownButton.click(); } // position of the block to remove // Block will no longer be present if this succeeds - async removeBlockAtPosition( position: number, blockName: string = '' ) { + async removeBlockAtPosition( blockName = '', position = 1 ) { if ( ! ( await this.hasBlockAtPosition( position, blockName ) ) ) { throw Error( `No Block at position ${ position }` ); } @@ -252,7 +280,7 @@ export default class EditorPage { const removeBlockLocator = `${ buttonElementName }[contains(@${ this.accessibilityIdXPathAttrib }, "${ removeButtonIdentifier }")]`; if ( isAndroid() ) { - const block = await this.getBlockAtPosition( position, blockName ); + const block = await this.getBlockAtPosition( blockName, position ); let checkList = await this.driver.elementsByXPath( removeBlockLocator ); @@ -274,28 +302,7 @@ export default class EditorPage { // Paragraph Block functions // ========================= - async addNewParagraphBlock() { - await this.addNewBlock( this.paragraphBlockName ); - } - - async getParagraphBlockAtPosition( - position: number, - options: { autoscroll: boolean } = { autoscroll: false } - ) { - return this.getBlockAtPosition( - position, - this.paragraphBlockName, - options - ); - } - - async hasParagraphBlockAtPosition( position: number ) { - return this.hasBlockAtPosition( position, this.paragraphBlockName ); - } - - async getTextViewForParagraphBlock( - block: wd.PromiseChainWebdriver.Element - ) { + async getTextViewForParagraphBlock( block ) { let textViewElementName = 'XCUIElementTypeTextView'; if ( isAndroid() ) { textViewElementName = 'android.widget.EditText'; @@ -310,34 +317,36 @@ export default class EditorPage { return await this.driver.elementByXPath( blockLocator ); } - async sendTextToParagraphBlock( - block: wd.PromiseChainWebdriver.Element, - text: string - ) { + async typeTextToParagraphBlock( block, text, clear ) { const textViewElement = await this.getTextViewForParagraphBlock( block ); - await typeString( this.driver, textViewElement, text ); + await typeString( this.driver, textViewElement, text, clear ); await this.driver.sleep( 1000 ); // Give time for the block to rerender (such as for accessibility) } - async sendTextToParagraphBlockAtPosition( position: number, text: string ) { + async sendTextToParagraphBlock( position, text, clear ) { const paragraphs = text.split( '\n' ); for ( let i = 0; i < paragraphs.length; i++ ) { // Select block first - const block = await this.getParagraphBlockAtPosition( + const block = await this.getBlockAtPosition( + this.paragraphBlockName, position + i ); await block.click(); - await this.sendTextToParagraphBlock( block, paragraphs[ i ] ); + await this.typeTextToParagraphBlock( + block, + paragraphs[ i ], + clear + ); if ( i !== paragraphs.length - 1 ) { - await this.sendTextToParagraphBlock( block, '\n' ); + await this.typeTextToParagraphBlock( block, '\n', false ); } } } - async getTextForParagraphBlock( block: wd.PromiseChainWebdriver.Element ) { + async getTextForParagraphBlock( block ) { const textViewElement = await this.getTextViewForParagraphBlock( block ); @@ -345,16 +354,18 @@ export default class EditorPage { return text.toString(); } - async removeParagraphBlockAtPosition( position: number ) { - await this.removeBlockAtPosition( position, this.paragraphBlockName ); - } - - async getTextForParagraphBlockAtPosition( position: number ) { + async getTextForParagraphBlockAtPosition( position ) { // Select block first - let block = await this.getParagraphBlockAtPosition( position ); + let block = await this.getBlockAtPosition( + this.paragraphBlockName, + position + ); await block.click(); - block = await this.getParagraphBlockAtPosition( position ); + block = await this.getBlockAtPosition( + this.paragraphBlockName, + position + ); const text = await this.getTextForParagraphBlock( block ); return text.toString(); } @@ -363,19 +374,7 @@ export default class EditorPage { // List Block functions // ========================= - async addNewListBlock() { - await this.addNewBlock( this.listBlockName ); - } - - async getListBlockAtPosition( position: number ) { - return this.getBlockAtPosition( position, this.listBlockName ); - } - - async hasListBlockAtPosition( position: number ) { - return await this.hasBlockAtPosition( position, this.listBlockName ); - } - - async getTextViewForListBlock( block: wd.PromiseChainWebdriver.Element ) { + async getTextViewForListBlock( block ) { let textViewElementName = 'XCUIElementTypeTextView'; if ( isAndroid() ) { textViewElementName = 'android.widget.EditText'; @@ -390,43 +389,24 @@ export default class EditorPage { return await this.driver.elementByXPath( blockLocator ); } - async sendTextToListBlock( - block: wd.PromiseChainWebdriver.Element, - text: string - ) { + async sendTextToListBlock( block, text ) { const textViewElement = await this.getTextViewForListBlock( block ); - return await typeString( this.driver, textViewElement, text ); - } - async getTextForListBlock( block: wd.PromiseChainWebdriver.Element ) { - const textViewElement = await this.getTextViewForListBlock( block ); - const text = await textViewElement.text(); - return text.toString(); - } + // Cannot clear list blocks because it messes up the list bullet + const clear = false; - async removeListBlockAtPosition( position: number ) { - return await this.removeBlockAtPosition( position, this.listBlockName ); + return await typeString( this.driver, textViewElement, text, clear ); } - async getTextForListBlockAtPosition( position: number ) { - const block = await this.getListBlockAtPosition( position ); - const text = await this.getTextForListBlock( block ); - return text.toString(); + async clickOrderedListToolBarButton() { + await this.clickToolBarButton( this.orderedListButtonName ); } // ========================= // Image Block functions // ========================= - async addNewImageBlock() { - await this.addNewBlock( this.imageBlockName ); - } - - async getImageBlockAtPosition( position: number ) { - return this.getBlockAtPosition( position, this.imageBlockName ); - } - - async selectEmptyImageBlock( block: wd.PromiseChainWebdriver.Element ) { + async selectEmptyImageBlock( block ) { const accessibilityId = await block.getAttribute( this.accessibilityIdKey ); @@ -444,56 +424,20 @@ export default class EditorPage { await mediaLibraryButton.click(); } - async enterCaptionToSelectedImageBlock( caption: string ) { + async enterCaptionToSelectedImageBlock( caption, clear = true ) { const imageBlockCaptionField = await this.driver.elementByXPath( - '//XCUIElementTypeButton[@name="Image caption. Empty"]' + '//XCUIElementTypeButton[starts-with(@name, "Image caption.")]' ); await imageBlockCaptionField.click(); - await typeString( this.driver, imageBlockCaptionField, caption ); - } - - async removeImageBlockAtPosition( position: number ) { - return await this.removeBlockAtPosition( - position, - this.imageBlockName - ); - } - - // ========================= - // Gallery Block functions - // ========================= - - async addNewGalleryBlock() { - await this.addNewBlock( this.galleryBlockName ); - } - - async getGalleryBlockAtPosition( position: number ) { - return this.getBlockAtPosition( position, this.galleryBlockName ); - } - - async removeGalleryBlockAtPosition( position: number ) { - return await this.removeBlockAtPosition( - position, - this.galleryBlockName - ); + await typeString( this.driver, imageBlockCaptionField, caption, clear ); } // ========================= // Heading Block functions // ========================= - async addNewHeadingBlock() { - await this.addNewBlock( this.headingBlockName ); - } - - async getHeadingBlockAtPosition( position: number ) { - return this.getBlockAtPosition( position, this.headingBlockName ); - } // Inner element changes on iOS if Heading Block is empty - async getTextViewForHeadingBlock( - block: wd.PromiseChainWebdriver.Element, - empty: boolean - ) { + async getTextViewForHeadingBlock( block, empty ) { let textViewElementName = empty ? 'XCUIElementTypeStaticText' : 'XCUIElementTypeTextView'; @@ -508,23 +452,11 @@ export default class EditorPage { return await this.driver.elementByXPath( blockLocator ); } - async sendTextToHeadingBlock( - block: wd.PromiseChainWebdriver.Element, - text: string - ) { + async sendTextToHeadingBlock( block, text, clear = true ) { const textViewElement = await this.getTextViewForHeadingBlock( block, true ); - return await typeString( this.driver, textViewElement, text ); - } - - async getTextForHeadingBlock( block: wd.PromiseChainWebdriver.Element ) { - const textViewElement = await this.getTextViewForHeadingBlock( - block, - false - ); - const text = await textViewElement.text(); - return text.toString(); + return await typeString( this.driver, textViewElement, text, clear ); } } diff --git a/packages/react-native-editor/bin/sauce-pre-upload.sh b/packages/react-native-editor/bin/sauce-pre-upload.sh index 234f013081f838..64dba88ec7e3f7 100755 --- a/packages/react-native-editor/bin/sauce-pre-upload.sh +++ b/packages/react-native-editor/bin/sauce-pre-upload.sh @@ -1,9 +1,9 @@ #!/bin/sh -# check if CIRCLE_BRANCH variable is set -if [[ -n "$CIRCLE_BRANCH" ]] ; then +# check if TRAVIS_PULL_REQUEST_BRANCH variable is set +if [[ -n "$TRAVIS_PULL_REQUEST_BRANCH" ]] ; then # replace / with - and assign to new var SAUCE_FILENAME - export SAUCE_FILENAME=${CIRCLE_BRANCH//[\/]/-}; + export SAUCE_FILENAME=${TRAVIS_PULL_REQUEST_BRANCH//[\/]/-}; else - echo "Expected CIRCLE_BRANCH env variable"; + echo "Expected TRAVIS_PULL_REQUEST_BRANCH env variable"; fi diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index 84153743b8ad23..5100a50f6886e7 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -244,7 +244,7 @@ PODS: - RNTAztecView (0.1.11): - React-Core - WordPress-Aztec-iOS (~> 1.19.0) - - WordPress-Aztec-iOS (1.19.0) + - WordPress-Aztec-iOS (1.19.1) - Yoga (1.14.0) DEPENDENCIES: @@ -400,7 +400,7 @@ SPEC CHECKSUMS: ReactNativeDarkMode: f61376360c5d983907e5c316e8e1c853a8c2f348 RNSVG: 68a534a5db06dcbdaebfd5079349191598caef7b RNTAztecView: b8caab8aea3cc3628d751f8025555642e1b7ea9e - WordPress-Aztec-iOS: fb6ea6409a5228292568f665eb22ea0a0aa7ad7e + WordPress-Aztec-iOS: 25a9cbe204a22dd6d540d66d90b8a889421e0b42 Yoga: f2a7cd4280bfe2cca5a7aed98ba0eb3d1310f18b PODFILE CHECKSUM: aac0fd05ad076bb61d290e7c36d06c9cb30154c8 diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index 90e5c4ea572460..0c44e12766547b 100644 --- a/packages/react-native-editor/package.json +++ b/packages/react-native-editor/package.json @@ -110,12 +110,12 @@ "test:e2e:ios": "TEST_RN_PLATFORM=ios npm run device-tests", "test:e2e:android:local": "npm run test:e2e:build-app:android && npm run test:e2e:install-app:android && TEST_RN_PLATFORM=android npm run device-tests:local", "test:e2e:android:local:debug": "npm run test:e2e:build-app:android && npm run test:e2e:install-app:android && npm run test:e2e:android:debug", - "test:e2e:ios:local": "npm run test:e2e:build-app:ios && TEST_RN_PLATFORM=ios npm run device-tests:local", + "test:e2e:ios:local": "npm run test:e2e:bundle:ios && npm run test:e2e:build-app:ios && TEST_RN_PLATFORM=ios npm run device-tests:local", "test:e2e:bundle:android": "mkdir -p android/app/src/main/assets && react-native bundle --reset-cache --platform android --dev false --minify false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res", "test:e2e:build-app:android": "npm run test:e2e:bundle:android && cd android && ./gradlew clean && ./gradlew assembleDebug", "test:e2e:install-app:android": "cd android && ./gradlew installDebug", - "test:e2e:bundle:ios": "react-native bundle --reset-cache --platform=ios --dev=false --minify false --entry-file=index.js --bundle-output=./ios/build/gutenberg/Build/Products/Release-iphonesimulator/GutenbergDemo.app/main.jsbundle --assets-dest=./ios/build/gutenberg/Build/Products/Release-iphonesimulator/GutenbergDemo.app", - "test:e2e:build-app:ios": "npm run ios --configuration Release --no-packager", + "test:e2e:bundle:ios": "mkdir -p ios/build/gutenberg/Build/Products/Release-iphonesimulator/GutenbergDemo.app && react-native bundle --reset-cache --platform=ios --dev=false --minify false --entry-file=index.js --bundle-output=./ios/build/gutenberg/Build/Products/Release-iphonesimulator/GutenbergDemo.app/main.jsbundle --assets-dest=./ios/build/gutenberg/Build/Products/Release-iphonesimulator/GutenbergDemo.app", + "test:e2e:build-app:ios": "npm run ios -- --configuration Release --no-packager --simulator 'iPhone 11 (13.4)'", "build:gutenberg": "cd gutenberg && npm ci && npm run build", "clean": "npm run clean:build-artifacts; npm run clean:aztec; npm run cache clean; npm run clean:haste; npm run clean:jest; npm run clean:metro; npm run clean:react; npm run clean:watchman; npm run clean:node; npm run clean:pot", "clean:runtime": "npm run clean:haste; npm run clean:react; npm run clean:metro; npm run clean:jest; npm run clean:watchman; npm run clean:babel-cache",