diff --git a/.gitignore b/.gitignore index 678fbe434..3ab758b97 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,9 @@ fastlane/test_output fastlane/README.md test-ci-secrets.json + +# appium +appium/node_modules +appium/package-lock.json +appium/tmp +appium/.env diff --git a/appium/README.md b/appium/README.md new file mode 100644 index 000000000..799cb9abd --- /dev/null +++ b/appium/README.md @@ -0,0 +1,16 @@ +# appium project + +Project to run Appium tests together with WebdriverIO for: + +- iOS/Android Native Apps + +## Before running tests, please create a `./apps` directory, download the app and move the app file into that directory + +##Create .env file in appium root and set next variables in that file: +ACCOUNT_EMAIL= +ACCOUNT_PASSWORD= +PASS_PHRASE= + +## Install dependencies with `npm i` + +## Run smoke tests for iOS with `npm run ios.smoke`, all tests with tag #smoke will be included diff --git a/appium/babel.config.js b/appium/babel.config.js new file mode 100644 index 000000000..b1ece97bd --- /dev/null +++ b/appium/babel.config.js @@ -0,0 +1,9 @@ +module.exports = { + presets: [ + [ '@babel/preset-env', { + targets: { + node: 'current', + }, + } ], + ] +}; diff --git a/appium/config/wdio.ios.app.conf.js b/appium/config/wdio.ios.app.conf.js new file mode 100644 index 000000000..ee0a8c1a4 --- /dev/null +++ b/appium/config/wdio.ios.app.conf.js @@ -0,0 +1,33 @@ +const { join } = require('path'); +const { config } = require('./wdio.shared.conf'); +const pathWdioConfig = require('path'); +require('dotenv').config({ path: pathWdioConfig.resolve(__dirname, '../.env') }); + +config.suites = { + all: [ + './tests/specs/**/*.spec.ts' + ], + smoke: [ + './tests/specs/login/GmailLogin.spec.ts', + './tests/specs/inbox/ReadTextEmail.spec.ts' + ] +}; + +config.capabilities = [ + { + platformName: 'iOS', + iosInstallPause: 5000, + deviceName: 'iPhone 11 Pro Max', + platformVersion: '14.5', + automationName: 'XCUITest', + app: join(process.cwd(), './apps/FlowCrypt.app'), + newCommandTimeout: 10000, + wdaLaunchTimeout: 300000, + wdaConnectionTimeout: 600000, + wdaStartupRetries: 4, + wdaStartupRetryInterval: 120000, + resetOnSessionStartOnly: true + }, +]; + +exports.config = config; diff --git a/appium/config/wdio.shared.conf.js b/appium/config/wdio.shared.conf.js new file mode 100644 index 000000000..a7b0730b3 --- /dev/null +++ b/appium/config/wdio.shared.conf.js @@ -0,0 +1,45 @@ +const { join } = require('path'); + +exports.config = { + + runner: 'local', + framework: 'jasmine', + jasmineNodeOpts: { + defaultTimeoutInterval: 300000, + requires: ['ts-node/register', 'tsconfig-paths/register'] + }, + sync: true, + logLevel: 'silent', + deprecationWarnings: true, + bail: 0, + waitforTimeout: 15000, + connectionRetryTimeout: 90000, + connectionRetryCount: 3, + maxInstancesPerCapability: 1, + reporters: ['spec', + ['allure', { + outputDir: './tmp', + disableWebdriverStepsReporting: true, + disableWebdriverScreenshotsReporting: false, + }] + ], + services: [ + ['appium', { + command : 'appium', + logPath : join(process.cwd(), './tmp') + }] + ], + port: 4723, + path: '/wd/hub', + specFileRetries: 1, + specFileRetriesDeferred: false, + + afterTest: function (test, context, { error, result, duration, passed, retries }) { + if (error) { + const timestampNow = new Date().getTime().toString(); + const path = join(process.cwd(), './tmp'); + driver.saveScreenshot(`${path}/${timestampNow}.png`); + console.log("Screenshot of failed test was saved to " + path) + } + } +}; diff --git a/appium/package.json b/appium/package.json new file mode 100644 index 000000000..1e4bf1400 --- /dev/null +++ b/appium/package.json @@ -0,0 +1,37 @@ +{ + "name": "flowcrypt-appium", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "ios.smoke": "./node_modules/.bin/wdio ./config/wdio.ios.app.conf.js --suite smoke" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@babel/cli": "^7.10.4", + "@babel/core": "^7.10.4", + "@babel/preset-env": "^7.10.4", + "@babel/register": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4", + "@wdio/allure-reporter": "6.10.6", + "@wdio/appium-service": "6.10.11", + "@wdio/cli": "6.10.11", + "@wdio/jasmine-framework": "6.10.11", + "@wdio/local-runner": "6.10.13", + "@wdio/spec-reporter": "6.10.6", + "@wdio/sync": "^6.1.14", + "babel-eslint": "^10.1.0", + "dotenv": "^10.0.0", + "eslint-config-standard": "^16.0.1", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-wdio": "^6.6.0", + "node-fetch": "^2.6.1", + "ts-node": "^9.1.1", + "typescript": "^4.1.3", + "webdriverio": "6.10.11" + } +} diff --git a/appium/tests/constants.ts b/appium/tests/constants.ts new file mode 100644 index 000000000..0a622d78c --- /dev/null +++ b/appium/tests/constants.ts @@ -0,0 +1,2 @@ +export const DEFAULT_TIMEOUT = 15000; + diff --git a/appium/tests/data/index.ts b/appium/tests/data/index.ts new file mode 100644 index 000000000..1f4396609 --- /dev/null +++ b/appium/tests/data/index.ts @@ -0,0 +1,10 @@ +export const CommonData = { + account: { + email: process.env.ACCOUNT_EMAIL, + password: process.env.ACCOUNT_PASSWORD, + passPhrase: process.env.PASS_PHRASE + }, + sender: { + email: 'dmitry@flowcrypt.com' + } +}; diff --git a/appium/tests/helpers/ElementHelper.ts b/appium/tests/helpers/ElementHelper.ts new file mode 100644 index 000000000..405e4b478 --- /dev/null +++ b/appium/tests/helpers/ElementHelper.ts @@ -0,0 +1,49 @@ +import {DEFAULT_TIMEOUT} from "../constants"; + +class ElementHelper { + + /** + * returns true or false for element visibility + */ + static elementDisplayed(element): boolean { + return element.isExisting(); + } + + static waitElementVisible(element, timeout: number = DEFAULT_TIMEOUT) { + try { + element.waitForDisplayed({timeout: timeout}); + } catch (error) { + throw new Error(`Element isn't visible after ${timeout} seconds. Error: ${error}`); + } + } + + static waitElementInvisible(element, timeout: number = DEFAULT_TIMEOUT) { + try { + element.waitForDisplayed({timeout: timeout, reverse: true}); + } catch (error) { + throw new Error(`Element still visible after ${timeout} seconds. Error: ${error}`); + } + } + + static staticText(label: string) { + const selector = `**/XCUIElementTypeStaticText[\`label == "${label}"\`]`; + return $(`-ios class chain:${selector}`); + } + + static staticTextContains(label: string) { + const selector = `**/XCUIElementTypeStaticText[\`label CONTAINS "${label}"\`]`; + return $(`-ios class chain:${selector}`); + } + + static clickStaticText (label: string) { + this.waitElementVisible(this.staticText(label)); + this.staticText(label).click(); + } + + static doubleClick(element) { + this.waitElementVisible(element); + element.doubleClick(); + } +} + +export default ElementHelper; diff --git a/appium/tests/screenobjects/all-screens.ts b/appium/tests/screenobjects/all-screens.ts new file mode 100644 index 000000000..c7f6b4550 --- /dev/null +++ b/appium/tests/screenobjects/all-screens.ts @@ -0,0 +1,13 @@ +import SplashScreen from './splash.screen'; +import CreateKeyScreen from './create-key.screen'; +import InboxScreen from './inbox.screen'; +import MenuBarScreen from './menu-bar.screen'; +import EmailScreen from './email.screen' + +export { + SplashScreen, + CreateKeyScreen, + InboxScreen, + MenuBarScreen, + EmailScreen +}; diff --git a/appium/tests/screenobjects/base.screen.ts b/appium/tests/screenobjects/base.screen.ts new file mode 100644 index 000000000..d2a178bff --- /dev/null +++ b/appium/tests/screenobjects/base.screen.ts @@ -0,0 +1,22 @@ +import { DEFAULT_TIMEOUT } from '../constants'; + +export default class BaseScreen { + + locator: string; + constructor (selector: string) { + this.locator = selector; + } + + /** + * Wait for screen to be visible + * + * @param {boolean} isShown + * @return {boolean} + */ + waitForScreen (isShown: boolean = true) { + return $(this.locator).waitForDisplayed({ + timeout: DEFAULT_TIMEOUT, + reverse: !isShown, + }); + } +} diff --git a/appium/tests/screenobjects/create-key.screen.ts b/appium/tests/screenobjects/create-key.screen.ts new file mode 100644 index 000000000..baebc04ab --- /dev/null +++ b/appium/tests/screenobjects/create-key.screen.ts @@ -0,0 +1,53 @@ +import BaseScreen from './base.screen'; +import {CommonData} from '../data'; + +const SELECTORS = { + SET_PASS_PHRASE_BUTTON: '~Set pass phrase', + ENTER_YOUR_PASS_PHRASE_FIELD: '-ios class chain:**/XCUIElementTypeSecureTextField[`value == "Enter your pass phrase"`]', + OK_BUTTON: '~Ok', + CONFIRM_PASS_PHRASE_FIELD: '~textField', +}; + +class CreateKeyScreen extends BaseScreen { + constructor () { + super(SELECTORS.SET_PASS_PHRASE_BUTTON); + } + + get setPassPhraseButton () { + return $(SELECTORS.SET_PASS_PHRASE_BUTTON); + } + + get enterPassPhraseField () { + return $(SELECTORS.ENTER_YOUR_PASS_PHRASE_FIELD); + } + + get okButton () { + return $(SELECTORS.OK_BUTTON) + } + + get confirmPassPhraseField () { + return $(SELECTORS.CONFIRM_PASS_PHRASE_FIELD) + } + + fillPassPhrase (passPhrase: string) { + this.enterPassPhraseField.setValue(passPhrase); + } + + clickSetPassPhraseBtn () { + this.setPassPhraseButton.click(); + } + + confirmPassPhrase (passPhrase: string) { + this.confirmPassPhraseField.click(); + this.confirmPassPhraseField.setValue(passPhrase); + this.okButton.click(); + } + + setPassPhrase(text: string = CommonData.account.passPhrase) { + this.fillPassPhrase(text); + this.clickSetPassPhraseBtn(); + this.confirmPassPhrase(text); + } +} + +export default new CreateKeyScreen(); diff --git a/appium/tests/screenobjects/email.screen.ts b/appium/tests/screenobjects/email.screen.ts new file mode 100644 index 000000000..b79102b7a --- /dev/null +++ b/appium/tests/screenobjects/email.screen.ts @@ -0,0 +1,45 @@ +import BaseScreen from './base.screen'; + +const SELECTORS = { + BACK_BTN: '~arrow left c' +}; + +const { join } = require('path'); + +class EmailScreen extends BaseScreen { + constructor () { + super(SELECTORS.BACK_BTN); + } + + get backButton() { + return $(SELECTORS.BACK_BTN) + } + + checkEmailAddress (email) { + const selector = `~${email}`; + $(selector).waitForDisplayed(); + } + + checkEmailSubject (subject) { + const selector = `~${subject}`; + $(selector).waitForDisplayed(); + } + + checkEmailText (text) { + const selector = `~${text}`; + $(selector).waitForDisplayed(); + } + + checkOpenedEmail (email, subject, text) { + this.backButton.waitForDisplayed(); + this.checkEmailAddress(email); + this.checkEmailSubject(subject); + this.checkEmailText(text); + } + + clickBackButton () { + this.backButton.click(); + } +} + +export default new EmailScreen(); diff --git a/appium/tests/screenobjects/inbox.screen.ts b/appium/tests/screenobjects/inbox.screen.ts new file mode 100644 index 000000000..cf71c829d --- /dev/null +++ b/appium/tests/screenobjects/inbox.screen.ts @@ -0,0 +1,22 @@ +import BaseScreen from './base.screen'; + +const SELECTORS = { + ENTER_YOUR_PASS_PHRASE_FIELD: '-ios class chain:**/XCUIElementTypeSecureTextField[`value == "Enter your pass phrase"`]', + OK_BUTTON: '~Ok', + CONFIRM_PASS_PHRASE_FIELD: '~textField', + +}; + +class InboxScreen extends BaseScreen { + constructor () { + super(SELECTORS.CONFIRM_PASS_PHRASE_FIELD); + } + clickOnUserEmail (email) { + const selector = `~${email}`; + $(selector).click(); + } + + +} + +export default new InboxScreen(); diff --git a/appium/tests/screenobjects/menu-bar.screen.ts b/appium/tests/screenobjects/menu-bar.screen.ts new file mode 100644 index 000000000..b09840d21 --- /dev/null +++ b/appium/tests/screenobjects/menu-bar.screen.ts @@ -0,0 +1,47 @@ +import BaseScreen from './base.screen'; +import {CommonData} from "../data"; +import ElementHelper from "../helpers/ElementHelper"; + +const SELECTORS = { + MENU_ICON: '~menu icn', + LOGOUT_BTN: '~Log out', + SETTINGS_BTN: '~Settings' +}; + +class MenuBarScreen extends BaseScreen { + constructor () { + super(SELECTORS.MENU_ICON); + } + + get menuIcon () { + return $(SELECTORS.MENU_ICON); + } + + get logoutButton () { + return $(SELECTORS.LOGOUT_BTN); + } + + get settingsButton () { + return $(SELECTORS.SETTINGS_BTN) + } + + clickMenuIcon () { + this.menuIcon.click(); + } + + checkUserEmail (email: string = CommonData.account.email) { + const selector = `~${email}`; + $(selector).waitForDisplayed(); + } + + checkMenuBar () { + expect(this.logoutButton).toBeDisplayed(); + expect(this.settingsButton).toBeDisplayed(); + } + + clickLogout () { + this.logoutButton.click(); + } +} + +export default new MenuBarScreen(); diff --git a/appium/tests/screenobjects/splash.screen.ts b/appium/tests/screenobjects/splash.screen.ts new file mode 100644 index 000000000..c0a79186b --- /dev/null +++ b/appium/tests/screenobjects/splash.screen.ts @@ -0,0 +1,140 @@ +import BaseScreen from './base.screen'; +import {CommonData} from "../data"; + +const SELECTORS = { + PRIVACY_TAB: '~privacy', + TERMS_TAB: '~terms', + SECURITY_TAB: '~security', + CONTINUE_WITH_GOOGLE_BTN: '~Continue with Gmail', + CONTINUE_WITH_OUTLOOK_BTN: '~Continue with Outlook', + OTHER_EMAIL_PROVIDER_BTN: '~Other email provider', + CONTINUE_BTN: '~Continue', + CANCEL_BTN: '~Cancel', + LOGIN_FIELD: '~Email or phone', + NEXT_BTN: '-ios class chain:**/XCUIElementTypeButton[`label == "Next"`][1]', + PASSWORD_FIELD: '~Enter your password', + DONE_BTN: '~Done', + LANGUAGE_DROPDOWN: '-ios class chain:**/XCUIElementTypeOther[`label == "content information"`]/XCUIElementTypeOther[1]', +}; + +class SplashScreen extends BaseScreen { + constructor () { + super(SELECTORS.PRIVACY_TAB); + } + + get privacyTab () { + return $(SELECTORS.PRIVACY_TAB); + } + + get termsTab () { + return $(SELECTORS.TERMS_TAB); + } + + get securityTab () { + return $(SELECTORS.SECURITY_TAB); + } + + get continueWithGmailBtn () { + return $(SELECTORS.CONTINUE_WITH_GOOGLE_BTN); + } + + get continueWithOutlookBtn () { + return $(SELECTORS.CONTINUE_WITH_OUTLOOK_BTN); + } + + get otherEmailProviderButton () { + return $(SELECTORS.OTHER_EMAIL_PROVIDER_BTN); + } + + get continueButton () { + return $(SELECTORS.CONTINUE_BTN); + } + + get cancelButton () { + return $(SELECTORS.CANCEL_BTN); + } + + get loginField () { + return $(SELECTORS.LOGIN_FIELD); + } + + get passwordField () { + return $(SELECTORS.PASSWORD_FIELD); + } + + get nextButton () { + return $(SELECTORS.NEXT_BTN); + } + + get doneButton () { + return $(SELECTORS.DONE_BTN) + } + + get languageDropdown () { + return $(SELECTORS.LANGUAGE_DROPDOWN) + } + + checkLoginPage () { + expect(this.privacyTab).toBeDisplayed(); + expect(this.termsTab).toBeDisplayed(); + expect(this.securityTab).toBeDisplayed(); + expect(this.continueWithGmailBtn).toBeDisplayed(); + expect(this.continueWithOutlookBtn).toBeDisplayed(); + expect(this.otherEmailProviderButton).toBeDisplayed(); + } + + clickContinueWithGmail () { + this.continueWithGmailBtn.click(); + } + + clickContinueBtn () { + expect(this.continueButton).toBeDisplayed(); + expect(this.cancelButton).toBeDisplayed(); + this.continueButton.click(); + } + + changeLanguage (language: string = '‪English (United States)‬') { + this.languageDropdown.click(); + const selector = `~${language}`; + $(selector).waitForDisplayed(); + $(selector).click(); + } + + fillEmail (email: string) { + this.loginField.click(); + this.loginField.setValue(email); + this.doneButton.click(); + } + + clickNextBtn () { + this.nextButton.click(); + } + + fillPassword(password: string) { + this.passwordField.click(); + this.passwordField.setValue(password); + this.doneButton.click(); + } + + gmailLogin (email: string, password: string) { + const emailSelector = `-ios class chain:**/XCUIElementTypeStaticText[\`label == "${email}"\`]`; + if($(emailSelector).isDisplayed()) { + $(emailSelector).click(); + } else { + this.fillEmail(email); + this.clickNextBtn(); + this.fillPassword(password); + this.clickNextBtn(); + } + } + + login(email: string = CommonData.account.email, password: string = CommonData.account.password) { + driver.launchApp(); + this.clickContinueWithGmail(); + this.clickContinueBtn(); + this.changeLanguage(); + this.gmailLogin(email, password); + } +} + +export default new SplashScreen(); diff --git a/appium/tests/specs/inbox/ReadTextEmail.spec.ts b/appium/tests/specs/inbox/ReadTextEmail.spec.ts new file mode 100644 index 000000000..8ca0386de --- /dev/null +++ b/appium/tests/specs/inbox/ReadTextEmail.spec.ts @@ -0,0 +1,24 @@ +import { + SplashScreen, + CreateKeyScreen, + InboxScreen, + EmailScreen +} from '../../screenobjects/all-screens'; + +import {CommonData} from '../../data'; + +describe('INBOX: ', () => { + + it('user is able to view text email', () => { + + const senderEmail = CommonData.sender.email; + const emailSubject = 'Test 1'; + const emailText = 'Test email'; + + SplashScreen.login(); + CreateKeyScreen.setPassPhrase(); + + InboxScreen.clickOnUserEmail(senderEmail); + EmailScreen.checkOpenedEmail(senderEmail, emailSubject, emailText); + }); +}); diff --git a/appium/tests/specs/login/GmailLogin.spec.ts b/appium/tests/specs/login/GmailLogin.spec.ts new file mode 100644 index 000000000..cb60c6b4a --- /dev/null +++ b/appium/tests/specs/login/GmailLogin.spec.ts @@ -0,0 +1,23 @@ +import { + SplashScreen, + CreateKeyScreen, + MenuBarScreen +} from '../../screenobjects/all-screens'; + +import {CommonData} from '../../data'; + +describe('LOGIN: ', () => { + + it('user is able to login via gmail', () => { + + SplashScreen.login(); + CreateKeyScreen.setPassPhrase(); + + MenuBarScreen.clickMenuIcon(); + MenuBarScreen.checkUserEmail(); + MenuBarScreen.checkMenuBar(); + + MenuBarScreen.clickLogout(); + SplashScreen.checkLoginPage(); + }); +}); diff --git a/appium/tsconfig.json b/appium/tsconfig.json new file mode 100644 index 000000000..b7ecf8797 --- /dev/null +++ b/appium/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "outDir": "./.tsbuild/", + "target": "es2016", + "module": "commonjs", + "types": ["node", "@wdio/sync", "@wdio/jasmine-framework"], + "esModuleInterop": true + }, + "include": [ + "./tests/**/*.ts" + ] +}