diff --git a/.detoxrc.json b/.detoxrc.json index 271e099258..5f25c571ff 100644 --- a/.detoxrc.json +++ b/.detoxrc.json @@ -24,7 +24,7 @@ "build": "cd android && ENVFILE=.env.mock ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..", "type": "android.emulator", "device": { - "avdName": "Nexus_5X_API_29_x86" + "avdName": "Nexus_6_API_30" } }, "android.staging": { @@ -33,7 +33,16 @@ "build": "cd android && ENVFILE=.env.mock ./gradlew assembleStagingRelease assembleAndroidTest -DtestBuildType=stagingRelease && cd ..", "type": "android.emulator", "device": { - "avdName": "Nexus_5X_API_29_x86" + "avdName": "Nexus_6_API_30" + } + }, + "android.staging-ci": { + "binaryPath": "android/app/build/outputs/apk/stagingRelease/app-x86-stagingRelease.apk", + "testBinaryPath": "android/app/build/outputs/apk/androidTest/stagingRelease/app-stagingRelease-androidTest.apk", + "build": "cd android && ENVFILE=.env.mock ./gradlew assembleStagingRelease assembleAndroidTest -DtestBuildType=stagingRelease -PwithTestButler --warning-mode all && cd ..", + "type": "android.emulator", + "device": { + "avdName": "Nexus_6_API_30" } } } diff --git a/.github/workflows/detox-ci.yml b/.github/workflows/detox-ci.yml new file mode 100644 index 0000000000..724feef066 --- /dev/null +++ b/.github/workflows/detox-ci.yml @@ -0,0 +1,77 @@ +name: Detox E2E CI + +on: + pull_request: + branches: + - "*" + workflow_dispatch: + inputs: + prNumber: + description: pr number to trigger on + required: true + schedule: + - cron: "0 0 * * *" + +jobs: + ios: + runs-on: macos-latest + steps: + - name: Checkout PR + uses: actions/checkout@v2 + with: + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6 # Not needed with a .ruby-version file + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + + - name: Install applesimutils + run: | + brew tap wix/brew + brew install applesimutils + + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: 14 + cache: 'yarn' + + - name: Has hash commit deps + uses: ledgerhq/actions/packages/has-hash-commit-deps@main + id: has-hash-commit-deps + with: + workspace: ${{ github.workspace }} + + - name: Install dependencies without network concurrency + if: ${{ steps.has-hash-commit-deps.outputs.has-hash-commit-deps == 'true' }} + env: + JOBS: max + run: yarn --prefer-offline --frozen-lockfile --network-timeout 100000 --network-concurrency 1 + + - name: Install dependencies with network concurrency + if: ${{ steps.has-hash-commit-deps.outputs.has-hash-commit-deps == 'false' }} + env: + JOBS: max + run: yarn --prefer-offline --frozen-lockfile --network-timeout 100000 + + - name: Rebuild detox + run: yarn detox clean-framework-cache && yarn detox build-framework-cache + + - name: Build iOS app for Detox test run + env: + NODE_OPTIONS: "--max-old-space-size=7168" + run: yarn e2e:build -c ios.staging + + - name: Test iOS app + timeout-minutes: 15 + run: yarn e2e:test -c ios.staging --loglevel verbose --record-logs failing --record-timeline all --take-screenshots all --record-videos failing --detectOpenHandles + + - name: Upload test artifacts + if: always() + uses: actions/upload-artifact@v2 + with: + name: test-ios-artifacts + path: artifacts diff --git a/e2e/engine/bridge/client.js b/e2e/bridge/client.js similarity index 85% rename from e2e/engine/bridge/client.js rename to e2e/bridge/client.js index dca7397d9a..0500499e87 100644 --- a/e2e/engine/bridge/client.js +++ b/e2e/bridge/client.js @@ -3,12 +3,12 @@ import { Platform } from "react-native"; import invariant from "invariant"; import { Subject } from "rxjs/Subject"; import type { AccountRaw } from "@ledgerhq/live-common/lib/types"; -import { store } from "../../../src/context/LedgerStore"; -import { importSettings } from "../../../src/actions/settings"; -import { setAccounts } from "../../../src/actions/accounts"; -import { acceptTerms } from "../../../src/logic/terms"; -import accountModel from "../../../src/logic/accountModel"; -import { navigate } from "../../../src/rootnavigation"; +import { store } from "../../src/context/LedgerStore"; +import { importSettings } from "../../src/actions/settings"; +import { setAccounts } from "../../src/actions/accounts"; +import { acceptTerms } from "../../src/logic/terms"; +import accountModel from "../../src/logic/accountModel"; +import { navigate } from "../../src/rootnavigation"; let ws: WebSocket; diff --git a/e2e/engine/bridge/server.js b/e2e/bridge/server.js similarity index 91% rename from e2e/engine/bridge/server.js rename to e2e/bridge/server.js index 1f79ad7e5a..1c072d6f7d 100644 --- a/e2e/engine/bridge/server.js +++ b/e2e/bridge/server.js @@ -3,8 +3,7 @@ import { Server } from "ws"; import path from "path"; import fs from "fs"; import type { E2EBridgeMessage } from "./client"; -import { $waitFor } from "../utils"; -import { NavigatorName } from "../../../src/const"; +import { NavigatorName } from "../../src/const"; let wss: Server; @@ -35,15 +34,12 @@ export async function loadConfig( const { data } = JSON.parse(f); postMessage({ type: "importSettngs", payload: data.settings }); + navigate(NavigatorName.Base); if (data.accounts.length) { postMessage({ type: "importAccounts", payload: data.accounts }); - await $waitFor("PortfolioAccountsList", -1, 10000); - return; } - - await $waitFor("PortfolioEmptyAccount"); } function navigate(name: string) { diff --git a/e2e/config.json b/e2e/config.json index 7c8421798e..8dbcb8b990 100644 --- a/e2e/config.json +++ b/e2e/config.json @@ -1,9 +1,11 @@ { + "globalSetup": "./global-setup.js", + "globalTeardown": "./global-teardown.js", + "setupFilesAfterEnv": ["./setup.js"], "testEnvironment": "./environment", "testRunner": "jest-circus/runner", - "testTimeout": 120000, + "testTimeout": 300000, "testRegex": "\\.spec\\.js$", "reporters": ["detox/runners/jest/streamlineReporter"], - "verbose": true, - "setupFilesAfterEnv": ["./init.js"] + "verbose": true } diff --git a/e2e/e2e-bridge-setup.js b/e2e/e2e-bridge-setup.js index 8da1f012cb..d91cd0b371 100644 --- a/e2e/e2e-bridge-setup.js +++ b/e2e/e2e-bridge-setup.js @@ -2,5 +2,5 @@ import Config from "react-native-config"; if (Config.MOCK) { - import("./engine/bridge/client").then(({ init }) => init()); + import("./bridge/client").then(({ init }) => init()); } diff --git a/e2e/engine/flows/index.js b/e2e/engine/flows/index.js deleted file mode 100644 index 2406d001cc..0000000000 --- a/e2e/engine/flows/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// @flow -export * from "./onboarding"; diff --git a/e2e/engine/flows/onboarding.js b/e2e/engine/flows/onboarding.js deleted file mode 100644 index 9774d92109..0000000000 --- a/e2e/engine/flows/onboarding.js +++ /dev/null @@ -1,66 +0,0 @@ -// @flow -import * as bridge from "../bridge/server"; -import { $tap, $proceed, $, $scrollTill, $waitFor } from "../utils"; - -export function onboard(modelId: DeviceModelId, usecase: Usecase) { - getStarted(); - acceptTerms(); - selectUsecase(modelId, usecase); -} - -function getStarted() { - it("should go terms screen", async () => { - await $proceed(); - }); -} - -function acceptTerms() { - it("should check terms and policy", async () => { - await $tap("TermsAcceptSwitch"); - }); - - it("should enter Ledger App", async () => { - await $proceed(); - }); -} - -async function selectUsecase(modelId: DeviceModelId, usecase: Usecase) { - it(`should go connect screen for ${modelId}`, async () => { - await $tap(`Onboarding Device - Selection|${modelId}`); - }); - - switch (usecase) { - case "connect": - if (modelId === "nanoX") { - await connectViaBluetooth(modelId); - } - break; - default: - break; - } -} - -async function connectViaBluetooth(modelId: DeviceModelId) { - it("should pair Nano via Bluetooth", async () => { - const el = $(`Onboarding - Connect|${modelId}`); - await $scrollTill(el); - await $tap(el); - await $waitFor("Onboarding - Seed warning"); - const dismissSeedWarning = $("Onboarding - Seed warning"); - await $tap(dismissSeedWarning); - await $tap("OnboardingStemPairNewContinue"); - await $proceed(); - const [david] = bridge.addDevices(); - // TODO E2E: Android - await $waitFor(`DeviceItemEnter ${david}`); - await $tap(`DeviceItemEnter ${david}`); - bridge.setInstalledApps(); - bridge.open(); - await $proceed(); - await $tap("OnboardingFinish"); - }); -} - -type DeviceModelId = "nanoS" | "nanoX" | "blue"; - -type Usecase = "newDevice" | "import" | "restore" | "connect"; diff --git a/e2e/engine/index.js b/e2e/engine/index.js deleted file mode 100644 index 052d1a9e54..0000000000 --- a/e2e/engine/index.js +++ /dev/null @@ -1,6 +0,0 @@ -// @flow -import * as bridge from "./bridge/server"; - -export { bridge }; -export * from "./utils"; -export * from "./flows"; diff --git a/e2e/engine/utils.js b/e2e/engine/utils.js deleted file mode 100644 index 6cacc6235d..0000000000 --- a/e2e/engine/utils.js +++ /dev/null @@ -1,63 +0,0 @@ -// @flow -const { device, element, by, waitFor } = require("detox"); - -export function cleanLaunch() { - return device.launchApp({ delete: true }); -} - -export async function $proceed(): Promise { - const el = $("Proceed"); - await $waitFor(el); - await el.tap(); -} - -export function $(id: string) { - return element(by.id(id)); -} - -export function $byText(text: string) { - return element(by.text(text)); -} - -// specifing -1 as percentage would check just an existence -export function $waitFor( - q: Query, - percentage: number = 75, - timeout: number = 5000, -) { - const el = getElement(q); - return percentage < 0 - ? waitFor(el) - .toExist() - .withTimeout(timeout) - : waitFor(el) - .toBeVisible(percentage) - .withTimeout(timeout); -} - -export function $tap(q: Query) { - return getElement(q).tap(); -} - -export function $scrollTill( - visibleTarget: Query, - scrollViewId: string = "ScrollView", - pixel: number = 200, - direction: ScrollDirection = "down", -) { - const targetEl = getElement(visibleTarget); - return waitFor(targetEl) - .toBeVisible() - .whileElement(by.id(scrollViewId)) - .scroll(pixel, direction); -} - -type Element = any; - -type Query = string | Element; - -function getElement(q: Query): Element { - return typeof q === "string" ? $(q) : q; -} - -type ScrollDirection = "top" | "down" | "right" | "left"; diff --git a/e2e/global-setup.js b/e2e/global-setup.js new file mode 100644 index 0000000000..5b0f1ca4a1 --- /dev/null +++ b/e2e/global-setup.js @@ -0,0 +1,7 @@ +const detox = require("detox"); + +async function globalSetup() { + await detox.globalInit(); +} + +module.exports = globalSetup; diff --git a/e2e/global-teardown.js b/e2e/global-teardown.js new file mode 100644 index 0000000000..1926f701cb --- /dev/null +++ b/e2e/global-teardown.js @@ -0,0 +1,7 @@ +const detox = require("detox"); + +async function globalTeardown() { + await detox.globalCleanup(); +} + +module.exports = globalTeardown; diff --git a/e2e/helpers.js b/e2e/helpers.js new file mode 100644 index 0000000000..bec019a775 --- /dev/null +++ b/e2e/helpers.js @@ -0,0 +1,109 @@ +import { readFileSync } from "fs"; + +const DEFAULT_TIMEOUT = 30000; + +export function waitAndTap(elementId, timeout) { + waitFor(element(by.id(elementId))) + .toBeVisible() + .withTimeout(timeout || DEFAULT_TIMEOUT); + + return element(by.id(elementId)).tap(); +} + +export function waitForElement(elementId, timeout) { + return waitFor(element(by.id(elementId))) + .toBeVisible() + .withTimeout(timeout || DEFAULT_TIMEOUT); +} + +export function waitForElementByText(text, timeout) { + return waitFor(element(by.text(text))) + .toBeVisible() + .withTimeout(timeout || DEFAULT_TIMEOUT); +} + +export function tap(elementId) { + return element(by.id(elementId)).tap(); +} + +export function tapByText(text, index) { + return element(by.text(text)) + .atIndex(index || 0) + .tap(); +} + +export async function typeText(elementId, text, focus = true) { + if (focus) { + await tap(elementId); + } + return element(by.id(elementId)).typeText(text); +} + +export function clearField(elementId) { + element(by.id(elementId)).replaceText(""); +} + +export async function scrollToElementById( + elementToScrollToId, + parentElementId, + pixelsToScroll, + direction = "down", + startPositionXAxis = NaN, + startPositionYAxis = 0.5, +) { + await waitFor(element(by.id(elementToScrollToId))) + .toBeVisible() + .whileElement(by.id(parentElementId)) + .scroll(pixelsToScroll, direction, startPositionXAxis, startPositionYAxis); +} + +export async function retryAction(action, timeout) { + let shouldContinue = true; + const startTime = Date.now(); + + while (shouldContinue) { + shouldContinue = false; + + try { + await action(); + } catch { + shouldContinue = true; + } + + if (timeout && Date.now() - startTime > timeout) { + throw new Error("Timed out when waiting for action"); + } + + // eslint-disable-next-line no-console + console.log("Trying again..."); + } +} + +export async function verifyIsVisible(elementId) { + await delay(1000); + await expect(element(by.id(elementId))).toBeVisible(); +} + +export async function verifyTextIsVisible(text) { + await delay(1000); + await expect(element(by.text(text))).toBeVisible(); +} + +export function delay(ms) { + return new Promise(resolve => { + setTimeout(() => { + resolve("delay complete"); + }, ms); + }); +} + +// for future use for screenshot conmparison +export function expectBitmapsToBeEqual(imagePath, expectedImagePath) { + const bitmapBuffer = readFileSync(imagePath); + const expectedBitmapBuffer = readFileSync(expectedImagePath); + if (!bitmapBuffer.equals(expectedBitmapBuffer)) { + throw new Error( + `Expected image at ${imagePath} to be equal to image at ${expectedImagePath}, but it was different!`, + ); + } +} diff --git a/e2e/init.js b/e2e/init.js deleted file mode 100644 index 78643f69da..0000000000 --- a/e2e/init.js +++ /dev/null @@ -1,10 +0,0 @@ -// @flow -import { bridge } from "./engine"; - -beforeAll(async () => { - bridge.init(); -}); - -afterAll(() => { - bridge.close(); -}); diff --git a/e2e/models/onboarding/onboardingSteps.js b/e2e/models/onboarding/onboardingSteps.js new file mode 100644 index 0000000000..ce451a1b5f --- /dev/null +++ b/e2e/models/onboarding/onboardingSteps.js @@ -0,0 +1,62 @@ +import * as bridge from "../../bridge/server"; +import * as testHelpers from "../../helpers"; + +export default class OnboardingSteps { + static async waitForPageToBeVisible() { + await testHelpers.waitForElementByText("Get started"); + } + + static async startOnboarding() { + await testHelpers.tapByText("Get started"); + } + + // change to tap by text + static async chooseToSetupLedger() { + await testHelpers.tapByText("SET UP MY LEDGER"); + // await testHelpers.tap("Onboarding PostWelcome - Selection|SetupLedger"); + } + + static async selectYourDevice(device) { + await testHelpers.tapByText(device); + // await testHelpers.tap(`Onboarding Device - Selection|${device}`); + } + + static async chooseToConnectYourNano() { + await testHelpers.tapByText("Connect your Nano"); + } + + static async verifyContentsOfBoxAreChecked() { + await testHelpers.tapByText("Continue"); + } + + static async chooseToPairMyNano() { + await testHelpers.tapByText("Let’s pair my Nano"); + } + + static async selectPairWithBluetooth() { + await testHelpers.tapByText("Pair with bluetooth"); + } + + static async addDeviceViaBluetooth() { + bridge.addDevices(); + await testHelpers.delay(1000); // give time for devices to appear + await testHelpers.tapByText("Nano X de David"); + + bridge.setInstalledApps(); // tell LLM what apps the mock device has + + bridge.open(); // open ledger manager + + // Continue to welcome screen + // await waitFor( + // element(by.text("Device authentication check")), + // ).not.toBeVisible(); + // issue here: the 'Pairing Successful' text is 'visible' before it actually is, so it's failing at the continue step as continue isn't actually visible + // await waitFor(element(by.text("Pairing Successful"))).toBeVisible(); + + await testHelpers.delay(5000); // wait for flaky 'device authentication check screen' + } + + static async openLedgerLive() { + await testHelpers.tapByText("Continue"); + } +} diff --git a/e2e/models/passwordEntryPage.js b/e2e/models/passwordEntryPage.js new file mode 100644 index 0000000000..709cb8f2b5 --- /dev/null +++ b/e2e/models/passwordEntryPage.js @@ -0,0 +1,17 @@ +import * as testHelpers from "../helpers"; + +export default class PasswordEntryPage { + static async enterPassword(password) { + if (device.getPlatform() === "ios") { + await element(by.type("RCTUITextField")).typeText(password); + } + + if (device.getPlatform() === "android") { + await element(by.type("android.widget.TextView")).typeText(password); + } + } + + static async login() { + await testHelpers.tapByText("Log in"); + } +} diff --git a/e2e/models/portfolioPage.js b/e2e/models/portfolioPage.js new file mode 100644 index 0000000000..38e8f8ea9f --- /dev/null +++ b/e2e/models/portfolioPage.js @@ -0,0 +1,15 @@ +import * as testHelpers from "../helpers"; + +export default class PortfolioPage { + static async waitForPageToBeVisible() { + await testHelpers.waitForElement("settings-icon"); + } + + static async navigateToSettings() { + await testHelpers.tap("settings-icon"); + } + + static async emptyPortfolioIsVisible() { + await testHelpers.verifyTextIsVisible("Add asset"); + } +} diff --git a/e2e/models/settings/generalSettingsPage.js b/e2e/models/settings/generalSettingsPage.js new file mode 100644 index 0000000000..652e6ebd27 --- /dev/null +++ b/e2e/models/settings/generalSettingsPage.js @@ -0,0 +1,32 @@ +import * as testHelpers from "../../helpers"; + +export default class GeneralSettingsPage { + static async togglePassword() { + if (device.getPlatform() === "ios") { + await element(by.type("RCTSwitch")) + .atIndex(0) + .tap(); + } + + if (device.getPlatform() === "android") { + await element(by.type("android.widget.Switch")) + .atIndex(0) + .tap(); + } + } + + static async enterNewPassword(passwordText) { + if (device.getPlatform() === "ios") { + await element(by.type("RCTUITextField")).typeText(passwordText); + } + + if (device.getPlatform() === "android") { + await element(by.type("android.widget.TextView")).typeText(passwordText); + } + await testHelpers.tapByText("Confirm"); + } + + static async isVisible() { + await testHelpers.verifyTextIsVisible("Preferred currency"); + } +} diff --git a/e2e/models/settings/settingsPage.js b/e2e/models/settings/settingsPage.js new file mode 100644 index 0000000000..c7208b1113 --- /dev/null +++ b/e2e/models/settings/settingsPage.js @@ -0,0 +1,7 @@ +import * as testHelpers from "../../helpers"; + +export default class SettingsPage { + static async navigateToGeneralSettings() { + await testHelpers.tap("general-settings-card"); + } +} diff --git a/e2e/setup.js b/e2e/setup.js new file mode 100644 index 0000000000..2532c55493 --- /dev/null +++ b/e2e/setup.js @@ -0,0 +1,41 @@ +import { execSync } from "child_process"; +import * as bridge from "./bridge/server"; + +beforeAll(() => { + bridge.init(); + setupDemoModeForScreenshots(); +}); + +afterAll(() => { + bridge.close(); +}); + +function setupDemoModeForScreenshots() { + // set app to demo mode for screenshots + if (device.getPlatform() === "ios") { + execSync( + 'xcrun simctl status_bar "iPhone 11 Pro" override --time "12:00" --batteryState charged --batteryLevel 100 --wifiBars 3 --cellularMode active --cellularBars 4', + ); + } + + if (device.getPlatform() === "android") { + // enter demo mode + execSync("adb shell settings put global sysui_demo_allowed 1"); + // display time 12:00 + execSync( + "adb shell am broadcast -a com.android.systemui.demo -e command clock -e hhmm 1200", + ); + // Display full mobile data with 4g type and no wifi + execSync( + "adb shell am broadcast -a com.android.systemui.demo -e command network -e mobile show -e level 4 -e datatype 4g -e wifi false", + ); + // Hide notifications + execSync( + "adb shell am broadcast -a com.android.systemui.demo -e command notifications -e visible false", + ); + // Show full battery but not in charging state + execSync( + "adb shell am broadcast -a com.android.systemui.demo -e command battery -e plugged false -e level 100", + ); + } +} diff --git a/e2e/setups/allLiveCoinsNoOperations.json b/e2e/setups/allLiveCoinsNoOperations.json new file mode 100644 index 0000000000..23bc865e79 --- /dev/null +++ b/e2e/setups/allLiveCoinsNoOperations.json @@ -0,0 +1,470 @@ +{ + "data": { + "settings": { + "hasCompletedOnboarding": true, + "counterValue": "EUR", + "preferredDeviceModel": "nanoX", + "language": null, + "theme": "dusk", + "region": null, + "orderAccounts": "name|asc", + "countervalueFirst": true, + "autoLockTimeout": 10, + "selectedTimeRange": "year", + "marketIndicator": "western", + "currenciesSettings": {}, + "pairExchanges": {}, + "developerMode": false, + "loaded": true, + "shareAnalytics": true, + "sentryLogs": true, + "lastUsedVersion": "2.3.2", + "dismissedBanners": [], + "accountsViewMode": "list", + "showAccountsHelperBanner": true, + "hideEmptyTokenAccounts": false, + "sidebarCollapsed": false, + "discreetMode": false, + "hasInstalledApps": true, + "blacklistedTokenIds": [] + }, + "user": { + "id": "9434c303-1d22-43a6-8fce-44c92d815ca8" + }, + "accounts": [ + { + "data": { + "id": "libcore:1:bitcoin:xpub6Bm5P7Xyx2UYrVBAgb54gEswXhbZaryZSWsPjeJ1jpb9K9S5UTD5z5cXW4EREkTqkNjSHQHxwHKZJVE7TFvftySnKabMAXAQCMSVJBdJxMC:", + "seedIdentifier": "04b9b3078fbdef02b5f5aa8bb400423d5170015da06c31ad7745160cbab1fa4cdc965f271b924c2999639211310f6d35029698749b38ea7e64608de3ebcdbaa46a", + "name": "Bitcoin 1 (legacy)", + "starred": false, + "derivationMode": "", + "index": 0, + "freshAddress": "1GaciDQKuhYMz5pcycBcBTwhRao8e4sfoK", + "freshAddressPath": "44'/0'/0'/0/112", + "freshAddresses": [ + { + "address": "1GaciDQKuhYMz5pcycBcBTwhRao8e4sfoK", + "derivationPath": "44'/0'/0'/0/112" + }, + { + "address": "1FJkd4ZQsz2GoaNhE7R3fDrKhPNm3MvSyw", + "derivationPath": "44'/0'/0'/0/113" + }, + { + "address": "1KskXU25MWGFb2dR9Y1nfUZ3SaU2Xkd5gZ", + "derivationPath": "44'/0'/0'/0/114" + }, + { + "address": "19W7vMG3MdEPrwdczh6QwT5bWZFnTM6rw7", + "derivationPath": "44'/0'/0'/0/115" + }, + { + "address": "1FZuY8soygaCxqbrSURTteVj8ZxBrV2ouz", + "derivationPath": "44'/0'/0'/0/116" + }, + { + "address": "17EutyEbazm1mWXWDfK2K5ejwYizPVq2gK", + "derivationPath": "44'/0'/0'/0/117" + }, + { + "address": "19bMyLBBq8FbTE4L7hFCJsbjYewPBNe6Lf", + "derivationPath": "44'/0'/0'/0/118" + }, + { + "address": "15jWmZ3ujDQCFzukYofW7LkCxerov5AAog", + "derivationPath": "44'/0'/0'/0/119" + }, + { + "address": "1Asii6uVSc5Pk4pwXn7rrourtkMrzVsfyL", + "derivationPath": "44'/0'/0'/0/120" + }, + { + "address": "1MY3Gsho5t3kkxa52FUYnXEJeuKiKZxSe5", + "derivationPath": "44'/0'/0'/0/121" + }, + { + "address": "1AXqhTrgoy7PFFraTiMoGb7XnK261THDny", + "derivationPath": "44'/0'/0'/0/122" + }, + { + "address": "1MErAsyj34SvUgdqJP5Xbs5kcrceETEBRo", + "derivationPath": "44'/0'/0'/0/123" + }, + { + "address": "1EihrhVvQ2DZb1LQ2in7ZgdWaTxJ5qmLJD", + "derivationPath": "44'/0'/0'/0/124" + }, + { + "address": "14TvLS3n8qEbVUfi8bPtLyiFrsecQR99in", + "derivationPath": "44'/0'/0'/0/125" + }, + { + "address": "18PQpXEJb8yRerzTHRgP3sNzZwYwPw3tak", + "derivationPath": "44'/0'/0'/0/126" + }, + { + "address": "1F5nBYmGfnFCUrHP6vgLh7Cr3rN5DmgEpb", + "derivationPath": "44'/0'/0'/0/127" + }, + { + "address": "1jA5Gkk4eSp4d3aUqw8kK1ZDWggAu9ibd", + "derivationPath": "44'/0'/0'/0/128" + }, + { + "address": "18nzNgCTonAxbG56Nu5ELfwyrGNch5vBQX", + "derivationPath": "44'/0'/0'/0/129" + }, + { + "address": "1Mw5HcE4Pnt9KSK1FKTFQEVyqLnvhTJLVe", + "derivationPath": "44'/0'/0'/0/130" + }, + { + "address": "1Epk9Hbsf8cEUZCjS6SvZWBqscX9mCZ4mj", + "derivationPath": "44'/0'/0'/0/131" + } + ], + "blockHeight": 628010, + "operationsCount": 219, + "operations": [], + "pendingOperations": [], + "currencyId": "bitcoin", + "unitMagnitude": 8, + "lastSyncDate": "", + "balance": "0", + "spendableBalance": "2578", + "xpub": "xpub6Bm5P7Xyx2UYrVBAgb54gEswXhbZaryZSWsPjeJ1jpb9K9S5UTD5z5cXW4EREkTqkNjSHQHxwHKZJVE7TFvftySnKabMAXAQCMSVJBdJxMC", + "balanceHistory": {}, + "subAccounts": [] + }, + "version": 1 + }, + { + "data": { + "id": "libcore:1:bitcoin:xpub6DVfEXXjv6nspJK6h7A57pRKgAStQDhEST2NNUUwmtGpatc2cEhvP4FbCYSmtF4DJoLHBPG5imcTNVxq8V2SDdENaf63HXPKgMkeZ7SEbsK:native_segwit", + "seedIdentifier": "04c2f0fe9c27396ba3e37558d71d8308572c5bba380cfe0a68450bc38039b781ce02f1ce14db54eed0ba56ed4aebba93338d3d0af82a6078ace90458f09069b125", + "name": "Bitcoin 1 (native segwit)", + "starred": false, + "derivationMode": "native_segwit", + "index": 0, + "freshAddress": "bc1qehatadheaph4jjxfph0fn5sc5vsk0fskch0x5t", + "freshAddressPath": "84'/0'/0'/0/52", + "freshAddresses": [ + { + "address": "bc1qehatadheaph4jjxfph0fn5sc5vsk0fskch0x5t", + "derivationPath": "84'/0'/0'/0/52" + }, + { + "address": "bc1qzaavs2djzt34n0jkvm0063z0cgml3mfcyu25m0", + "derivationPath": "84'/0'/0'/0/53" + }, + { + "address": "bc1qk0zcnpddv3z4eag68kt04d0e750kk4dq8exgqk", + "derivationPath": "84'/0'/0'/0/54" + }, + { + "address": "bc1qmter0ysa83gh8q5y8xnetmj0ncd85l975zfpg3", + "derivationPath": "84'/0'/0'/0/55" + }, + { + "address": "bc1qsh98tjaj7w5e88l5yzyyxz7kalsw3ar4amlpmy", + "derivationPath": "84'/0'/0'/0/56" + }, + { + "address": "bc1queyny73jh45y74fwhfx62hmtplhv4hjftd7ltd", + "derivationPath": "84'/0'/0'/0/57" + }, + { + "address": "bc1qlthx83tmu7kqjf4qt2ykspy0fs0u8mlslcjvje", + "derivationPath": "84'/0'/0'/0/58" + }, + { + "address": "bc1qgy0azhlqze9q4axns0pfskvwfue3aqc5ldvzkc", + "derivationPath": "84'/0'/0'/0/59" + }, + { + "address": "bc1qpqzqp3fmy456hfvatkw3akuq87ta7gn0q6wn7x", + "derivationPath": "84'/0'/0'/0/60" + }, + { + "address": "bc1qtzkz29ejg28udkwrckner8wvlcxnzzx2l55r6x", + "derivationPath": "84'/0'/0'/0/61" + }, + { + "address": "bc1qwrwgy6eddn4kmm4eac8y2tefrfk2ftfx854l3g", + "derivationPath": "84'/0'/0'/0/62" + }, + { + "address": "bc1qrfe9n6gfs066jp46w2negq99ytrdg2va6vvkq5", + "derivationPath": "84'/0'/0'/0/63" + }, + { + "address": "bc1qvlwgq9qmjuz2xdq76ygty0aafdvgauk08sezjw", + "derivationPath": "84'/0'/0'/0/64" + }, + { + "address": "bc1qrrpkpqwxjs6qumn0xmmls45hywck77psrw7wnp", + "derivationPath": "84'/0'/0'/0/65" + }, + { + "address": "bc1qpdkxswkqcqg8tge4l6tfhr7wjpkqv608quar9x", + "derivationPath": "84'/0'/0'/0/66" + }, + { + "address": "bc1q8jwdg7u33vw6m5q4h68dcyat3hlhr67w3uw90s", + "derivationPath": "84'/0'/0'/0/67" + }, + { + "address": "bc1q5n2gtsx782mdjq22gw4wyq2tsk9gehhg2zkwp3", + "derivationPath": "84'/0'/0'/0/68" + }, + { + "address": "bc1qp6j23np9l93vp7asgc4g7cupm5wvekxa95wgsq", + "derivationPath": "84'/0'/0'/0/69" + }, + { + "address": "bc1qpv55nkqalxweyphdse8nmqgwp6vuxaa9gpg7sh", + "derivationPath": "84'/0'/0'/0/70" + }, + { + "address": "bc1qssp0tza6wrnxzsd405d7t9d6m7jvshz0glfl67", + "derivationPath": "84'/0'/0'/0/71" + } + ], + "blockHeight": 628010, + "operationsCount": 117, + "operations": [], + "pendingOperations": [], + "currencyId": "bitcoin", + "unitMagnitude": 8, + "lastSyncDate": "", + "balance": "0", + "spendableBalance": "1700", + "xpub": "xpub6DVfEXXjv6nspJK6h7A57pRKgAStQDhEST2NNUUwmtGpatc2cEhvP4FbCYSmtF4DJoLHBPG5imcTNVxq8V2SDdENaf63HXPKgMkeZ7SEbsK", + "balanceHistory": {}, + "subAccounts": [] + }, + "version": 1 + }, + { + "data": { + "id": "libcore:1:komodo:v4PKUB9vPNEx9QFvX7SJkYhpX2QbsXdikCPFbeJLZK4hrFSD2XBJ2Ze7zC68e3Kfaia5Xzgey12HeqwSzF5itsprdyJghVxvrrMod6oMzS14oBVf:", + "seedIdentifier": "04fb0f483df5f252ff2ef489859d4f630bcd15ff6706fb3c8515f65f9faeedef1ab8ca4a561dc9366d5c3e456b2c4765e4c7b5098dcf3abd468cecba3ef0bdf270", + "name": "Komodo 1", + "starred": false, + "derivationMode": "", + "index": 0, + "freshAddress": "RJ8BdEPiYShsysUdYgDdVMRutkkTDgnu8w", + "freshAddressPath": "44'/141'/0'/0/48", + "freshAddresses": [ + { + "address": "RJ8BdEPiYShsysUdYgDdVMRutkkTDgnu8w", + "derivationPath": "44'/141'/0'/0/48" + }, + { + "address": "RXD42V42BhaGJtsxYr1YXdoUQnLHrPygbu", + "derivationPath": "44'/141'/0'/0/49" + }, + { + "address": "RTq6JkQ4gttGiViKKwkruojSX8BUdMeKfg", + "derivationPath": "44'/141'/0'/0/50" + }, + { + "address": "RVVZ32YuzimXokYkPFQveyf3nEfoMs981x", + "derivationPath": "44'/141'/0'/0/51" + }, + { + "address": "RFZbwN64jQ8KoHACCirMUx99nkKv4Bauq4", + "derivationPath": "44'/141'/0'/0/52" + }, + { + "address": "RVjjo4FATzgSUrmYwqbkU5brUKeXfhBhat", + "derivationPath": "44'/141'/0'/0/53" + }, + { + "address": "RSLjcDRremkb98xXLScGmBNWkCbbTYFEtb", + "derivationPath": "44'/141'/0'/0/54" + }, + { + "address": "RYQxTeUpWjFSTjvsg4jxAKXNjjjM34s2jE", + "derivationPath": "44'/141'/0'/0/55" + }, + { + "address": "RQZHBDLWNx2zUnTPoYeKUeMFbUbRGVKUy1", + "derivationPath": "44'/141'/0'/0/56" + }, + { + "address": "RQkUxunVMnJFSNMHZtMGzdKkLZhcmHKDyJ", + "derivationPath": "44'/141'/0'/0/57" + }, + { + "address": "RLGJf75BE1wKwWjPJseGgs6tgH98YwGBGp", + "derivationPath": "44'/141'/0'/0/58" + }, + { + "address": "RLf2gRk8MGwWnrXPGfRBxaUq3TrkrphDRN", + "derivationPath": "44'/141'/0'/0/59" + }, + { + "address": "RAs7DhvoUE22gy3n3vsfuNeWJbRZT3yfZ1", + "derivationPath": "44'/141'/0'/0/60" + }, + { + "address": "RByLBNDax7NscVf3egVVvPeAnVWetU8s3F", + "derivationPath": "44'/141'/0'/0/61" + }, + { + "address": "RVb7N1aKZSbT7SJ94YsZNQ1bNwpsEfb9W8", + "derivationPath": "44'/141'/0'/0/62" + }, + { + "address": "RPrgvcgR2piGZ4281YK8FqJqcXyFoCzV8b", + "derivationPath": "44'/141'/0'/0/63" + }, + { + "address": "RQJUD12DZipBGj2PfprBXznLvKR3jEPP7h", + "derivationPath": "44'/141'/0'/0/64" + }, + { + "address": "RETUiR3P5efnMiYiU9PiC3yad7VjYj6Hdg", + "derivationPath": "44'/141'/0'/0/65" + }, + { + "address": "RP4XM58vo8bakSCWdUZ7jmroStSZRocEp7", + "derivationPath": "44'/141'/0'/0/66" + }, + { + "address": "RTGK34H73gtrYspyMdtkCcHamhFi3dhN9W", + "derivationPath": "44'/141'/0'/0/67" + } + ], + "blockHeight": 1854558, + "operationsCount": 111, + "operations": [], + "pendingOperations": [], + "currencyId": "komodo", + "unitMagnitude": 8, + "lastSyncDate": "", + "balance": "0", + "spendableBalance": "233798275", + "balanceHistory": {}, + "xpub": "v4PKUB9vPNEx9QFvX7SJkYhpX2QbsXdikCPFbeJLZK4hrFSD2XBJ2Ze7zC68e3Kfaia5Xzgey12HeqwSzF5itsprdyJghVxvrrMod6oMzS14oBVf", + "subAccounts": [] + }, + "version": 1 + }, + { + "data": { + "id": "libcore:1:zcash:xpub6CEGMFW91HKTGhzxJjnTZ4mSrRkNqCAiArP14Dy8GhkpwHGzyMrxWgh3aT69bpi3t4RQSg5TtwKjzVcFdfyf86jMMxTABBF6e66UC1vMGze:", + "seedIdentifier": "04ddd8dc9459f1991eb8f4ae03f2095eb66511917de8076e5aa6974fe2cb527a03f2467654f038ecdd123d447de7bd4019870f6f24a6666dee47f456f82aa9b904", + "name": "Zcash 2", + "starred": false, + "derivationMode": "", + "index": 1, + "freshAddress": "t1fEuxn1xqTsxRev942RtWY5gn6AWrC9GpY", + "freshAddressPath": "44'/133'/1'/0/83", + "freshAddresses": [ + { + "address": "t1fEuxn1xqTsxRev942RtWY5gn6AWrC9GpY", + "derivationPath": "44'/133'/1'/0/83" + }, + { + "address": "t1Me9UKBpfrCiiosVDpNhVXzjWnsAG6cTS2", + "derivationPath": "44'/133'/1'/0/84" + }, + { + "address": "t1dRxwFjbpVUupmuR8iupykTF1wy33JJoG9", + "derivationPath": "44'/133'/1'/0/85" + }, + { + "address": "t1X8HcikGryH9ZZPgCiDAhdur5Upfd1ApSm", + "derivationPath": "44'/133'/1'/0/86" + }, + { + "address": "t1KCiKxLosdCLueGet6kwonPfvVJ8hg3N2q", + "derivationPath": "44'/133'/1'/0/87" + }, + { + "address": "t1KTw9ghcEpL1Hwq1zkHy3nkEx1j98Tg44r", + "derivationPath": "44'/133'/1'/0/88" + }, + { + "address": "t1KeC6nBJUg2oKbBv4Lb7qDXUcJDgXtQ7fs", + "derivationPath": "44'/133'/1'/0/89" + }, + { + "address": "t1dFWqa61wTE9bkNoR5qCGKq7rSQy11hFBx", + "derivationPath": "44'/133'/1'/0/90" + }, + { + "address": "t1MmT8DFhVQoapE6pCkPp7ec878zWnczNUX", + "derivationPath": "44'/133'/1'/0/91" + }, + { + "address": "t1Tsrmy181nmhhiTdALGSTP3dCNM18TXpEx", + "derivationPath": "44'/133'/1'/0/92" + }, + { + "address": "t1fERbygnTo8WafXrrU1YdVTdr6TaaJ7gp2", + "derivationPath": "44'/133'/1'/0/93" + }, + { + "address": "t1eRu5VPo6iGUqn2yxBHx1Cyv6mJyNcNPbE", + "derivationPath": "44'/133'/1'/0/94" + }, + { + "address": "t1Z3uPUb5wvWN27V9M2nehZc2nCiywne5mo", + "derivationPath": "44'/133'/1'/0/95" + }, + { + "address": "t1LADMvGe6euGonbwc77YPdMCgKkyT9VD2u", + "derivationPath": "44'/133'/1'/0/96" + }, + { + "address": "t1eFaYwmpDnPtHQEHqgExvxHA2yzKFhLm9T", + "derivationPath": "44'/133'/1'/0/97" + }, + { + "address": "t1S2V2idhSDvPtrCKqfMYWSBvuVBsACPoQc", + "derivationPath": "44'/133'/1'/0/98" + }, + { + "address": "t1czT6CaTrRUv4RuywwporncyjJCGCebv36", + "derivationPath": "44'/133'/1'/0/99" + }, + { + "address": "t1ajRwDpYGE1W7heZibg3MXkHDo7QiPjgPt", + "derivationPath": "44'/133'/1'/0/100" + }, + { + "address": "t1PXTyzjuuRUDRMgUJi8CRxX4ZSAW8nPmgg", + "derivationPath": "44'/133'/1'/0/101" + }, + { + "address": "t1f1iYgWkzQmYsrrAA7AnWaaPzzTijJXvP6", + "derivationPath": "44'/133'/1'/0/102" + } + ], + "blockHeight": 812636, + "operationsCount": 179, + "operations": [], + "pendingOperations": [], + "currencyId": "zcash", + "unitMagnitude": 8, + "lastSyncDate": "", + "balance": "0", + "spendableBalance": "0", + "balanceHistory": {}, + "xpub": "xpub6CEGMFW91HKTGhzxJjnTZ4mSrRkNqCAiArP14Dy8GhkpwHGzyMrxWgh3aT69bpi3t4RQSg5TtwKjzVcFdfyf86jMMxTABBF6e66UC1vMGze", + "subAccounts": [] + }, + "version": 1 + } + ], + "countervalues": { + "version": 2, + "daily": {}, + "hourly": {} + } + } +} diff --git a/e2e/setups/freshApp.json b/e2e/setups/freshApp.json new file mode 100644 index 0000000000..c9dd13aa40 --- /dev/null +++ b/e2e/setups/freshApp.json @@ -0,0 +1,89 @@ +{ + "data": { + "user": { + "id": "ff2b91f8-5513-448e-8b7c-ea02ca8478da" + }, + "settings": { + "hasCompletedOnboarding": false, + "counterValue": "USD", + "language": "en", + "theme": null, + "region": null, + "locale": "en-US", + "orderAccounts": "balance|desc", + "countervalueFirst": false, + "autoLockTimeout": 10, + "selectedTimeRange": "month", + "marketIndicator": "western", + "currenciesSettings": {}, + "pairExchanges": {}, + "developerMode": false, + "loaded": true, + "shareAnalytics": true, + "sentryLogs": true, + "lastUsedVersion": "2.40.0", + "dismissedBanners": [], + "accountsViewMode": "list", + "nftsViewMode": "list", + "showAccountsHelperBanner": true, + "hideEmptyTokenAccounts": false, + "sidebarCollapsed": false, + "discreetMode": false, + "preferredDeviceModel": "nanoS", + "hasInstalledApps": true, + "carouselVisibility": 0, + "lastSeenDevice": null, + "latestFirmware": null, + "blacklistedTokenIds": [], + "deepLinkUrl": null, + "firstTimeLend": false, + "showClearCacheBanner": false, + "fullNodeEnabled": false, + "allowDebugApps": false, + "allowExperimentalApps": false, + "enablePlatformDevTools": false, + "catalogProvider": "production", + "enableLearnPageStagingUrl": false, + "swap": { + "hasAcceptedIPSharing": false, + "acceptedProviders": [], + "selectableCurrencies": [], + "KYC": {} + }, + "starredMarketCoins": [] + }, + "announcements": { + "announcements": [ + { + "content": { + "title": "NFTs are now in Ledger!", + "text": "You can now manage, view, store, and enjoy your NFTs from your Ethereum accounts on Ledger Live. If you are already a Ledger Nano X user, you can even send your NFTs securely, without any blind signing. All you have to do is to make sure to update your Ledger Live app." + }, + "published_at": "2022-02-25T12:00:00.000Z", + "expired_at": "2022-03-16T23:00:00.000Z", + "icon": "info", + "level": "info", + "uuid": "f3772990-e45f-4b08-baa2-a09905df629a" + }, + { + "content": { + "title": "Manage your SOL in Ledger Live!", + "text": "You can now create a Solana account, buy, send, and receive SOL in your Ledger Live app. All this while enjoying the security of your Ledger device.", + "link": { + "href": "ledgerlive://accounts", + "label": "Create your SOL account" + } + }, + "published_at": "2022-03-11T11:00:00.000Z", + "expired_at": "2022-04-01T08:00:00.000Z", + "icon": "info", + "level": "info", + "uuid": "5fc85a93-8cbf-4393-a1ce-ec297db4ee67" + } + ], + "seenIds": [], + "lastUpdateTime": 1647449227606 + }, + "accounts": [] + } +} \ No newline at end of file diff --git a/e2e/specs/engine.spec.js b/e2e/specs/engine.spec.js deleted file mode 100644 index 44dc076905..0000000000 --- a/e2e/specs/engine.spec.js +++ /dev/null @@ -1,20 +0,0 @@ -// @flow -import { cleanLaunch, bridge } from "../engine"; - -describe("Mobile E2E Test Engine", () => { - describe("Bridge", () => { - describe("loadConfig", () => { - beforeAll(async () => { - await cleanLaunch(); - }); - - it("should import settings", async () => { - await bridge.loadConfig("onboardingcompleted", true); - }); - - it("should import accounts", async () => { - await bridge.loadConfig("1AccountBTC1AccountETH", true); - }); - }); - }); -}); diff --git a/e2e/specs/onboarding.spec.js b/e2e/specs/onboarding.spec.js index 1a3788f578..ee1b112e3d 100644 --- a/e2e/specs/onboarding.spec.js +++ b/e2e/specs/onboarding.spec.js @@ -1,29 +1,24 @@ -// @flow -import { it } from "jest-circus"; -import { cleanLaunch, onboard } from "../engine"; +import OnboardingSteps from "../models/onboarding/onboardingSteps"; +import PortfolioPage from "../models/portfolioPage"; describe("Onboarding", () => { - describe("Nano X", () => { - beforeAll(async () => { - await cleanLaunch(); - }); - - describe("New Device", () => {}); - - describe("Import", () => {}); - - describe("Restore", () => {}); - - describe("Connect", () => { - onboard("nanoX", "connect"); - }); + beforeAll(async () => { + await device.launchApp(); }); - describe("Nano S", () => { - it.todo("should run through Nano S onboarding"); - }); + it("should be able to connect a Nano X", async () => { + await OnboardingSteps.waitForPageToBeVisible(); + await OnboardingSteps.startOnboarding(); + await OnboardingSteps.chooseToSetupLedger(); + await OnboardingSteps.selectYourDevice("NANO X"); + await OnboardingSteps.chooseToConnectYourNano(); + await OnboardingSteps.verifyContentsOfBoxAreChecked(); + await OnboardingSteps.chooseToPairMyNano(); + await OnboardingSteps.selectPairWithBluetooth(); + await OnboardingSteps.addDeviceViaBluetooth(); + await OnboardingSteps.openLedgerLive(); - describe("Nano Blue", () => { - it.todo("should run through Nano blue onboarding"); + await PortfolioPage.waitForPageToBeVisible(); + await PortfolioPage.emptyPortfolioIsVisible(); }); }); diff --git a/e2e/specs/password.spec.js b/e2e/specs/password.spec.js new file mode 100644 index 0000000000..1311189254 --- /dev/null +++ b/e2e/specs/password.spec.js @@ -0,0 +1,31 @@ +import PortfolioPage from "../models/portfolioPage"; +import SettingsPage from "../models/settings/settingsPage"; +import GeneralSettingsPage from "../models/settings/generalSettingsPage"; +import PasswordEntryPage from "../models/passwordEntryPage"; +import { delay } from "../helpers"; +import { loadConfig } from "../bridge/server"; + +const CORRECT_PASSWORD = "passWORD$123!"; + +describe("Password Lock Screen", () => { + beforeAll(async () => { + await device.launchApp(); + }); + + it("should be able to enter the correct password", async () => { + await loadConfig("1AccountBTC1AccountETH", true); + + await PortfolioPage.waitForPageToBeVisible(); + await PortfolioPage.navigateToSettings(); + await SettingsPage.navigateToGeneralSettings(); + await GeneralSettingsPage.togglePassword(); + await GeneralSettingsPage.enterNewPassword(CORRECT_PASSWORD); + await GeneralSettingsPage.enterNewPassword(CORRECT_PASSWORD); // confirm password step + await device.sendToHome(); // leave LLM app and go to phone's home screen + await delay(60001); // password takes 60 seconds of app inactivity to activate + await device.launchApp(); // restart LLM + await PasswordEntryPage.enterPassword(CORRECT_PASSWORD); + await PasswordEntryPage.login(); + await GeneralSettingsPage.isVisible(); + }); +}); diff --git a/e2e/specs/perf-test.spec.js b/e2e/specs/perf-test.spec.js new file mode 100644 index 0000000000..87875cc4c6 --- /dev/null +++ b/e2e/specs/perf-test.spec.js @@ -0,0 +1,44 @@ +import { loadConfig } from "../bridge/server"; +import { delay, retryAction } from "../helpers"; + +const { device, element, by, waitFor } = require("detox"); + +describe.skip("Navigation while syncing - performance test", () => { + beforeAll(async () => { + await device.launchApp({ + delete: true, + launchArgs: { + detoxURLBlacklistRegex: ".*://explorers.api.live.ledger.com/.*", + }, + }); + }); + + it.skip("should import accounts", async () => { + const initialTime = Date.now(); + await device.disableSynchronization(); + + await retryAction(async () => { + await loadConfig("allLiveCoinsNoOperations", true); + }); + + await retryAction(async () => { + const accountTabButton = element(by.id("TabBarAccounts")); + await waitFor(accountTabButton).toBeVisible(); + await delay(1000); + await accountTabButton.tap(); + }); + + await retryAction(async () => { + const firstAccountButton = element(by.text("Komodo 1")); + await waitFor(firstAccountButton).toBeVisible(); + await firstAccountButton.tap(); + }); + + await device.enableSynchronization(); + + // eslint-disable-next-line no-console + console.log( + `Test finished, took ${(Date.now() - initialTime) / 1000}s to execute`, + ); + }); +}); diff --git a/package.json b/package.json index 613279826f..a679b3fe92 100644 --- a/package.json +++ b/package.json @@ -212,7 +212,8 @@ "@types/react-test-renderer": "^17.0.1", "babel-jest": "^26.6.3", "babel-plugin-module-resolver": "^4.1.0", - "detox": "^18.2.1", + "detox": "^19.6.5", + "detox-recorder": "^1.0.151", "eslint": "7.32.0", "eslint-config-airbnb": "^18.2.1", "eslint-config-prettier": "^8.3.0", @@ -227,8 +228,8 @@ "flipper-plugin-rn-performance-android": "^0.1.0", "flow-bin": "0.122.0", "flow-typed": "^2.6.2", - "jest": "^26.6.3", - "jest-circus": "^26.6.3", + "jest": "^27.5.1", + "jest-circus": "^27.5.1", "jetifier": "^1.6.6", "local-web-server": "^4.2.1", "metro": "0.66.2", @@ -250,7 +251,8 @@ "react-native-svg": "12.1.1", "d3-array": "2.3.3", "@polkadot/util": "8.0.6-8", - "@polkadot/util-crypto": "8.0.6-8" + "@polkadot/util-crypto": "8.0.6-8", + "hermes-engine": "0.10.0" }, "react-native": { "zlib": "browserify-zlib", diff --git a/src/components/SettingsCard.tsx b/src/components/SettingsCard.tsx index e77fb7b084..983705aaf6 100644 --- a/src/components/SettingsCard.tsx +++ b/src/components/SettingsCard.tsx @@ -12,6 +12,7 @@ type Props = { Icon: IconType; onClick: Function; arrowRight?: boolean; + settingsCardTestId?: string; }; function Card({ @@ -48,6 +49,7 @@ export default function SettingsCard({ Icon, onClick, arrowRight, + settingsCardTestId, }: Props) { return ( @@ -59,7 +61,7 @@ export default function SettingsCard({ borderColor={"primary.c80"} iconColor={"primary.c80"} /> - + {title} diff --git a/src/components/Touchable.js b/src/components/Touchable.js index ef91ab7ff2..f464c7fab9 100644 --- a/src/components/Touchable.js +++ b/src/components/Touchable.js @@ -22,7 +22,7 @@ type Props = { event?: string, eventProperties?: { [key: string]: any }, style?: *, - testID?: string, + touchableTestID?: string, }; export default class Touchable extends Component< @@ -67,7 +67,7 @@ export default class Touchable extends Component< children, event, eventProperties, - testID, + touchableTestID, ...rest } = this.props; const { pending } = this.state; @@ -79,7 +79,7 @@ export default class Touchable extends Component< onPress={this.onPress} disabled={disabled} hitSlop={defaultHitSlop} - testID={testID ?? event} + testID={touchableTestID ?? event} {...rest} > {children} diff --git a/src/context/Locale.js b/src/context/Locale.js index 788ea66872..5c86a52633 100644 --- a/src/context/Locale.js +++ b/src/context/Locale.js @@ -19,10 +19,15 @@ import { } from "../languages"; import { languageSelector } from "../reducers/settings"; -if ("__setDefaultTimeZone" in Intl.DateTimeFormat) { - /** https://formatjs.io/docs/polyfills/intl-datetimeformat/#default-timezone */ - // $FlowFixMe - Intl.DateTimeFormat.__setDefaultTimeZone(getTimeZone()); // eslint-disable-line no-underscore-dangle +try { + if ("__setDefaultTimeZone" in Intl.DateTimeFormat) { + /** https://formatjs.io/docs/polyfills/intl-datetimeformat/#default-timezone */ + // $FlowFixMe + Intl.DateTimeFormat.__setDefaultTimeZone(getTimeZone()); // eslint-disable-line no-underscore-dangle + } +} catch (error) { + // eslint-disable-next-line no-console + console.log(error); } i18next.use(initReactI18next).init({ diff --git a/src/react-native-hw-transport-ble/makeMock.js b/src/react-native-hw-transport-ble/makeMock.js index db6cb6887a..0d552b659e 100644 --- a/src/react-native-hw-transport-ble/makeMock.js +++ b/src/react-native-hw-transport-ble/makeMock.js @@ -5,7 +5,7 @@ import { from } from "rxjs"; import { take, first, filter } from "rxjs/operators"; import type { ApduMock } from "../logic/createAPDUMock"; import { hookRejections } from "../logic/debugReject"; -import { e2eBridgeSubject } from "../../e2e/engine/bridge/client"; +import { e2eBridgeSubject } from "../../e2e/bridge/client"; export type DeviceMock = { id: string, diff --git a/src/screens/Onboarding/steps/setupDevice/scenes/PairNew.tsx b/src/screens/Onboarding/steps/setupDevice/scenes/PairNew.tsx index 951ccf1e73..01979b49f0 100644 --- a/src/screens/Onboarding/steps/setupDevice/scenes/PairNew.tsx +++ b/src/screens/Onboarding/steps/setupDevice/scenes/PairNew.tsx @@ -30,7 +30,12 @@ const Next = ({ const { t } = useTranslation(); return ( - ); diff --git a/src/screens/Portfolio/Header.tsx b/src/screens/Portfolio/Header.tsx index 440dedd9de..209a005576 100644 --- a/src/screens/Portfolio/Header.tsx +++ b/src/screens/Portfolio/Header.tsx @@ -188,7 +188,7 @@ function PortfolioHeader({ )} - + diff --git a/src/screens/Settings/General/AuthSecurityToggle.tsx b/src/screens/Settings/General/AuthSecurityToggle.tsx index cc0ab5d870..e9167bf038 100644 --- a/src/screens/Settings/General/AuthSecurityToggle.tsx +++ b/src/screens/Settings/General/AuthSecurityToggle.tsx @@ -28,7 +28,11 @@ export default function AuthSecurityToggle() { title={t("settings.display.password")} desc={t("settings.display.passwordDesc")} > - + {privacy ? : null} diff --git a/src/screens/Settings/General/PasswordForm.tsx b/src/screens/Settings/General/PasswordForm.tsx index f027fb432d..62fa622575 100644 --- a/src/screens/Settings/General/PasswordForm.tsx +++ b/src/screens/Settings/General/PasswordForm.tsx @@ -58,6 +58,7 @@ class PasswordForm extends PureComponent { secureTextEntry={secureTextEntry} placeholder={placeholder} password={value} + testID="password-text-input" /> {error && ( diff --git a/src/screens/Settings/index.tsx b/src/screens/Settings/index.tsx index 5c189ae3b8..93567efbe2 100644 --- a/src/screens/Settings/index.tsx +++ b/src/screens/Settings/index.tsx @@ -52,6 +52,7 @@ export default function Settings({ navigation }: Props) { Icon={Icons.MobileMedium} onClick={() => navigation.navigate(ScreenName.GeneralSettings)} arrowRight + settingsCardTestId="general-settings-card" /> {accounts.length > 0 && (