diff --git a/.github/workflows/test-mobile-e2e-reusable.yml b/.github/workflows/test-mobile-e2e-reusable.yml index 06bc8ff129e6..469e2b3d8b56 100644 --- a/.github/workflows/test-mobile-e2e-reusable.yml +++ b/.github/workflows/test-mobile-e2e-reusable.yml @@ -158,7 +158,7 @@ jobs: allure-report-ios: name: "Allure Reports Export on Server" runs-on: [ledger-live-medium] - if: ${{ always() && (inputs.slack_notif || github.event_name == 'push') }} + if: ${{ !inputs.speculos_tests && (inputs.slack_notif || github.event_name == 'push') }} needs: [detox-tests-ios] outputs: report-url: ${{ steps.upload.outputs.report-url }} @@ -200,7 +200,9 @@ jobs: AVD_RAM_SIZE: 4096M AVD_OPTIONS: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none outputs: - status: ${{ steps.detox.outcome }} + status_1: ${{ steps.set-output.outputs.status_1 }} + status_2: ${{ steps.set-output.outputs.status_2 }} + status_3: ${{ steps.set-output.outputs.status_3 }} strategy: fail-fast: false matrix: @@ -315,6 +317,10 @@ jobs: with: name: "android-test-artifacts-${{ matrix.shardIndex }}" path: apps/ledger-live-mobile/artifacts/ + - name: Set job output based on detox result + id: set-output + if: always() + run: echo "status_${{ matrix.shardIndex }}=${{ steps.detox.outcome }}" >> $GITHUB_OUTPUT allure-report-android: name: "Allure Reports Export on Server" @@ -323,7 +329,7 @@ jobs: outputs: report-url: ${{ steps.upload.outputs.report-url }} result: ${{ steps.summary.outputs.test_result }} - status: ${{ needs.detox-tests-android.outputs.status }} + finalStatus: ${{ steps.aggregate.outputs.finalStatus }} needs: [detox-tests-android] steps: - name: checkout @@ -344,6 +350,22 @@ jobs: with: allure-results-path: android-test-artifacts platform: android + - name: Aggregate test results + id: aggregate + run: | + if [ "${{ env.SPECULOS_RUN }}" == "true" ]; then + statuses=("${{ needs.detox-tests-android.outputs.status_1 }}" "${{ needs.detox-tests-android.outputs.status_2 }}" "${{ needs.detox-tests-android.outputs.status_3 }}") + else + statuses=("${{ needs.detox-tests-android.outputs.status_1 }}") + fi + finalStatus="success" + for status in "${statuses[@]}"; do + if [ "$status" != "success" ]; then + finalStatus="failure" + break + fi + done + echo "finalStatus=$finalStatus" >> $GITHUB_OUTPUT upload-to-xray: name: "Upload to Xray" @@ -409,7 +431,7 @@ jobs: env: IOS_STATUS: ${{ needs.allure-report-ios.outputs.status }} IOS_REPORT_URL: ${{ needs.allure-report-ios.outputs.report-url }} - ANDROID_STATUS: ${{ needs.allure-report-android.outputs.status }} + ANDROID_STATUS: ${{ needs.allure-report-android.outputs.finalStatus }} ANDROID_REPORT_URL: ${{ needs.allure-report-android.outputs.report-url }} steps: - name: format message diff --git a/apps/ledger-live-mobile/e2e/bridge/client.ts b/apps/ledger-live-mobile/e2e/bridge/client.ts index 3a8586614751..a8c1e73f9d55 100644 --- a/apps/ledger-live-mobile/e2e/bridge/client.ts +++ b/apps/ledger-live-mobile/e2e/bridge/client.ts @@ -25,9 +25,13 @@ const retryDelay = 500; // Initial retry delay in milliseconds export function init() { const wsPort = LaunchArguments.value()["wsPort"] || "8099"; const mock = LaunchArguments.value()["mock"]; + const disable_broadcast = LaunchArguments.value()["disable_broadcast"]; + log(`[E2E Bridge Client]: wsPort=${wsPort}, mock=${mock}`); if (mock == "0") setEnv("MOCK", ""); + setEnv("DISABLE_TRANSACTION_BROADCAST", disable_broadcast != "0"); + if (ws) { ws.close(); } diff --git a/apps/ledger-live-mobile/e2e/helpers.ts b/apps/ledger-live-mobile/e2e/helpers.ts index c882035473f8..aa50ece3d831 100644 --- a/apps/ledger-live-mobile/e2e/helpers.ts +++ b/apps/ledger-live-mobile/e2e/helpers.ts @@ -63,6 +63,17 @@ export function getWebElementByTag(tag: string, index = 0) { return web.element(by.web.tag(tag)).atIndex(index); } +export async function IsIdVisible(id: string | RegExp) { + try { + await waitFor(element(by.id(id))) + .toBeVisible() + .withTimeout(1000); + return true; + } catch { + return false; + } +} + export async function tapById(id: string | RegExp, index = 0) { return getElementById(id, index).tap(); } @@ -180,6 +191,9 @@ export async function launchApp() { detoxURLBlacklistRegex: '\\(".*sdk.*.braze.*",".*.googleapis.com/.*",".*clients3.google.com.*"\\)', mock: getEnv("MOCK") ? getEnv("MOCK") : "0", + disable_broadcast: getEnv("DISABLE_TRANSACTION_BROADCAST") + ? getEnv("DISABLE_TRANSACTION_BROADCAST") + : "1", }, languageAndLocale: { language: "en-US", diff --git a/apps/ledger-live-mobile/e2e/models/send.ts b/apps/ledger-live-mobile/e2e/models/send.ts new file mode 100644 index 000000000000..371523bf66e0 --- /dev/null +++ b/apps/ledger-live-mobile/e2e/models/send.ts @@ -0,0 +1,43 @@ +import { Currency } from "@ledgerhq/live-common/e2e/enum/Currency"; +import { Application } from "../page"; +import { Transaction } from "@ledgerhq/live-common/e2e/models/Transaction"; + +export async function verifyAppValidationSendInfo( + app: Application, + transaction: Transaction, + amount: string, +) { + const currenciesForValidationAmount = [ + Currency.sepETH, + Currency.DOT, + Currency.POL, + Currency.ALGO, + Currency.ADA, + Currency.DOGE, + Currency.SOL, + Currency.TRX, + Currency.XLM, + Currency.XRP, + Currency.ATOM, + Currency.BCH, + ]; + + const currenciesForValidationRecipient = [Currency.sepETH, Currency.POL]; + const currenciesForValidationSender = [Currency.ATOM]; + + const currency = transaction.accountToCredit.currency; + const addressRecipient = transaction.accountToCredit.address; + const addressSender = transaction.accountToDebit.address; + + if (currenciesForValidationAmount.includes(currency)) { + await app.send.expectValidationAmount(amount); + } + + if (currenciesForValidationRecipient.includes(currency)) { + await app.send.expectValidationAddress(addressRecipient); + } + + if (currenciesForValidationSender.includes(currency)) { + await app.send.expectValidationAddress(addressSender); + } +} diff --git a/apps/ledger-live-mobile/e2e/page/accounts/account.page.ts b/apps/ledger-live-mobile/e2e/page/accounts/account.page.ts index 66d2a749ef82..cb9e6d5e7f70 100644 --- a/apps/ledger-live-mobile/e2e/page/accounts/account.page.ts +++ b/apps/ledger-live-mobile/e2e/page/accounts/account.page.ts @@ -9,10 +9,13 @@ export default class AccountPage { accountAdvancedLogRow = () => getElementById("account-advanced-log-row"); accountDeleteRow = () => getElementById("account-settings-delete-row"); accountDeleteConfirm = () => getElementById("delete-account-confirmation-button"); - operationHistorySectionId = (accountId: string) => `operations-history-${accountId}`; + operationHistorySection = "operations-history-"; + operationHistorySectionRegexp = new RegExp(this.operationHistorySection + ".*"); + operationHistorySectionId = (accountId: string) => this.operationHistorySection + accountId; accountScreenScrollView = "account-screen-scrollView"; accountAdvancedLogsId = "account-advanced-logs"; receiveButton = () => getElementById("account-quick-action-button-Receive"); + sendButton = () => getElementById("account-quick-action-button-Send"); @Step("Open account settings") async openAccountSettings() { @@ -41,6 +44,11 @@ export default class AccountPage { await expect(getElementById(id)).toBeVisible(); } + @Step("Scroll to transaction history") + async scrollToTransactions() { + await scrollToId(this.operationHistorySectionRegexp, this.accountScreenScrollView); + } + @Step("Expect account balance to be visible") async expectAccountBalanceVisible(accountId: string) { await expect(this.accountGraph(accountId)).toBeVisible(); @@ -60,4 +68,9 @@ export default class AccountPage { async tapReceive() { await tapByElement(this.receiveButton()); } + + @Step("Tap on send button") + async tapSend() { + await tapByElement(this.sendButton()); + } } diff --git a/apps/ledger-live-mobile/e2e/page/common.page.ts b/apps/ledger-live-mobile/e2e/page/common.page.ts index 50fc13af7945..3eef14bb8bd1 100644 --- a/apps/ledger-live-mobile/e2e/page/common.page.ts +++ b/apps/ledger-live-mobile/e2e/page/common.page.ts @@ -21,6 +21,7 @@ export default class CommonPage { searchBarId = "common-search-field"; searchBar = () => getElementById(this.searchBarId); successCloseButtonId = "success-close-button"; + successViewDetailsButtonId = "success-view-details-button"; closeButton = () => getElementById("NavigationHeaderCloseButton"); accoundCardId = (id: string) => "account-card-" + id; @@ -53,6 +54,12 @@ export default class CommonPage { await tapById(this.successCloseButtonId); } + @Step("Tap on view details") + async successViewDetails() { + await waitForElementById(this.successViewDetailsButtonId); + await tapById(this.successViewDetailsButtonId); + } + async selectAccount(accountId: string) { const id = this.accoundCardId(accountId); await waitForElementById(id); diff --git a/apps/ledger-live-mobile/e2e/page/index.ts b/apps/ledger-live-mobile/e2e/page/index.ts index 428c72bd49cc..dad053ccff10 100644 --- a/apps/ledger-live-mobile/e2e/page/index.ts +++ b/apps/ledger-live-mobile/e2e/page/index.ts @@ -82,10 +82,10 @@ export class Application { public transfertMenu = new TransfertMenuDrawer(); public walletTabNavigator = new WalletTabNavigatorPage(); - constructor() { + constructor(userdata?: string) { if (!getEnv("MOCK")) { // Create a temporary userdata file for Speculos tests - const originalUserdata = "skip-onboarding"; + const originalUserdata = userdata || "skip-onboarding"; this.userdataSpeculos = `temp-userdata-${Date.now()}`; this.userdataPath = getUserdataPath(this.userdataSpeculos); const originalFilePath = getUserdataPath(originalUserdata); diff --git a/apps/ledger-live-mobile/e2e/page/speculos.page.ts b/apps/ledger-live-mobile/e2e/page/speculos.page.ts index 7dbdb37596fc..3525ebac220f 100644 --- a/apps/ledger-live-mobile/e2e/page/speculos.page.ts +++ b/apps/ledger-live-mobile/e2e/page/speculos.page.ts @@ -1,9 +1,15 @@ -import { expectValidAddressDevice } from "@ledgerhq/live-common/e2e/speculos"; +import { expectValidAddressDevice, signSendTransaction } from "@ledgerhq/live-common/e2e/speculos"; import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { Transaction } from "@ledgerhq/live-common/e2e/models/Transaction"; export default class SpeculosPage { @Step("Verify receive address correctness on device") async expectValidAddressDevice(account: Account, addressDisplayed: string) { await expectValidAddressDevice(account, addressDisplayed); } + + @Step("Sign Send Transaction") + async signSendTransaction(tx: Transaction) { + await signSendTransaction(tx); + } } diff --git a/apps/ledger-live-mobile/e2e/page/trade/operationDetails.page.ts b/apps/ledger-live-mobile/e2e/page/trade/operationDetails.page.ts index 4b0db462e5ad..078a5aaa5b5a 100644 --- a/apps/ledger-live-mobile/e2e/page/trade/operationDetails.page.ts +++ b/apps/ledger-live-mobile/e2e/page/trade/operationDetails.page.ts @@ -1,15 +1,23 @@ -import { getElementById } from "../../helpers"; +import { getElementById, scrollToId, waitForElementById } from "../../helpers"; import { expect } from "detox"; export default class OperationDetailsPage { - title = () => getElementById("operationDetails-title"); + titleId = "operationDetails-title"; + title = () => getElementById(this.titleId); account = () => getElementById("operationDetails-account"); amount = () => getElementById("operationDetails-amount"); + recipientId = "operationDetails-recipient0"; async isOpened() { await expect(this.title()).toBeVisible(); } + @Step("Wait for operation details") + async waitForOperationDetails() { + await waitForElementById(this.titleId); + } + + @Step("Check account details") async checkAccount(account: string) { await expect(this.account()).toHaveText(account); } @@ -17,4 +25,10 @@ export default class OperationDetailsPage { async checkAmount(amount: string) { await expect(this.amount()).toHaveText(amount); } + + @Step("Check recipient details") + async checkRecipient(recipient: string) { + await scrollToId(this.recipientId); + await expect(getElementById(this.recipientId)).toHaveText(recipient); + } } diff --git a/apps/ledger-live-mobile/e2e/page/trade/send.page.ts b/apps/ledger-live-mobile/e2e/page/trade/send.page.ts index 86f2df633d1f..053a67f95e4d 100644 --- a/apps/ledger-live-mobile/e2e/page/trade/send.page.ts +++ b/apps/ledger-live-mobile/e2e/page/trade/send.page.ts @@ -6,6 +6,7 @@ import { openDeeplink, typeTextById, tapByElement, + IsIdVisible, } from "../../helpers"; import { expect } from "detox"; @@ -13,12 +14,16 @@ const baseLink = "send"; export default class SendPage { summaryAmount = () => getElementById("send-summary-amount"); + summaryRecipient = () => getElementById("send-summary-recipient"); + validationAmountId = "send-validation-amount"; + validationAddressId = "send-validation-address"; getStep1HeaderTitle = () => getElementById("send-header-step1-title"); recipientContinueButtonId = "recipient-continue-button"; recipientInputId = "recipient-input"; amountInputId = "amount-input"; amountContinueButton = () => getElementById("amount-continue-button"); summaryContinueButton = () => getElementById("summary-continue-button"); + highFreeConfirmButtonID = "confirmation-modal-confirm-button"; async openViaDeeplink() { await openDeeplink(baseLink); @@ -42,6 +47,12 @@ export default class SendPage { await tapById(this.recipientContinueButtonId); } + @Step("Set recipient and continue") + async setRecipientAndContinue(address: string) { + await this.setRecipient(address); + await this.recipientContinue(); + } + async setAmount(amount: string) { const element = getElementById(this.amountInputId); await element.replaceText(amount); @@ -52,11 +63,41 @@ export default class SendPage { await tapByElement(this.amountContinueButton()); } + @Step("Set amount and continue") + async setAmountAndContinue(amount: string) { + await this.setAmount(amount); + await this.amountContinue(); + } + async summaryContinue() { await tapByElement(this.summaryContinueButton()); } + @Step("Expect amount in summary") async expectSummaryAmount(amount: string) { await expect(this.summaryAmount()).toHaveText(amount); } + + @Step("Expect recipient in summary") + async expectSummaryRecepient(recipient: string) { + await expect(this.summaryRecipient()).toHaveText(recipient); + } + + @Step("Dismiss high fee modal if visible") + async dismissHighFeeModal() { + if (await IsIdVisible(this.highFreeConfirmButtonID)) + await tapById(this.highFreeConfirmButtonID); + } + + @Step("Expect amount in device validation screen") + async expectValidationAmount(amount: string) { + await waitForElementById(this.validationAmountId); + await expect(getElementById(this.validationAmountId)).toHaveText(amount); + } + + @Step("Expect address in device validation screen") + async expectValidationAddress(recipient: string) { + await waitForElementById(this.validationAddressId); + await expect(getElementById(this.validationAddressId)).toHaveText(recipient); + } } diff --git a/apps/ledger-live-mobile/e2e/specs/speculos/send/send.ts b/apps/ledger-live-mobile/e2e/specs/speculos/send/send.ts new file mode 100644 index 000000000000..9064e49ffa00 --- /dev/null +++ b/apps/ledger-live-mobile/e2e/specs/speculos/send/send.ts @@ -0,0 +1,65 @@ +import { verifyAppValidationSendInfo } from "../../../models/send"; +import { Application } from "../../../page"; +import { CLI } from "../../../utils/cliUtils"; +import { Transaction } from "@ledgerhq/live-common/e2e/models/Transaction"; +import { device } from "detox"; + +export async function runSendTest(transaction: Transaction, tmsLink: string) { + const app = new Application(); + + $TmsLink(tmsLink); + describe(`Send flow on ${transaction.accountToCredit.currency.name}`, () => { + beforeAll(async () => { + await app.init({ + speculosApp: transaction.accountToCredit.currency.speculosApp, + cliCommands: [ + () => { + return CLI.liveData({ + currency: transaction.accountToCredit.currency.currencyId, + index: transaction.accountToCredit.index, + add: true, + appjson: app.userdataPath, + }); + }, + () => { + return CLI.liveData({ + currency: transaction.accountToDebit.currency.currencyId, + index: transaction.accountToDebit.index, + add: true, + appjson: app.userdataPath, + }); + }, + ], + }); + + await app.portfolio.waitForPortfolioPageToLoad(); + }); + + it(`Send from 1 account to another on ${transaction.accountToCredit.currency.name}`, async () => { + await app.accounts.openViaDeeplink(); + await app.common.goToAccountByName(transaction.accountToDebit.accountName); + await app.account.tapSend(); + + const amountWithCode = transaction.amount + " " + transaction.accountToCredit.currency.ticker; + + await app.send.setRecipientAndContinue(transaction.accountToCredit.address); + await app.send.setAmountAndContinue(transaction.amount); + + await app.send.expectSummaryAmount(amountWithCode); + await app.send.expectSummaryRecepient(transaction.accountToCredit.address); + await app.send.summaryContinue(); + await app.send.dismissHighFeeModal(); + + await verifyAppValidationSendInfo(app, transaction, amountWithCode); + + await app.speculos.signSendTransaction(transaction); + + await device.disableSynchronization(); + await app.common.successViewDetails(); + + await app.operationDetails.waitForOperationDetails(); + await app.operationDetails.checkAccount(transaction.accountToDebit.accountName); + await app.operationDetails.checkRecipient(transaction.accountToCredit.address); + }); + }); +} diff --git a/apps/ledger-live-mobile/e2e/specs/speculos/send/sendADA.spec.ts b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendADA.spec.ts new file mode 100644 index 000000000000..0d8962efd476 --- /dev/null +++ b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendADA.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { Transaction } from "@ledgerhq/live-common/e2e/models/Transaction"; +import { runSendTest } from "../send/send"; + +const transaction = new Transaction(Account.ADA_1, Account.ADA_2, "1"); +runSendTest(transaction, "B2CQA-2815"); diff --git a/apps/ledger-live-mobile/e2e/specs/speculos/send/sendALGO.spec.ts b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendALGO.spec.ts new file mode 100644 index 000000000000..6e361e5b576d --- /dev/null +++ b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendALGO.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { Transaction } from "@ledgerhq/live-common/e2e/models/Transaction"; +import { runSendTest } from "../send/send"; + +const transaction = new Transaction(Account.ALGO_1, Account.ALGO_2, "0.001"); +runSendTest(transaction, "B2CQA-2810"); diff --git a/apps/ledger-live-mobile/e2e/specs/speculos/send/sendATOM.spec.ts b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendATOM.spec.ts new file mode 100644 index 000000000000..beb0868a3dee --- /dev/null +++ b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendATOM.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { Transaction } from "@ledgerhq/live-common/e2e/models/Transaction"; +import { runSendTest } from "../send/send"; + +const transaction = new Transaction(Account.ATOM_1, Account.ATOM_2, "0.0001"); +runSendTest(transaction, "B2CQA-2814"); diff --git a/apps/ledger-live-mobile/e2e/specs/speculos/send/sendBCH.spec.ts b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendBCH.spec.ts new file mode 100644 index 000000000000..a06e50efff40 --- /dev/null +++ b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendBCH.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { Transaction } from "@ledgerhq/live-common/e2e/models/Transaction"; +import { runSendTest } from "../send/send"; + +const transaction = new Transaction(Account.BCH_1, Account.BCH_2, "0.0001"); +runSendTest(transaction, "B2CQA-2808"); diff --git a/apps/ledger-live-mobile/e2e/specs/speculos/send/sendDOGE.spec.ts b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendDOGE.spec.ts new file mode 100644 index 000000000000..bf5db5cfac9a --- /dev/null +++ b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendDOGE.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { Transaction } from "@ledgerhq/live-common/e2e/models/Transaction"; +import { runSendTest } from "../send/send"; + +const transaction = new Transaction(Account.DOGE_1, Account.DOGE_2, "0.01"); +runSendTest(transaction, "B2CQA-2573"); diff --git a/apps/ledger-live-mobile/e2e/specs/speculos/send/sendDOT.spec.ts b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendDOT.spec.ts new file mode 100644 index 000000000000..9b025c7332b4 --- /dev/null +++ b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendDOT.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { Transaction } from "@ledgerhq/live-common/e2e/models/Transaction"; +import { runSendTest } from "../send/send"; + +const transaction = new Transaction(Account.DOT_1, Account.DOT_2, "0.0001"); +runSendTest(transaction, "B2CQA-2809"); diff --git a/apps/ledger-live-mobile/e2e/specs/speculos/send/sendPOL.spec.ts b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendPOL.spec.ts new file mode 100644 index 000000000000..22231f33c41f --- /dev/null +++ b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendPOL.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { Transaction } from "@ledgerhq/live-common/e2e/models/Transaction"; +import { runSendTest } from "../send/send"; + +const transaction = new Transaction(Account.POL_1, Account.POL_2, "0.001"); +runSendTest(transaction, "B2CQA-2807"); diff --git a/apps/ledger-live-mobile/e2e/specs/speculos/send/sendSOL.spec.ts b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendSOL.spec.ts new file mode 100644 index 000000000000..4e1ec0fc09dc --- /dev/null +++ b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendSOL.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { Transaction } from "@ledgerhq/live-common/e2e/models/Transaction"; +import { runSendTest } from "../send/send"; + +const transaction = new Transaction(Account.SOL_1, Account.SOL_2, "0.000001"); +runSendTest(transaction, "B2CQA-2811"); diff --git a/apps/ledger-live-mobile/e2e/specs/speculos/send/sendSepETH.spec.ts b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendSepETH.spec.ts new file mode 100644 index 000000000000..55d4c6ad357e --- /dev/null +++ b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendSepETH.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { Transaction } from "@ledgerhq/live-common/e2e/models/Transaction"; +import { runSendTest } from "../send/send"; + +const transaction = new Transaction(Account.sep_ETH_1, Account.sep_ETH_2, "0.00001"); +runSendTest(transaction, "B2CQA-2574"); diff --git a/apps/ledger-live-mobile/e2e/specs/speculos/send/sendTRX.spec.ts b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendTRX.spec.ts new file mode 100644 index 000000000000..59cb2300d7f8 --- /dev/null +++ b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendTRX.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { Transaction } from "@ledgerhq/live-common/e2e/models/Transaction"; +import { runSendTest } from "../send/send"; + +const transaction = new Transaction(Account.TRX_1, Account.TRX_2, "0.01"); +runSendTest(transaction, "B2CQA-2812"); diff --git a/apps/ledger-live-mobile/e2e/specs/speculos/send/sendXLM.spec.ts b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendXLM.spec.ts new file mode 100644 index 000000000000..5ddb612c8673 --- /dev/null +++ b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendXLM.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { Transaction } from "@ledgerhq/live-common/e2e/models/Transaction"; +import { runSendTest } from "../send/send"; + +const transaction = new Transaction(Account.XLM_1, Account.XLM_2, "0.0001"); +runSendTest(transaction, "B2CQA-2813"); diff --git a/apps/ledger-live-mobile/e2e/specs/speculos/send/sendXRP.spec.ts b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendXRP.spec.ts new file mode 100644 index 000000000000..40a6c9474c2f --- /dev/null +++ b/apps/ledger-live-mobile/e2e/specs/speculos/send/sendXRP.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { Transaction } from "@ledgerhq/live-common/e2e/models/Transaction"; +import { runSendTest } from "../send/send"; + +const transaction = new Transaction(Account.XRP_1, Account.XRP_2, "0.0001"); +runSendTest(transaction, "B2CQA-2816"); diff --git a/apps/ledger-live-mobile/e2e/specs/speculos/verifyAddress/receiveFlowETH.spec.ts b/apps/ledger-live-mobile/e2e/specs/speculos/verifyAddress/receiveFlowETH.spec.ts index c74e26df13a2..fb5154a2a0ce 100644 --- a/apps/ledger-live-mobile/e2e/specs/speculos/verifyAddress/receiveFlowETH.spec.ts +++ b/apps/ledger-live-mobile/e2e/specs/speculos/verifyAddress/receiveFlowETH.spec.ts @@ -1,12 +1,11 @@ import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; import { Application } from "../../../page"; -const app = new Application(); +const app = new Application("EthAccountXrpAccountReadOnlyFalse"); const account = Account.ETH_1; describe("Receive Flow", () => { beforeAll(async () => { - app.userdataSpeculos = "EthAccountXrpAccountReadOnlyFalse"; await app.init({ speculosApp: account.currency.speculosApp, }); diff --git a/apps/ledger-live-mobile/package.json b/apps/ledger-live-mobile/package.json index eec7ea717db5..8a3a4c562015 100644 --- a/apps/ledger-live-mobile/package.json +++ b/apps/ledger-live-mobile/package.json @@ -20,7 +20,7 @@ "e2e:build": "pnpm detox build", "e2e:ci": "zx ./scripts/e2e-ci.mjs", "e2e:test": "export MOCK=1 && pnpm detox test", - "e2e:test:speculos": "export MOCK=0 && pnpm detox test --testMatch $(pwd)/e2e/specs/speculos/**/**/*.spec.ts --testTimeout=300000", + "e2e:test:speculos": "export MOCK=0 && pnpm detox test --testMatch \"$(pwd)/e2e/specs/speculos/**/*.spec.ts\" --testTimeout=300000", "prebeta": "bundle install", "debug:detox": "pnpm detox test -c ios.manual currencies.spec.ts", "ios:staging": "ENVFILE=.env.ios.staging react-native run-ios --mode Staging", diff --git a/apps/ledger-live-mobile/src/components/ConfirmationModal.tsx b/apps/ledger-live-mobile/src/components/ConfirmationModal.tsx index 5d13fc67fb27..563b0322d8d7 100644 --- a/apps/ledger-live-mobile/src/components/ConfirmationModal.tsx +++ b/apps/ledger-live-mobile/src/components/ConfirmationModal.tsx @@ -24,6 +24,7 @@ type Props = { colors: Theme["colors"]; preventBackdropClick?: boolean; iconMarginBottom?: number; + testID?: string; }; class ConfirmationModal extends PureComponent { @@ -92,6 +93,7 @@ class ConfirmationModal extends PureComponent { containerStyle={styles.confirmationButton} type="secondary" title={rejectButtonText || } + testID="confirmation-modal-cancel-button" onPress={onClose} /> )} @@ -101,6 +103,7 @@ class ConfirmationModal extends PureComponent { containerStyle={[styles.confirmationButton, styles.confirmationLastButton]} type={alert ? "alert" : "primary"} title={confirmButtonText || } + testID="confirmation-modal-confirm-button" onPress={onConfirm} /> diff --git a/apps/ledger-live-mobile/src/components/ValidateOnDevice.tsx b/apps/ledger-live-mobile/src/components/ValidateOnDevice.tsx index 9736c734cb93..44b79de4f3a2 100644 --- a/apps/ledger-live-mobile/src/components/ValidateOnDevice.tsx +++ b/apps/ledger-live-mobile/src/components/ValidateOnDevice.tsx @@ -44,7 +44,14 @@ const AnimationContainer = styled(Flex).attrs({ function AmountField({ account, status, field }: FieldComponentProps) { const unit = useAccountUnit(account); - return ; + return ( + + ); } function FeesField({ account, parentAccount, status, field }: FieldComponentProps) { @@ -57,7 +64,9 @@ function FeesField({ account, parentAccount, status, field }: FieldComponentProp function AddressField({ field }: FieldComponentProps) { invariant(field.type === "address", "AddressField invalid"); - return ; + return ( + + ); } // NB Leaving AddressField although I think it's redundant at this point diff --git a/apps/ledger-live-mobile/src/components/ValidateOnDeviceDataRow.tsx b/apps/ledger-live-mobile/src/components/ValidateOnDeviceDataRow.tsx index 93e8462d4e11..08023a5480e7 100644 --- a/apps/ledger-live-mobile/src/components/ValidateOnDeviceDataRow.tsx +++ b/apps/ledger-live-mobile/src/components/ValidateOnDeviceDataRow.tsx @@ -77,10 +77,12 @@ export function TextValueField({ label, numberOfLines, value, + testID, }: { label: React.ReactNode; numberOfLines?: number; value: React.ReactNode; + testID?: string; }) { const { colors } = useTheme(); return ( @@ -95,7 +97,7 @@ export function TextValueField({ {label} - + {value} @@ -177,12 +179,13 @@ type Props = { label: React.ReactNode; value: BigNumber; unit: Unit; + testID?: string; }; -export const DataRowUnitValue = ({ label, value, unit }: Props) => { +export const DataRowUnitValue = ({ label, value, unit, testID }: Props) => { return ( - + diff --git a/apps/ledger-live-mobile/src/components/ValidateSuccess.tsx b/apps/ledger-live-mobile/src/components/ValidateSuccess.tsx index 54d99bfef8e3..da715c0c31e8 100644 --- a/apps/ledger-live-mobile/src/components/ValidateSuccess.tsx +++ b/apps/ledger-live-mobile/src/components/ValidateSuccess.tsx @@ -69,6 +69,7 @@ function ValidateSuccess({ outline={false} onPress={onViewDetails} mt={7} + testID="success-view-details-button" > diff --git a/apps/ledger-live-mobile/src/screens/OperationDetails/Content.tsx b/apps/ledger-live-mobile/src/screens/OperationDetails/Content.tsx index be3336956fa2..d605a44db92c 100644 --- a/apps/ledger-live-mobile/src/screens/OperationDetails/Content.tsx +++ b/apps/ledger-live-mobile/src/screens/OperationDetails/Content.tsx @@ -417,6 +417,7 @@ export default function Content({ } + testID="operationDetails-recipient" rightComp={ uniqueRecipients.length > 1 ? ( { +const DataList = ({ data, title, rightComp, colors, testID }: Props) => { const [showAll, setShowAll] = useState(false); const toggleShowAll = () => { @@ -61,14 +62,14 @@ const DataList = ({ data, title, rightComp, colors }: Props) => { )} - {(shouldShowMore ? data.slice(0, numToShow) : data).map(line => ( - + {(shouldShowMore ? data.slice(0, numToShow) : data).map((line, index) => ( + {line} ))} {showAll && - data.slice(numToShow).map(line => ( - + data.slice(numToShow).map((line, index) => ( + {line} ))} diff --git a/apps/ledger-live-mobile/src/screens/SendFunds/SummaryToSection.tsx b/apps/ledger-live-mobile/src/screens/SendFunds/SummaryToSection.tsx index 15e0163b134f..63c9d66b8f6f 100644 --- a/apps/ledger-live-mobile/src/screens/SendFunds/SummaryToSection.tsx +++ b/apps/ledger-live-mobile/src/screens/SendFunds/SummaryToSection.tsx @@ -30,6 +30,7 @@ const DefaultRecipientTemplate = memo(({ transaction }: Pick {recipient} @@ -56,6 +57,7 @@ const RecipientWithResolutionTemplate = memo(({ transaction }: Pick {recipient} diff --git a/libs/coin-framework/src/transaction/common.ts b/libs/coin-framework/src/transaction/common.ts index 33bf6b1d62db..4a1fa0bcf663 100644 --- a/libs/coin-framework/src/transaction/common.ts +++ b/libs/coin-framework/src/transaction/common.ts @@ -3,6 +3,7 @@ export type CommonDeviceTransactionField = | { type: "amount"; label: string; + value?: string; } | { type: "address"; diff --git a/libs/coin-modules/coin-algorand/src/deviceTransactionConfig.ts b/libs/coin-modules/coin-algorand/src/deviceTransactionConfig.ts index c203e5b7f214..8d844e9a3665 100644 --- a/libs/coin-modules/coin-algorand/src/deviceTransactionConfig.ts +++ b/libs/coin-modules/coin-algorand/src/deviceTransactionConfig.ts @@ -59,8 +59,8 @@ const getSendFields = ( if (amount) { fields.push({ - type: "text", label: account.type === "TokenAccount" ? "Asset amt" : "Amount", + type: "amount", value: formatCurrencyUnit(getAccountCurrency(account).units[0], amount, { showCode: true, disableRounding: true, diff --git a/libs/coin-modules/coin-cardano/src/deviceTransactionConfig.ts b/libs/coin-modules/coin-cardano/src/deviceTransactionConfig.ts index 84775707925e..8c910f2f15b9 100644 --- a/libs/coin-modules/coin-cardano/src/deviceTransactionConfig.ts +++ b/libs/coin-modules/coin-cardano/src/deviceTransactionConfig.ts @@ -82,7 +82,7 @@ function getDeviceTransactionConfig({ value: decodeTokenName(assetName), }); fields.push({ - type: "text", + type: "amount", label: "Amount", value: formatCurrencyUnit(getAccountCurrency(account).units[0], transactionAmount, { showCode: true, @@ -91,7 +91,7 @@ function getDeviceTransactionConfig({ }); } else if (account.type === "Account") { fields.push({ - type: "text", + type: "amount", label: "Amount", value: formatCurrencyUnit(getAccountCurrency(account).units[0], transaction.amount, { showCode: true, diff --git a/libs/coin-modules/coin-cosmos/src/deviceTransactionConfig.ts b/libs/coin-modules/coin-cosmos/src/deviceTransactionConfig.ts index 0bc470f1c91d..7b11710289c4 100644 --- a/libs/coin-modules/coin-cosmos/src/deviceTransactionConfig.ts +++ b/libs/coin-modules/coin-cosmos/src/deviceTransactionConfig.ts @@ -47,7 +47,7 @@ const getSendFields = ({ if (!amount.isZero()) { fields.push({ - type: "text", + type: "amount", label: "Amount", value: formatCurrencyUnit(getAccountCurrency(account).units[0], amount, { showCode: true, diff --git a/libs/coin-modules/coin-polkadot/src/bridge/deviceTransactionConfig.ts b/libs/coin-modules/coin-polkadot/src/bridge/deviceTransactionConfig.ts index fc43e672da40..e37298a2fb06 100644 --- a/libs/coin-modules/coin-polkadot/src/bridge/deviceTransactionConfig.ts +++ b/libs/coin-modules/coin-polkadot/src/bridge/deviceTransactionConfig.ts @@ -26,7 +26,7 @@ const getSendFields = ({ value: transaction && transaction.useAllAmount ? "Transfer" : "Transfer keep alive", }); fields.push({ - type: "text", + type: "amount", label: "Amount", value: formatCurrencyUnit(currency.units[0], amount, { showCode: true, diff --git a/libs/ledger-live-common/src/e2e/speculos.ts b/libs/ledger-live-common/src/e2e/speculos.ts index 71b68ffad5e7..ceda0854cfbf 100644 --- a/libs/ledger-live-common/src/e2e/speculos.ts +++ b/libs/ledger-live-common/src/e2e/speculos.ts @@ -369,7 +369,7 @@ export async function waitFor(text: string, maxAttempts: number = 10): Promise event.text); - if (texts[0].includes(text)) { + if (texts?.[0]?.includes(text)) { textFound = true; return texts; } diff --git a/patches/detox@20.28.0.patch b/patches/detox@20.28.0.patch index 6e19883a3ead..ac4f1ae2a2ee 100644 --- a/patches/detox@20.28.0.patch +++ b/patches/detox@20.28.0.patch @@ -1,5 +1,43 @@ +diff --git a/local-cli/testCommand/TestRunnerCommand.js b/local-cli/testCommand/TestRunnerCommand.js +index afdc985d5a55396fbec0affd0927156348e656d0..1c71bce08e05768ec2f92245316154ecd0f3445f 100644 +--- a/local-cli/testCommand/TestRunnerCommand.js ++++ b/local-cli/testCommand/TestRunnerCommand.js +@@ -35,6 +35,7 @@ class TestRunnerCommand { + this._startCommands = this._prepareStartCommands(commands, cliConfig); + this._envFwd = {}; + this._terminating = false; ++ this._isRetry = false; // Track whether it's a retry + + if (runnerConfig.forwardEnv) { + this._envFwd = this._buildEnvOverride(cliConfig, deviceConfig); +@@ -79,6 +80,7 @@ class TestRunnerCommand { + } + + if (--runsLeft > 0) { ++ this._isRetry = true; + // @ts-ignore + detox.session.testSessionIndex++; // it is always the primary context, so we can update it + +@@ -204,11 +206,16 @@ class TestRunnerCommand { + /* istanbul ignore next */ + const { _: specs = [], '--': passthrough = [], $0, ...argv } = this._argv; + const { _: $0_, ...$0argv } = parser($0); ++ ++ // Remove shard arguments for retries ++ const filteredArgv = this._isRetry ++ ? _.omit(argv, ['shard']) ++ : argv; + + return [ + ...$0_, + ...unparse($0argv), +- ...unparse(argv), ++ ...unparse(filteredArgv), + ...unparse({ _: [...passthrough, ...specs] }), + ].map(String); + } diff --git a/src/devices/common/drivers/android/exec/ADB.js b/src/devices/common/drivers/android/exec/ADB.js -index 3600972facda80dd0b74d6aa6e425ecab1d205ba..0dd7a4b59cb0bc4d4189f32d819c244ea5f66ba1 100644 +index 434b0d65c89f8086084c8714c98e446c9bf54c3a..3b83753d3a3bd5468b53c4b0555bd278e301dbab 100644 --- a/src/devices/common/drivers/android/exec/ADB.js +++ b/src/devices/common/drivers/android/exec/ADB.js @@ -8,7 +8,10 @@ const { escape } = require('../../../../../utils/pipeCommands'); @@ -14,7 +52,7 @@ index 3600972facda80dd0b74d6aa6e425ecab1d205ba..0dd7a4b59cb0bc4d4189f32d819c244e class ADB { constructor() { -@@ -109,7 +112,7 @@ class ADB { +@@ -113,7 +116,7 @@ class ADB { const command = (apiLvl >= 23) ? `install -r -g -t ${apkPath}` : `install -rg ${apkPath}`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40e07b3fd28e..2e3332ab122b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,7 +32,7 @@ patchedDependencies: hash: 2xnca52oxhztvr7iaoovwclcze path: patches/buffer@6.0.3.patch detox@20.28.0: - hash: yanhspfjw3apvvf4gev5ovygia + hash: bvmee6puznwczhjmzfxagey6x4 path: patches/detox@20.28.0.patch react-native-fast-crypto@2.2.0: hash: bmot5xcuajv3pdpobflt4ufvae @@ -1454,10 +1454,10 @@ importers: version: 2.8.5 detox: specifier: 20.28.0 - version: 20.28.0(patch_hash=yanhspfjw3apvvf4gev5ovygia)(jest@29.7.0(ts-node@10.9.2(@swc/core@1.4.11)(typescript@5.4.3))) + version: 20.28.0(patch_hash=bvmee6puznwczhjmzfxagey6x4)(jest@29.7.0(ts-node@10.9.2(@swc/core@1.4.11)(typescript@5.4.3))) detox-allure2-adapter: specifier: 1.0.0-alpha.12 - version: 1.0.0-alpha.12(detox@20.28.0(patch_hash=yanhspfjw3apvvf4gev5ovygia)(jest@29.7.0(ts-node@10.9.2(@swc/core@1.4.11)(typescript@5.4.3))))(jest-allure2-reporter@2.0.0(@jest/reporters@29.7.0)(jest-docblock@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(ts-node@10.9.2(@swc/core@1.4.11)(typescript@5.4.3)))) + version: 1.0.0-alpha.12(detox@20.28.0(patch_hash=bvmee6puznwczhjmzfxagey6x4)(jest@29.7.0(ts-node@10.9.2(@swc/core@1.4.11)(typescript@5.4.3))))(jest-allure2-reporter@2.0.0(@jest/reporters@29.7.0)(jest-docblock@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(ts-node@10.9.2(@swc/core@1.4.11)(typescript@5.4.3)))) eslint-import-resolver-typescript: specifier: 3.6.1 version: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) @@ -49525,16 +49525,16 @@ snapshots: transitivePeerDependencies: - supports-color - detox-allure2-adapter@1.0.0-alpha.12(detox@20.28.0(patch_hash=yanhspfjw3apvvf4gev5ovygia)(jest@29.7.0(ts-node@10.9.2(@swc/core@1.4.11)(typescript@5.4.3))))(jest-allure2-reporter@2.0.0(@jest/reporters@29.7.0)(jest-docblock@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(ts-node@10.9.2(@swc/core@1.4.11)(typescript@5.4.3)))): + detox-allure2-adapter@1.0.0-alpha.12(detox@20.28.0(patch_hash=bvmee6puznwczhjmzfxagey6x4)(jest@29.7.0(ts-node@10.9.2(@swc/core@1.4.11)(typescript@5.4.3))))(jest-allure2-reporter@2.0.0(@jest/reporters@29.7.0)(jest-docblock@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(ts-node@10.9.2(@swc/core@1.4.11)(typescript@5.4.3)))): dependencies: archiver: 6.0.2 - detox: 20.28.0(patch_hash=yanhspfjw3apvvf4gev5ovygia)(jest@29.7.0(ts-node@10.9.2(@swc/core@1.4.11)(typescript@5.4.3))) + detox: 20.28.0(patch_hash=bvmee6puznwczhjmzfxagey6x4)(jest@29.7.0(ts-node@10.9.2(@swc/core@1.4.11)(typescript@5.4.3))) jest-allure2-reporter: 2.0.0(@jest/reporters@29.7.0)(jest-docblock@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(ts-node@10.9.2(@swc/core@1.4.11)(typescript@5.4.3))) tslib: 2.6.2 detox-copilot@0.0.24: {} - detox@20.28.0(patch_hash=yanhspfjw3apvvf4gev5ovygia)(jest@29.7.0(ts-node@10.9.2(@swc/core@1.4.11)(typescript@5.4.3))): + detox@20.28.0(patch_hash=bvmee6puznwczhjmzfxagey6x4)(jest@29.7.0(ts-node@10.9.2(@swc/core@1.4.11)(typescript@5.4.3))): dependencies: '@jest/reporters': 29.7.0 ajv: 8.17.1