Skip to content

Commit 7379137

Browse files
committed
test(mobile): add e2e page object and specs for main navigation
Add MainNavigationPage with selectors for both Wallet 4.0 and legacy navigation elements. Add spec files for W40 and legacy navigation flows covering tab switching and destination page verification. Note: W40 specs are blocked by a DetoxSync/BlurView crash on iOS (NSNull __detox_sync_untrackAnimation) and require a fix before running. LIVE-24697 chore(mobile): add changeset for e2e main navigation tests LIVE-24697
1 parent 7106257 commit 7379137

File tree

11 files changed

+413
-2
lines changed

11 files changed

+413
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"live-mobile": minor
3+
---
4+
5+
Add E2E test infrastructure for main navigation (Wallet 4.0 and legacy)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* eslint-disable @typescript-eslint/no-var-requires */
2+
/**
3+
* Mock for @sbaiahmed1/react-native-blur used during E2E (Detox) builds.
4+
*
5+
* BlurView's UIViewPropertyAnimator sets an NSNull delegate on CAAnimation,
6+
* which crashes DetoxSync's animation tracking on iOS.
7+
* Replacing native blur components with plain Views avoids the crash while
8+
* keeping the rest of the UI intact.
9+
*/
10+
const React = require("react");
11+
const { View } = require("react-native");
12+
13+
const passthrough = React.forwardRef((props, ref) => {
14+
const { style, children } = props;
15+
return React.createElement(View, { ref, style }, children);
16+
});
17+
18+
passthrough.displayName = "BlurViewMock";
19+
20+
module.exports = {
21+
BlurView: passthrough,
22+
VibrancyView: passthrough,
23+
LiquidGlassView: passthrough,
24+
LiquidGlassContainer: passthrough,
25+
ProgressiveBlurView: passthrough,
26+
BlurSwitch: passthrough,
27+
default: passthrough,
28+
};

apps/ledger-live-mobile/rspack.config.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ const withRozeniteUrlFix = rozeniteConfig => {
109109
};
110110
};
111111

112+
const isDetoxBuild = process.env.DETOX === "1" || (process.env.ENVFILE || "").includes("mock");
113+
114+
const detoxAliases = isDetoxBuild
115+
? {
116+
"@sbaiahmed1/react-native-blur": path.resolve(__dirname, "e2e/mocks/react-native-blur.js"),
117+
}
118+
: {};
119+
112120
const hermesNonCompatibleDependencies = ["@polkadot/types-codec"];
113121

