From cd742af9b16ad84c3a8ee779613b634deb2662d5 Mon Sep 17 00:00:00 2001 From: Aleksei Maslakov Date: Tue, 23 Jun 2026 23:50:04 +0100 Subject: [PATCH 1/2] test(tezos): add mobile staking and unstaking e2e Five Detox specs (earning-choice, stake, unstake, change-validator and end-delegation blocked) plus the staking-section row test ids they need. --- .changeset/gentle-mangos-retire.md | 5 + .../src/families/tezos/Delegations/Row.tsx | 4 +- .../tezos/Delegations/UnstakingRow.tsx | 1 + .../src/families/tezos/Delegations/index.tsx | 1 + e2e/mobile/page/index.ts | 6 + e2e/mobile/page/trade/tezosStake.page.ts | 102 ++++++++++++ .../stake/changeValidatorBlockedTEZOS.spec.ts | 6 + .../specs/stake/earnChoiceTEZOS.spec.ts | 6 + .../stake/endDelegationBlockedTEZOS.spec.ts | 6 + e2e/mobile/specs/stake/stake.ts | 148 ++++++++++++++++++ e2e/mobile/specs/stake/stakeTEZOS.spec.ts | 6 + e2e/mobile/specs/stake/unstakeTEZOS.spec.ts | 6 + 12 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 .changeset/gentle-mangos-retire.md create mode 100644 e2e/mobile/page/trade/tezosStake.page.ts create mode 100644 e2e/mobile/specs/stake/changeValidatorBlockedTEZOS.spec.ts create mode 100644 e2e/mobile/specs/stake/earnChoiceTEZOS.spec.ts create mode 100644 e2e/mobile/specs/stake/endDelegationBlockedTEZOS.spec.ts create mode 100644 e2e/mobile/specs/stake/stake.ts create mode 100644 e2e/mobile/specs/stake/stakeTEZOS.spec.ts create mode 100644 e2e/mobile/specs/stake/unstakeTEZOS.spec.ts diff --git a/.changeset/gentle-mangos-retire.md b/.changeset/gentle-mangos-retire.md new file mode 100644 index 000000000000..1e3d50807ac5 --- /dev/null +++ b/.changeset/gentle-mangos-retire.md @@ -0,0 +1,5 @@ +--- +"live-mobile": patch +--- + +Add test ids to the Tezos staking-section rows (staked and unstaking) so e2e can target them distinctly from the delegation card diff --git a/apps/ledger-live-mobile/src/families/tezos/Delegations/Row.tsx b/apps/ledger-live-mobile/src/families/tezos/Delegations/Row.tsx index bfebe83692b7..80d3e2e6280c 100644 --- a/apps/ledger-live-mobile/src/families/tezos/Delegations/Row.tsx +++ b/apps/ledger-live-mobile/src/families/tezos/Delegations/Row.tsx @@ -22,6 +22,7 @@ type Props = Readonly<{ onPress: () => void; isLast?: boolean; statusLabel?: string; + testID?: string; }>; export default function DelegationRow({ @@ -33,13 +34,14 @@ export default function DelegationRow({ onPress, isLast = false, statusLabel, + testID = "tezos-delegation-row", }: Props) { const { colors } = useTheme(); const { t } = useTranslation(); return ( {info.isStaked && ( `enabled-${id}`; + +export default class TezosStakePage { + // Stake flow (TezosStakeFlow) + stakeAmountInputId = "tezos-stake-amount-input"; + stakeAmountContinueId = "tezos-stake-amount-continue"; + // Unstake flow (TezosUnstakeFlow) + unstakeAmountInputId = "tezos-unstake-amount-input"; + unstakeAmountContinueId = "tezos-unstake-amount-continue"; + // Earning-choice chooser (TezosDelegationFlow -> TezosEarnRewards) and the delegation summary it leads to + earnRewardsStartButtonId = "tezos-earn-rewards-start-button"; + delegationSummaryValidatorId = "tezos-delegation-summary-validator"; + // Account staking-section cards (families/tezos/Delegations) + stakingRowId = "tezos-staking-row"; + delegationRowId = "tezos-delegation-row"; + // DelegationDrawer actions: Touchable sets testID to the analytics event when no explicit testID is given + stakeMoreActionId = "TezosStakeMore"; + unstakeActionId = "TezosUnstake"; + changeValidatorActionId = "TezosChangeBaker"; + endDelegationActionId = "TezosEndDelegation"; + // Unstake-required guard drawer + unstakeRequiredCloseId = "tezos-unstake-required-close"; + + @Step("Verify the earning-choice chooser is shown") + async verifyEarningChoice() { + await waitForElementById(this.earnRewardsStartButtonId); + } + + @Step("Start earning from the earning-choice chooser") + async startEarning() { + await tapById(this.earnRewardsStartButtonId); + } + + @Step("Verify the delegation summary is shown") + async verifyDelegationSummary() { + await waitForElementById(this.delegationSummaryValidatorId); + } + + @Step("Fill stake amount $0") + async fillStakeAmount(amount: string) { + await waitForElementById(this.stakeAmountInputId); + await typeTextById(this.stakeAmountInputId, amount); + await waitForElementById(enabled(this.stakeAmountContinueId)); // Issue with RN75 : QAA-370 + } + + @Step("Continue from the stake amount step") + async continueStakeAmount() { + await tapById(enabled(this.stakeAmountContinueId)); + } + + @Step("Open the unstake flow from the staking section") + async openUnstakeFromStakingSection() { + await scrollToId(this.stakingRowId, accountScrollViewId); + await tapById(this.stakingRowId); + await waitForElementById(this.unstakeActionId); + await tapById(this.unstakeActionId); + } + + @Step("Fill unstake amount $0") + async fillUnstakeAmount(amount: string) { + await waitForElementById(this.unstakeAmountInputId); + await typeTextById(this.unstakeAmountInputId, amount); + await waitForElementById(enabled(this.unstakeAmountContinueId)); // Issue with RN75 : QAA-370 + } + + @Step("Continue from the unstake amount step") + async continueUnstakeAmount() { + await tapById(enabled(this.unstakeAmountContinueId)); + } + + @Step("Open change-validator from the delegation section") + async openChangeValidator() { + await scrollToId(this.delegationRowId, accountScrollViewId); + await tapById(this.delegationRowId); + await waitForElementById(this.changeValidatorActionId); + await tapById(this.changeValidatorActionId); + } + + @Step("Open stop-delegation from the delegation section") + async openStopDelegation() { + await scrollToId(this.delegationRowId, accountScrollViewId); + await tapById(this.delegationRowId); + await waitForElementById(this.endDelegationActionId); + await tapById(this.endDelegationActionId); + } + + @Step("Verify the unstake-required guard is shown") + async verifyUnstakeRequired() { + await waitForElementById(this.unstakeRequiredCloseId); + } + + @Step("Dismiss the unstake-required guard") + async dismissUnstakeRequired() { + await tapById(this.unstakeRequiredCloseId); + } +} diff --git a/e2e/mobile/specs/stake/changeValidatorBlockedTEZOS.spec.ts b/e2e/mobile/specs/stake/changeValidatorBlockedTEZOS.spec.ts new file mode 100644 index 000000000000..b032873a3ec5 --- /dev/null +++ b/e2e/mobile/specs/stake/changeValidatorBlockedTEZOS.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { runUnstakeRequiredTezos } from "./stake"; + +// XTZ_2 (index 1) is DELEGATED + STAKED: changing validator is blocked until the user unstakes first. +const delegation = new Delegate(Account.XTZ_2, "N/A", "Ledger by Kiln"); +runUnstakeRequiredTezos(delegation, "changeValidator", ["B2CQA-5919"]); diff --git a/e2e/mobile/specs/stake/earnChoiceTEZOS.spec.ts b/e2e/mobile/specs/stake/earnChoiceTEZOS.spec.ts new file mode 100644 index 000000000000..6fc4ea76a423 --- /dev/null +++ b/e2e/mobile/specs/stake/earnChoiceTEZOS.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { runEarningChoiceTezos } from "./stake"; + +// XTZ_1 (index 0) is funded + UNDELEGATED: with the staking flag on, Earn opens the earning-choice chooser. +const delegation = new Delegate(Account.XTZ_1, "N/A", "Ledger by Kiln"); +runEarningChoiceTezos(delegation, ["B2CQA-5915"]); diff --git a/e2e/mobile/specs/stake/endDelegationBlockedTEZOS.spec.ts b/e2e/mobile/specs/stake/endDelegationBlockedTEZOS.spec.ts new file mode 100644 index 000000000000..118a1e5e653b --- /dev/null +++ b/e2e/mobile/specs/stake/endDelegationBlockedTEZOS.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { runUnstakeRequiredTezos } from "./stake"; + +// XTZ_2 (index 1) is DELEGATED + STAKED: stopping delegation is blocked until the user unstakes first. +const delegation = new Delegate(Account.XTZ_2, "N/A", "Ledger by Kiln"); +runUnstakeRequiredTezos(delegation, "stopDelegation", ["B2CQA-5921"]); diff --git a/e2e/mobile/specs/stake/stake.ts b/e2e/mobile/specs/stake/stake.ts new file mode 100644 index 000000000000..64cfe0f2b72c --- /dev/null +++ b/e2e/mobile/specs/stake/stake.ts @@ -0,0 +1,148 @@ +import { setEnv } from "@ledgerhq/live-env"; +import { DelegateType } from "@ledgerhq/live-common/e2e/models/Delegate"; +import { Team } from "@ledgerhq/live-common/e2e/enum/Team"; +import { setTeamOwner } from "../../helpers/allure/allure-helper"; + +const TEZOS_STAKING_TAGS = [ + "@NanoSP", + "@LNS", + "@NanoX", + "@Stax", + "@Flex", + "@NanoGen5", + "@tezos", + "@family-tezos", +]; + +// Mobile twin of desktop's lldTezosStaking; the staking screens, routing and the account-screen +// staking section are all gated on it (default off). +const STAKING_FEATURE_FLAGS = { llmTezosStaking: { enabled: true } }; + +async function initStakingAccount(delegation: DelegateType) { + await app.init({ + speculosApp: delegation.account.currency.speculosApp, + cliCommands: [liveDataWithAddressCommand(delegation.account)], + featureFlags: STAKING_FEATURE_FLAGS, + }); + await app.mainNavigation.waitForWallet40Ready(); +} + +async function goToTezosAccount(delegation: DelegateType) { + await app.portfolio.goToAccounts(delegation.account.currency.name); + await app.common.goToAccountByName(delegation.account.accountName); +} + +function tagSuite(tmsLinks: string[], tags: string[]) { + setTeamOwner(Team.EARN); + tags.forEach(tag => $Tag(tag)); + tmsLinks.forEach(tms => $TmsLink(tms)); +} + +export function runEarningChoiceTezos( + delegation: DelegateType, + tmsLinks: string[], + tags: string[] = TEZOS_STAKING_TAGS, +) { + setEnv("DISABLE_TRANSACTION_BROADCAST", true); + tagSuite(tmsLinks, tags); + describe("Earning choice on TEZOS", () => { + beforeAll(async () => { + await initStakingAccount(delegation); + }); + + it("Earning choice routes to the delegation summary", async () => { + await goToTezosAccount(delegation); + // Funded + undelegated => Earn opens the earning-choice chooser (not the legacy delegate starter). + await app.account.tapEarn(); + await app.tezosStake.verifyEarningChoice(); + await app.tezosStake.startEarning(); + // Undelegated => the single chooser CTA leads into the delegation summary. + await app.tezosStake.verifyDelegationSummary(); + }); + }); +} + +export function runStakeTezos( + delegation: DelegateType, + tmsLinks: string[], + tags: string[] = TEZOS_STAKING_TAGS, +) { + // Broadcast off so CI never mutates the seed; the app still reaches the success screen. + setEnv("DISABLE_TRANSACTION_BROADCAST", true); + tagSuite(tmsLinks, tags); + describe("Stake flow on TEZOS", () => { + beforeAll(async () => { + await initStakingAccount(delegation); + }); + + it("Stake on a delegated account", async () => { + await app.speculos.goToSettings(); + await app.speculos.activateExpertMode(); + await goToTezosAccount(delegation); + // Already delegated => Earn opens the stake amount step directly (skipDelegation). + await app.account.tapEarn(); + await app.tezosStake.fillStakeAmount(delegation.amount); + await app.tezosStake.continueStakeAmount(); + // Tezos signs stake via the same on-device review flow as delegation. + await app.speculos.signDelegationTransaction(delegation); + await app.common.successViewDetails(); + }); + }); +} + +export function runUnstakeTezos( + delegation: DelegateType, + tmsLinks: string[], + tags: string[] = TEZOS_STAKING_TAGS, +) { + setEnv("DISABLE_TRANSACTION_BROADCAST", true); + tagSuite(tmsLinks, tags); + describe("Unstake flow on TEZOS", () => { + beforeAll(async () => { + await initStakingAccount(delegation); + }); + + it("Unstake from a staked account", async () => { + await app.speculos.goToSettings(); + await app.speculos.activateExpertMode(); + await goToTezosAccount(delegation); + // Delegated + staked => the staking section card opens the unstake action. + await app.tezosStake.openUnstakeFromStakingSection(); + await app.tezosStake.fillUnstakeAmount(delegation.amount); + await app.tezosStake.continueUnstakeAmount(); + await app.speculos.signDelegationTransaction(delegation); + await app.common.successViewDetails(); + }); + }); +} + +export function runUnstakeRequiredTezos( + delegation: DelegateType, + action: "changeValidator" | "stopDelegation", + tmsLinks: string[], + tags: string[] = TEZOS_STAKING_TAGS, +) { + setEnv("DISABLE_TRANSACTION_BROADCAST", true); // assertion-only: never signs or broadcasts + tagSuite(tmsLinks, tags); + const title = + action === "changeValidator" + ? "Change validator is blocked while staked" + : "Stopping delegation is blocked while staked"; + describe(`Unstake required guard on TEZOS - ${action}`, () => { + beforeAll(async () => { + await initStakingAccount(delegation); + }); + + it(title, async () => { + await goToTezosAccount(delegation); + // Delegated + staked => both actions hit the "unstake first" guard instead of the real flow. + if (action === "changeValidator") { + await app.tezosStake.openChangeValidator(); + } else { + await app.tezosStake.openStopDelegation(); + } + await app.tezosStake.verifyUnstakeRequired(); + await app.tezosStake.dismissUnstakeRequired(); + }); + }); +} diff --git a/e2e/mobile/specs/stake/stakeTEZOS.spec.ts b/e2e/mobile/specs/stake/stakeTEZOS.spec.ts new file mode 100644 index 000000000000..19893dd1f84a --- /dev/null +++ b/e2e/mobile/specs/stake/stakeTEZOS.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { runStakeTezos } from "./stake"; + +// XTZ_2 (index 1) is DELEGATED + STAKED: Earn opens the stake amount step directly. +const delegation = new Delegate(Account.XTZ_2, "0.005", "Ledger by Kiln"); +runStakeTezos(delegation, ["B2CQA-5917"]); diff --git a/e2e/mobile/specs/stake/unstakeTEZOS.spec.ts b/e2e/mobile/specs/stake/unstakeTEZOS.spec.ts new file mode 100644 index 000000000000..199d451b1979 --- /dev/null +++ b/e2e/mobile/specs/stake/unstakeTEZOS.spec.ts @@ -0,0 +1,6 @@ +import { Account } from "@ledgerhq/live-common/e2e/enum/Account"; +import { runUnstakeTezos } from "./stake"; + +// XTZ_2 (index 1) is DELEGATED + STAKED: the account screen shows the staking section with the unstake action. +const delegation = new Delegate(Account.XTZ_2, "0.005", "Ledger by Kiln"); +runUnstakeTezos(delegation, ["B2CQA-5918"]); From fb46bf1b30a0f33924dcdfb37f48c0c2249f6690 Mon Sep 17 00:00:00 2001 From: Aleksei Maslakov Date: Wed, 24 Jun 2026 10:41:20 +0100 Subject: [PATCH 2/2] test(tezos): make unstaking row test id unique per position Suffix with position.uid so repeated unstaking rows are not ambiguous to Detox. --- .../src/families/tezos/Delegations/UnstakingRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ledger-live-mobile/src/families/tezos/Delegations/UnstakingRow.tsx b/apps/ledger-live-mobile/src/families/tezos/Delegations/UnstakingRow.tsx index eafe0e9b0daa..7c6e4d3f95b3 100644 --- a/apps/ledger-live-mobile/src/families/tezos/Delegations/UnstakingRow.tsx +++ b/apps/ledger-live-mobile/src/families/tezos/Delegations/UnstakingRow.tsx @@ -23,7 +23,7 @@ export default function UnstakingRow({ position, unit, currency, onPress, isLast return (