diff --git a/package.json b/package.json index 6c6db98aaf4f90..e981c167b12b49 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,8 @@ "!template/package-lock.json", "!template/yarn.lock", "third-party-podspecs", - "types" + "types", + "!packages/rn-tester-e2e" ], "scripts": { "start": "react-native start", diff --git a/packages/rn-tester-e2e/README-addTestAndExecute.md b/packages/rn-tester-e2e/README-addTestAndExecute.md new file mode 100644 index 00000000000000..aac2ef3afed6a5 --- /dev/null +++ b/packages/rn-tester-e2e/README-addTestAndExecute.md @@ -0,0 +1,32 @@ +# How to add a test + +1. Create a feature file `(rn-tester-e2e/test/features)` - GivenWhenThen Gherkin syntax +2. **OPTIONAL -** Create a screen object or extend the existing one (depends on the test scope) - `rn-tester-e2e/test/screenObjects` - map screen elements for iOS and Android +3. **OPTIONAL -** Add another common step in `rn-tester-e2e/test/common_steps/common.steps.js` +4. Create a runner file `(rn-tester-e2e/test/runners)` - import steps and screen objects from point 2 and 3. Create test scenarios +5. Update `(rn-tester-e2e/e2e-config.js)` with proper capabilities of your emulator + +# How to execute a test +1. Open new Terminal -> navigate to the react-native path -> open Metro by typing +>yarn start + +or + +>npm start + + +2. Open second terminal -> navigate to the react-native/packages/rn-tester-e2e -> MAKE SURE YOUR APPIUM HAS UIAUTOMATOR2 AND XCUITEST INSTALLED! type +>npm install appium@2.0.0-beta.40 -g + +>appium driver install uiautomator2 + +>appium driver install xcuitest + +>appium --base-path /wd/hub + +3. Open third terminal -> navigate to the react-native/packages/rn-tester-e2e -> run all tests by typing +>npm run ios + +or + +>npm run android \ No newline at end of file diff --git a/packages/rn-tester-e2e/README-describedAllFoldersAndFiles.md b/packages/rn-tester-e2e/README-describedAllFoldersAndFiles.md new file mode 100644 index 00000000000000..d09ce554ae8266 --- /dev/null +++ b/packages/rn-tester-e2e/README-describedAllFoldersAndFiles.md @@ -0,0 +1,30 @@ +# Description of all folders and files in rn-tester-e2e + +# folders 🗂 +## common_steps 🪜 +Common steps reusable between different features files + +## features 🥒 +Cucumber feature files. GivenWhenThen Gherkin syntax. One feature per screen/functionality + +## helpers 🧑🏻‍🚒 +Utils file with generic, simple actions and methods + +## runners 🏃🏽‍♀️ +Runner file which combines feature file and steps file. Runner file imports steps file and declares step functions in the same order as in the feature file + +## screenObjects 📱 +Screen object files based on Page Object Pattern. One file defines all necessary elements to interact with. These elements are defined as screen class variables, they are used by the steps file + +# root files 📄 +## e2e-config.js +Android and iOS emulator/physical device configuration, process.env.E2E_device global variable is defined there - it can be used across the whole rn-tester-e2e directory + +## jest.config.js +Global jest config setup - such as timeout, test runner path + +## jest.setup.js +Jest and wdio setup file + +## package.json +all external dependencies and project parameters \ No newline at end of file diff --git a/packages/rn-tester-e2e/README.md b/packages/rn-tester-e2e/README.md new file mode 100644 index 00000000000000..49b51a3d2fcdfd --- /dev/null +++ b/packages/rn-tester-e2e/README.md @@ -0,0 +1,80 @@ +# Building the app +Bulding manually *.app* and *.apk* is required to run automation tests on local environment. + +Before building app, make sure you ran: + +```bash +cd react-native +yarn install +``` +## Building for iOS + +If you prevoiusly built RNTester, you may need to clean up build files and Pods: +```bash +rm -rf node_modules && yarn +cd packages/rn-tester +yarn clean-ios +``` + +Build the app for not M1 mac user: + +1. Install Bundler `gem install bundler`. We use bundler to install the right version of CocoaPods locally. +2. Install Bundler and CocoaPods dependencies: `bundle install && bundle exec pod install` or `yarn setup-ios-hermes`. In order to use JSC instead of Hermes engine, run: `USE_HERMES=0 bundle exec pod install` or `yarn setup-ios-jsc` instead. +3. Open the generated `RNTesterPods.xcworkspace`. +4. Build the app. + +If you are M1 mac user: +1. Install ffi package `gem install ffi -v '1.15.5' --source 'https://rubygems.org/'` +2. Install pods with new architecture, e.g. using JSC `USE_HERMES=0 arch -x86_64 pod install` +3. Open the generated `RNTesterPods.xcworkspace`. +4. Build the app. + +Find the **RNTester.app** in `~/Library/Developer/Xcode/DerivedData/RNTesterPods-{id}/Build/Products/Debug-iphonesimulator` and move the app to the following directory `/react-native/packages/rn-tester-e2e/apps` + + +## Building for Android +1. You'll need to have all the [prerequisites](https://reactnative.dev/contributing/how-to-build-from-source#prerequisites) (SDK, NDK) for Building React Native installed. + +2. Start an Android emulator. +3. Build the app +```sh +cd packages/rn-tester +# In order to use Hermes engine, run `yarn install-android-hermes` instead. +yarn install-android-jsc +yarn start +``` + +_Note: Building for the first time can take a while._ + +Find the **RNTester.app** in `~/AndroidStudioProjects/{ProjectName}/app/build/intermediates/apk/debug/` and move the app to the following directory `/react-native/packages/rn-tester-e2e/apps` + + +## Usage + +In /react-native/packages/rn-tester-e2e + +```bash +npm install +``` + +Go to +```bash +/react-native/packages/rn-tester-e2e/e2e-config.js +``` +Prepare capabilities for your simulators + +The next step is to run the appium + +```bash +appium +``` + +After running appium open your simulator and + +```bash +npm run android - for android +npm run ios - for ios +``` + + + diff --git a/packages/rn-tester-e2e/apps/put-app-here.txt b/packages/rn-tester-e2e/apps/put-app-here.txt new file mode 100644 index 00000000000000..1378eef1a954f9 --- /dev/null +++ b/packages/rn-tester-e2e/apps/put-app-here.txt @@ -0,0 +1 @@ +put app in this folder \ No newline at end of file diff --git a/packages/rn-tester-e2e/babel.config.js b/packages/rn-tester-e2e/babel.config.js new file mode 100644 index 00000000000000..ddb7bea4205c27 --- /dev/null +++ b/packages/rn-tester-e2e/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [['@babel/preset-env', {targets: {node: 'current'}}]], + }; diff --git a/packages/rn-tester-e2e/e2e-config.js b/packages/rn-tester-e2e/e2e-config.js new file mode 100644 index 00000000000000..2db7d7d8b02684 --- /dev/null +++ b/packages/rn-tester-e2e/e2e-config.js @@ -0,0 +1,40 @@ +// utility file to extract the config for E2E testing at runtime +// for appium +const path = require('path'); +let capabilities; + +const android = { + 'platformName': 'Android', + 'appium:platformVersion': '', + 'appium:deviceName': '', + 'appium:app': path.join(process.cwd(), '/apps/RNTester.apk'), + 'appium:automationName': 'uiautomator2', + 'appium:newCommandTimeout': 240, +}; + +const ios = { + 'platformName': 'iOS', + 'appium:platformVersion': '', + 'appium:deviceName': '', + //bundleId: 'org.reactjs.native.example.TestForE2E', + 'appium:automationName': 'XCUITest', + 'appium:app': path.join(process.cwd(), '/apps/RNTester.app'), +}; + +if (!process.env.E2E_DEVICE) { + throw new Error('E2E_DEVICE environment variable is not defined'); +} + +if (!(process.env.E2E_DEVICE.includes('android') || process.env.E2E_DEVICE.includes('ios'))) { + throw new Error('No e2e device configuration found'); +} + +if (process.env.E2E_DEVICE === 'android') { + capabilities = android; +} + +if (process.env.E2E_DEVICE === 'ios') { + capabilities = ios; +} + +export default capabilities; diff --git a/packages/rn-tester-e2e/jest.config.js b/packages/rn-tester-e2e/jest.config.js new file mode 100644 index 00000000000000..9eac9bad37187d --- /dev/null +++ b/packages/rn-tester-e2e/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testTimeout: 60000, + bail: 0, + setupFilesAfterEnv: ['./jest.setup.js'], + testMatch: [ + '**/runners/*.js', + ], +}; diff --git a/packages/rn-tester-e2e/jest.setup.js b/packages/rn-tester-e2e/jest.setup.js new file mode 100644 index 00000000000000..ca283417ff3f10 --- /dev/null +++ b/packages/rn-tester-e2e/jest.setup.js @@ -0,0 +1,30 @@ +const wdio = require('webdriverio'); +import capabilities from './e2e-config.js'; +import { beforeEach, afterEach, jest } from '@jest/globals'; + + +jest.setTimeout(40000); + +let driver; +const config = { + path: '/wd/hub', + host: 'localhost', + port: 4723, + waitforTimeout: 30000, + logLevel: 'silent', + capabilities: { + ...capabilities, + }, +}; + +beforeEach(async () => { + driver = await wdio.remote(config); +}); + +afterEach(async () => { + console.info('[afterAll] Done with testing!'); + await driver.deleteSession(); +}); + +export { driver }; + diff --git a/packages/rn-tester-e2e/package.json b/packages/rn-tester-e2e/package.json new file mode 100644 index 00000000000000..c5584fd96f11d8 --- /dev/null +++ b/packages/rn-tester-e2e/package.json @@ -0,0 +1,25 @@ + +{ + "name": "rn-tester-e2e", + "version": "0.1.0", + "private": true, + "devDependencies": { + "appium": "^2.0.0-beta.40", + "appium-uiautomator2-driver": "^2.9.0", + "appium-xcuitest-driver": "^4.12.2", + "eslint": "^8.19.0", + "jest": "^29.2.1", + "webdriverio": "^7.25.4" + "jest-cucumber": "^3.0.1", + }, + "scripts": { + "test": "jest --runInBand", + "android": "E2E_DEVICE=\"android\" npm run test", + "ios": "E2E_DEVICE=\"ios\" npm run test" + }, + "dependencies": { + "@babel/preset-env": "^7.20.0", + "@types/jest": "^29.2.1" + } +} + diff --git a/packages/rn-tester-e2e/test/common_steps/common.steps.js b/packages/rn-tester-e2e/test/common_steps/common.steps.js new file mode 100644 index 00000000000000..38280d1ccd072b --- /dev/null +++ b/packages/rn-tester-e2e/test/common_steps/common.steps.js @@ -0,0 +1,14 @@ +import { driver } from '../../jest.setup.js'; + +// Common steps reusable between different features +export const userIsOnMainScreen = (given) => { + given('User is on the main screen', async () => { + await driver.pause(2000); + }); +}; + +export const clickOkButton = (given) => { + given('User clicks on the OK button', async () => { + await driver.pause(2000); + }); +}; diff --git a/packages/rn-tester-e2e/test/features/buttonComponentScreen.feature b/packages/rn-tester-e2e/test/features/buttonComponentScreen.feature new file mode 100644 index 00000000000000..66993077062274 --- /dev/null +++ b/packages/rn-tester-e2e/test/features/buttonComponentScreen.feature @@ -0,0 +1,19 @@ +Feature: Button component screen + + Scenario: Cancel Button + Given User is on the main screen + Then Verify that the Button component is displayed + When User clicks on the Button component + Then Verify that the "Button" header is displayed + When User clicks on the Cancel Application button + Then Verify that the cancel alert box has text: "Your application has been cancelled!" + And User clicks on the OK button + + Scenario: Submit Button + Given User is on the main screen + Then Verify that the Button component is displayed + When User clicks on the Button component + Then Verify that the "Button" header is displayed + When User clicks on the Submit Application button + Then Verify that the submit alert box has text: "Your application has been submitted!" + And User clicks on the OK button \ No newline at end of file diff --git a/packages/rn-tester-e2e/test/helpers/utils.js b/packages/rn-tester-e2e/test/helpers/utils.js new file mode 100644 index 00000000000000..54569db6dfaf29 --- /dev/null +++ b/packages/rn-tester-e2e/test/helpers/utils.js @@ -0,0 +1,26 @@ + +import { driver } from '../../jest.setup'; + +class Utils { + + async checkElementExistence(locator) { + await driver.$(locator).waitForDisplayed(); + return driver.$(locator).isDisplayed(); + } + + async clickElement(locator) { + await driver.$(locator).waitForDisplayed(); + await driver.$(locator).click(); + } + + async getElementText(locator) { + await driver.$(locator).waitForDisplayed(); + return driver.$(locator).getText(); + } + + + platformSelect(platforms) { + return platforms[process.env.E2E_DEVICE]; + } +} +module.exports = new Utils(); diff --git a/packages/rn-tester-e2e/test/runners/buttonComponentScreenRunner.js b/packages/rn-tester-e2e/test/runners/buttonComponentScreenRunner.js new file mode 100644 index 00000000000000..3da267c415a57c --- /dev/null +++ b/packages/rn-tester-e2e/test/runners/buttonComponentScreenRunner.js @@ -0,0 +1,86 @@ +import { defineFeature, loadFeature } from 'jest-cucumber'; +import { userIsOnMainScreen, clickOkButton } from '../common_steps/common.steps'; +import componentsScreen from '../screenObjects/components.screen.js'; +import buttonComponentScreen from '../screenObjects/buttonComponent.screen.js'; + +// Object.entries(steps).forEach(([name, exported]) => window[name] = exported); +const feature = loadFeature('test/features/buttonComponentScreen.feature'); + +//Methods used more than once in the feature +const buttonComponentShouldBeDisplayed = (then) => { + then('Verify that the Button component is displayed', async () => { + expect(await componentsScreen.checkButtonComponentIsDisplayed()).toBeTruthy(); + }); +}; + +const clickOnButtonComponent = (when) => { + when('User clicks on the Button component', async () => { + await componentsScreen.clickButtonComponent(); + }); +}; + +const buttonHeaderShouldBeDisplayed = (then) => { + then(/^Verify that the "(.*)" header is displayed$/, async (headerScreenName) => { + switch (headerScreenName) { + case 'Button': + expect(await buttonComponentScreen.checkButtonsScreenIsDisplayed()).toBeTruthy(); + break; + // here you can add more similar assertions + default: throw new Error(`Wrong parameter provided. There is no such case as: ${headerScreenName}`); + } + }); +}; + +const alertBoxShouldHaveText = (then) => { + then(/^Verify that the cancel|submit alert box has text: "(.*)"$/, async (alertBoxType, alertBoxText) => { + switch (alertBoxType) { + case 'cancel': + expect(await buttonComponentScreen.getCancelAlertText()).toContain(alertBoxText); + break; + case 'submit': + expect(await buttonComponentScreen.getSubmitAlertText()).toContain(alertBoxText); + break; + } + }); +}; + +defineFeature(feature, (test) => { + test('Cancel Button', ({ given, when, then }) => { + + userIsOnMainScreen(given); + + buttonComponentShouldBeDisplayed(then); + + clickOnButtonComponent(when); + + buttonHeaderShouldBeDisplayed(then); + + //method which is used only once in whole feature + when(/^User clicks on the Cancel Application button$/, async () => { + await buttonComponentScreen.clickCancelApplication(); + }); + + alertBoxShouldHaveText(then); + + clickOkButton(when); + }); + + test('Submit Button', ({ given, when, then }) => { + + userIsOnMainScreen(given); + + buttonComponentShouldBeDisplayed(then); + + clickOnButtonComponent(when); + + buttonHeaderShouldBeDisplayed(then); + + when(/^User clicks on the Submit Application button$/, async () => { + await buttonComponentScreen.clickCancelApplication(); + }); + + alertBoxShouldHaveText(then); + + clickOkButton(when); + }); +}); diff --git a/packages/rn-tester-e2e/test/screenObjects/apis.screen.js b/packages/rn-tester-e2e/test/screenObjects/apis.screen.js new file mode 100644 index 00000000000000..5aab200069a9b0 --- /dev/null +++ b/packages/rn-tester-e2e/test/screenObjects/apis.screen.js @@ -0,0 +1,2 @@ +class ApisScreen {} +module.exports = new ApisScreen(); diff --git a/packages/rn-tester-e2e/test/screenObjects/bookmarks.screen.js b/packages/rn-tester-e2e/test/screenObjects/bookmarks.screen.js new file mode 100644 index 00000000000000..8ceee8296aef36 --- /dev/null +++ b/packages/rn-tester-e2e/test/screenObjects/bookmarks.screen.js @@ -0,0 +1,2 @@ +class BookmarksScreen {} +module.exports = new BookmarksScreen(); diff --git a/packages/rn-tester-e2e/test/screenObjects/buttonComponent.screen.js b/packages/rn-tester-e2e/test/screenObjects/buttonComponent.screen.js new file mode 100644 index 00000000000000..383e5186b988c7 --- /dev/null +++ b/packages/rn-tester-e2e/test/screenObjects/buttonComponent.screen.js @@ -0,0 +1,67 @@ +import Utils from '../helpers/utils'; + +class ButtonComponentScreen { + + buttonScreenElement = Utils.platformSelect({ + ios: '[label="Button"]', + android: '//android.view.ViewGroup/android.widget.TextView[@text="Button"]', + }); + + btnSubmitElement = Utils.platformSelect({ + ios: '[label="Press to submit your application!"]', + android: '//android.widget.Button[@resource-id="button_default_styling"]', + }); + + inputSearchElement = Utils.platformSelect({ + ios: '[name="example_search"]', + android: '//android.widget.EditText[@resource-id="example_search"]', + }); + + btnOKElement = Utils.platformSelect({ + ios: '[label="OK"]', + android: '//android.widget.Button[@text="OK"]', + }); + + btnCancelElement = Utils.platformSelect({ + ios: '[label="Press to cancel your application!"]', + android: '//android.widget.Button[@resource-id="cancel_button"]', + }); + + submitAlertBoxElement = Utils.platformSelect({ + ios: '[label="Your application has been submitted!"]', + android: '//android.widget.TextView[@resource-id="android:id/alertTitle"]', + }); + + cancelAlertBoxElement = Utils.platformSelect({ + ios: '[label="Your application has been cancelled!"]', + android: '//android.widget.TextView[@resource-id="android:id/alertTitle"]', + }); + + + + async checkButtonsScreenIsDisplayed() { + return await Utils.checkElementExistence(this.buttonScreenElement); + } + + async clickSubmitApplication() { + await Utils.clickElement(this.btnSubmitElement); + } + + async clickCancelApplication() { + await Utils.clickElement(this.btnCancelElement); + } + + async getCancelAlertText() { + return await Utils.getElementText(this.cancelAlertBoxElement); + } + + async getSubmitAlertText() { + return await Utils.getElementText(this.submitAlertBoxElement); + } + + async clickOkButton() { + await Utils.clickElement(this.btnOKElement); + } + +} +module.exports = new ButtonComponentScreen(); diff --git a/packages/rn-tester-e2e/test/screenObjects/components.screen.js b/packages/rn-tester-e2e/test/screenObjects/components.screen.js new file mode 100644 index 00000000000000..758a9b3f2abd1d --- /dev/null +++ b/packages/rn-tester-e2e/test/screenObjects/components.screen.js @@ -0,0 +1,20 @@ +import Utils from '../helpers/utils'; + +class ComponentsScreen { + + buttonComponentElement = Utils.platformSelect({ + ios: '[label="Button Simple React Native button component."]', + android: '~Button Simple React Native button component.', + }); + + + async checkButtonComponentIsDisplayed() { + return await Utils.checkElementExistence(this.buttonComponentElement); + } + + async clickButtonComponent() { + await Utils.clickElement(this.buttonComponentElement); + } +} +module.exports = new ComponentsScreen(); +