114122
/**
@@ -140,6 +148,7 @@ export default withRozeniteUrlFix(
140148
modules: nodeModulesPaths,
141149
alias: {
142150
...buildTsAlias(tsconfig.compilerOptions.paths),
151+
...detoxAliases,
143152
// Packages with malformed exports field (missing "." subpath) - resolve to browser entry
144153
"@aptos-labs/aptos-client": resolvePackageFile(
145154
"@aptos-labs/aptos-client",

apps/ledger-live-mobile/src/mvvm/components/MainTabBar/MainTabBarView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const MainTabBarView: React.FC<MainTabBarViewProps> = ({
2222

2323
return (
2424
<Animated.View
25+
testID="w40-tab-bar"
2526
entering={FadeInDown}
2627
exiting={FadeOutDown}
2728
pointerEvents="box-none"
@@ -50,6 +51,7 @@ export const MainTabBarView: React.FC<MainTabBarViewProps> = ({
5051
label={item.label}
5152
icon={item.icon}
5253
activeIcon={item.activeIcon}
54+
testID={item.testID}
5355
/>
5456
))}
5557
</TabBar>

apps/ledger-live-mobile/src/mvvm/components/MainTabBar/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { BottomTabBarProps } from "@react-navigation/bottom-tabs";
22
import type { TabBarItemProps } from "@ledgerhq/lumen-ui-rnative";
33

4-
export type TabItemConfig = Pick<TabBarItemProps, "value" | "label" | "icon" | "activeIcon">;
4+
export type TabItemConfig = Pick<
5+
TabBarItemProps,
6+
"value" | "label" | "icon" | "activeIcon" | "testID"
7+
>;
58

69
export interface MainTabBarViewProps {
710
readonly activeRouteName: string;

apps/ledger-live-mobile/src/mvvm/components/MainTabBar/useMainTabBarViewModel.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ const TAB_ICONS: Partial<Record<string, TabIconConfig>> = {
3131
[NavigatorName.Earn]: { icon: Chart5, activeIcon: Chart5Fill },
3232
[NavigatorName.CardTab]: { icon: CreditCard, activeIcon: CreditCardFill },
3333
};
34+
35+
const TAB_TEST_IDS: Partial<Record<string, string>> = {
36+
[NavigatorName.Portfolio]: "w40-tab-home",
37+
[NavigatorName.Swap]: "w40-tab-swap",
38+
[NavigatorName.Earn]: "w40-tab-earn",
39+
[NavigatorName.CardTab]: "w40-tab-card",
40+
};
41+
3442
export const useMainTabBarViewModel = ({
3543
state,
3644
navigation,
@@ -43,6 +51,7 @@ export const useMainTabBarViewModel = ({
4351
state.routes.map(route => ({
4452
value: route.name,
4553
label: t(LABELKEY_MAP[route.name] ?? route.name),
54+
testID: TAB_TEST_IDS[route.name],
4655
...TAB_ICONS[route.name],
4756
})),
4857
[state.routes, t],

apps/ledger-live-mobile/src/screens/PTX/Earn/EarnV2Webview/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const EarnV2Webview = ({
5555
};
5656

5757
return (
58-
<View style={{ flex: 1, overflow: "visible" }}>
58+
<View testID="earn-screen" style={{ flex: 1, overflow: "visible" }}>
5959
{isPtxUiV2 && !hideMainNavigator && <EarnBackground scrollY={scrollY} />}
6060
<View style={{ flex: 1, zIndex: 1 }} pointerEvents="box-none">
6161
{manifest ? (

e2e/mobile/page/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import StakePage from "./trade/stake.page";
2525
import SwapPage from "./trade/swap.page";
2626
import SwapLiveAppPage from "./liveApps/swapLiveApp";
2727
import WalletTabNavigatorPage from "./wallet/walletTabNavigator.page";
28+
import MainNavigationPage from "./wallet/mainNavigation.page";
2829
import CeloManageAssetsPage from "./trade/celoManageAssets.page";
2930
import TransferMenuDrawer from "./wallet/transferMenu.drawer";
3031
import BuySellPage from "./trade/buySell.page";
@@ -76,6 +77,7 @@ export class Application {
7677
private swapLiveAppInstance = lazyInit(SwapLiveAppPage);
7778
private swapPageInstance = lazyInit(SwapPage);
7879
private walletTabNavigatorPageInstance = lazyInit(WalletTabNavigatorPage);
80+
private mainNavigationPageInstance = lazyInit(MainNavigationPage);
7981
private celoManageAssetsPageInstance = lazyInit(CeloManageAssetsPage);
8082
private TransferMenuDrawerInstance = lazyInit(TransferMenuDrawer);
8183
private buySellPageInstance = lazyInit(BuySellPage);
@@ -195,6 +197,10 @@ export class Application {
195197
return this.walletTabNavigatorPageInstance();
196198
}
197199

200+
public get mainNavigation() {
201+
return this.mainNavigationPageInstance();
202+
}
203+
198204
public get celoManageAssets() {
199205
return this.celoManageAssetsPageInstance();
200206
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { element, by } from "detox";
2+
import { Step } from "jest-allure2-reporter/api";
3+
import { openDeeplink } from "../../helpers/commonHelpers";
4+
5+
export default class MainNavigationPage {
6+
// --- Wallet 4.0 bottom tabs ---
7+
// withAncestor scopes to the tab bar, avoiding collisions with same-label CTAs elsewhere on the page.
8+
wallet40HomeTab = () => element(by.label("Home").withAncestor(by.id("w40-tab-bar")));
9+
wallet40SwapTab = () => element(by.label("Swap").withAncestor(by.id("w40-tab-bar")));
10+
wallet40EarnTab = () => element(by.label("Earn").withAncestor(by.id("w40-tab-bar")));
11+
wallet40CardTab = () => element(by.label("Card").withAncestor(by.id("w40-tab-bar")));
12+
13+
// --- Wallet 4.0 top bar buttons ---
14+
topBarMyLedgerId = "topbar-myledger";
15+
topBarDiscoverId = "topbar-discover";
16+
topBarNotificationsId = "topbar-notifications";
17+
topBarSettingsId = "topbar-settings";
18+
19+
// --- Legacy bottom tabs ---
20+
legacyPortfolioTabId = "tab-bar-portfolio";
21+
legacyEarnTabId = "tab-bar-earn";
22+
legacyTransferButtonId = "transfer-button";
23+
legacyDiscoverTabId = "tab-bar-discover";
24+
legacyMyLedgerTabId = "TabBarManager";
25+
26+
// --- Destination page verification IDs ---
27+
portfolioAccountsListId = "PortfolioAccountsList";
28+
portfolioEmptyListId = "PortfolioEmptyList";
29+
swapFormId = "swap-form-tab";
30+
earnScreenId = "earn-screen";
31+
cardScreenId = "card-landing-screen";
32+
managerHeaderTitle = "My Ledger";
33+
discoverHeaderTitle = "Discover";
34+
settingsCardId = "general-settings-card";
35+
36+
// =====================
37+
// Wait helpers
38+
// =====================
39+
40+
@Step("Wait for Wallet 4.0 navigation to be ready")
41+
async waitForWallet40Ready() {
42+
await waitForElementById(this.topBarSettingsId, 120000);
43+
}
44+
45+
@Step("Wait for Legacy navigation to be ready")
46+
async waitForLegacyReady() {
47+
await waitForElementById(this.legacyMyLedgerTabId, 120000);
48+
}
49+
50+
// =====================
51+
// Wallet 4.0 Tab Actions
52+
// =====================
53+
54+
@Step("Tap Home tab (Wallet 4.0)")
55+
async tapWallet40HomeTab() {
56+
await this.wallet40HomeTab().tap();
57+
}
58+
59+
@Step("Tap Swap tab (Wallet 4.0)")
60+
async tapWallet40SwapTab() {
61+
await this.wallet40SwapTab().tap();
62+
}
63+
64+
@Step("Tap Earn tab (Wallet 4.0)")
65+
async tapWallet40EarnTab() {
66+
await this.wallet40EarnTab().tap();
67+
}
68+
69+
@Step("Tap Card tab (Wallet 4.0)")
70+
async tapWallet40CardTab() {
71+
await this.wallet40CardTab().tap();
72+
}
73+
74+
// =====================
75+
// Wallet 4.0 Top Bar Actions
76+
// =====================
77+
78+
@Step("Tap My Ledger in top bar")
79+
async tapTopBarMyLedger() {
80+
await tapById(this.topBarMyLedgerId);
81+
}
82+
83+
@Step("Tap Discover in top bar")
84+
async tapTopBarDiscover() {
85+
await tapById(this.topBarDiscoverId);
86+
}
87+
88+
@Step("Tap Notifications in top bar")
89+
async tapTopBarNotifications() {
90+
await tapById(this.topBarNotificationsId);
91+
}
92+
93+
@Step("Tap Settings in top bar")
94+
async tapTopBarSettings() {
95+
await tapById(this.topBarSettingsId);
96+
}
97+
98+
// =====================
99+
// Legacy Tab Actions
100+
// =====================
101+
102+
@Step("Tap Portfolio tab (Legacy)")
103+
async tapLegacyPortfolioTab() {
104+
await tapById(this.legacyPortfolioTabId);
105+
}
106+
107+
@Step("Tap Earn tab (Legacy)")
108+
async tapLegacyEarnTab() {
109+
await tapById(this.legacyEarnTabId);
110+
}
111+
112+
@Step("Tap Discover tab (Legacy)")
113+
async tapLegacyDiscoverTab() {
114+
await tapById(this.legacyDiscoverTabId);
115+
}
116+
117+
@Step("Tap My Ledger tab (Legacy)")
118+
async tapLegacyMyLedgerTab() {
119+
await tapById(this.legacyMyLedgerTabId);
120+
}
121+
122+
// =====================
123+
// Wallet 4.0 Expectations
124+
// =====================
125+
126+
@Step("Expect Wallet 4.0 bottom tabs to be visible")
127+
async expectWallet40BottomTabsVisible() {
128+
await detoxExpect(this.wallet40HomeTab()).toBeVisible();
129+
await detoxExpect(this.wallet40SwapTab()).toBeVisible();
130+
await detoxExpect(this.wallet40EarnTab()).toBeVisible();
131+
await detoxExpect(this.wallet40CardTab()).toBeVisible();
132+
}
133+
134+
@Step("Expect Wallet 4.0 top bar to be visible")
135+
async expectWallet40TopBarVisible() {
136+
await detoxExpect(getElementById(this.topBarMyLedgerId)).toBeVisible();
137+
await detoxExpect(getElementById(this.topBarDiscoverId)).toBeVisible();
138+
await detoxExpect(getElementById(this.topBarNotificationsId)).toBeVisible();
139+
await detoxExpect(getElementById(this.topBarSettingsId)).toBeVisible();
140+
}
141+
142+
@Step("Expect legacy bottom tabs NOT visible")
143+
async expectLegacyTabsNotVisible() {
144+
await detoxExpect(getElementById(this.legacyTransferButtonId)).not.toBeVisible();
145+
await detoxExpect(getElementById(this.legacyMyLedgerTabId)).not.toBeVisible();
146+
}
147+
148+
// =====================
149+
// Legacy Expectations
150+
// =====================
151+
152+
@Step("Expect legacy bottom tabs to be visible")
153+
async expectLegacyBottomTabsVisible() {
154+
await detoxExpect(getElementById(this.legacyPortfolioTabId)).toBeVisible();
155+
await detoxExpect(getElementById(this.legacyEarnTabId)).toBeVisible();
156+
await detoxExpect(getElementById(this.legacyTransferButtonId)).toBeVisible();
157+
await detoxExpect(getElementById(this.legacyDiscoverTabId)).toBeVisible();
158+
await detoxExpect(getElementById(this.legacyMyLedgerTabId)).toBeVisible();
159+
}
160+
161+
@Step("Expect Wallet 4.0 top bar NOT visible")
162+
async expectWallet40TopBarNotVisible() {
163+
await detoxExpect(getElementById(this.topBarMyLedgerId)).not.toBeVisible();
164+
}
165+
166+
// =====================
167+
// Destination Page Expectations
168+
// =====================
169+
170+
@Step("Open Portfolio via deeplink (W40)")
171+
async openPortfolioViaDeeplink() {
172+
await openDeeplink("portfolio");
173+
await this.waitForWallet40Ready();
174+
}
175+
176+
@Step("Expect Portfolio page visible")
177+
async expectPortfolioPageVisible() {
178+
try {
179+
await waitForElementById(this.portfolioAccountsListId, 5000);
180+
} catch {
181+
await waitForElementById(this.portfolioEmptyListId, 5000);
182+
}
183+
}
184+
185+
@Step("Expect Swap page visible")
186+
async expectSwapPageVisible() {
187+
await waitForElementById(this.swapFormId);
188+
}
189+
190+
@Step("Expect Earn page visible")
191+
async expectEarnPageVisible() {
192+
await waitForElementById(this.earnScreenId);
193+
}
194+
195+
@Step("Expect Card page visible")
196+
async expectCardPageVisible() {
197+
await waitForElementById(this.cardScreenId);
198+
}
199+
200+
@Step("Expect My Ledger page visible")
201+
async expectMyLedgerPageVisible() {
202+
await detoxExpect(element(by.text(this.managerHeaderTitle)).atIndex(0)).toBeVisible();
203+
}
204+
205+
@Step("Go back via header back button")
206+
async tapHeaderBackButton() {
207+
await element(by.label("Back")).atIndex(0).tap();
208+
}
209+
210+
@Step("Expect Discover page visible")
211+
async expectDiscoverPageVisible() {
212+
await detoxExpect(element(by.text(this.discoverHeaderTitle)).atIndex(0)).toBeVisible();
213+
}
214+
215+
@Step("Expect Settings page visible")
216+
async expectSettingsPageVisible() {
217+
await waitForElementById(this.settingsCardId);
218+
}
219+
}

0 commit comments

Comments
 (0)