diff --git a/.changeset/add-e2e-main-navigation-tests.md b/.changeset/add-e2e-main-navigation-tests.md new file mode 100644 index 00000000000..85c72970bce --- /dev/null +++ b/.changeset/add-e2e-main-navigation-tests.md @@ -0,0 +1,5 @@ +--- +"live-mobile": minor +--- + +Add E2E test infrastructure for main navigation (Wallet 4.0 and legacy) diff --git a/apps/ledger-live-mobile/e2e/mocks/react-native-blur.js b/apps/ledger-live-mobile/e2e/mocks/react-native-blur.js new file mode 100644 index 00000000000..628bf473c43 --- /dev/null +++ b/apps/ledger-live-mobile/e2e/mocks/react-native-blur.js @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/** + * Mock for @sbaiahmed1/react-native-blur used during E2E (Detox) builds. + * + * BlurView's UIViewPropertyAnimator sets an NSNull delegate on CAAnimation, + * which crashes DetoxSync's animation tracking on iOS. + * Replacing native blur components with plain Views avoids the crash while + * keeping the rest of the UI intact. + */ +const React = require("react"); +const { View } = require("react-native"); + +const passthrough = React.forwardRef((props, ref) => { + const { style, children } = props; + return React.createElement(View, { ref, style }, children); +}); + +passthrough.displayName = "BlurViewMock"; + +module.exports = { + BlurView: passthrough, + VibrancyView: passthrough, + LiquidGlassView: passthrough, + LiquidGlassContainer: passthrough, + ProgressiveBlurView: passthrough, + BlurSwitch: passthrough, + default: passthrough, +}; diff --git a/apps/ledger-live-mobile/rspack.config.mjs b/apps/ledger-live-mobile/rspack.config.mjs index d71ef54b062..83c3571cc6e 100644 --- a/apps/ledger-live-mobile/rspack.config.mjs +++ b/apps/ledger-live-mobile/rspack.config.mjs @@ -109,6 +109,14 @@ const withRozeniteUrlFix = rozeniteConfig => { }; }; +const isDetoxBuild = process.env.DETOX === "1" || (process.env.ENVFILE || "").includes("mock"); + +const detoxAliases = isDetoxBuild + ? { + "@sbaiahmed1/react-native-blur": path.resolve(__dirname, "e2e/mocks/react-native-blur.js"), + } + : {}; + const hermesNonCompatibleDependencies = ["@polkadot/types-codec"]; /** @@ -140,6 +148,7 @@ export default withRozeniteUrlFix( modules: nodeModulesPaths, alias: { ...buildTsAlias(tsconfig.compilerOptions.paths), + ...detoxAliases, // Packages with malformed exports field (missing "." subpath) - resolve to browser entry "@aptos-labs/aptos-client": resolvePackageFile( "@aptos-labs/aptos-client", diff --git a/apps/ledger-live-mobile/src/components/RootNavigator/MainNavigator/LegacyTabNavigator.tsx b/apps/ledger-live-mobile/src/components/RootNavigator/MainNavigator/LegacyTabNavigator.tsx index 67cafc4c2e2..f741c58ae96 100644 --- a/apps/ledger-live-mobile/src/components/RootNavigator/MainNavigator/LegacyTabNavigator.tsx +++ b/apps/ledger-live-mobile/src/components/RootNavigator/MainNavigator/LegacyTabNavigator.tsx @@ -12,6 +12,14 @@ import EarnLiveAppNavigator from "../EarnLiveAppNavigator"; import { Tab } from "./tabNavigator"; import type { LegacyTabNavigatorProps } from "./types"; +const LEGACY_TAB_TEST_IDS: Partial> = { + [NavigatorName.Portfolio]: "tab-bar-portfolio", + [NavigatorName.Earn]: "tab-bar-earn", + [NavigatorName.Web3HubTab]: "tab-bar-discover", + [NavigatorName.Discover]: "tab-bar-discover", + [NavigatorName.MyLedger]: "TabBarManager", +}; + export function LegacyTabNavigator({ tabBar, screenOptions, @@ -31,6 +39,7 @@ export function LegacyTabNavigator({ options={{ headerShown: false, tabBarIcon: props => , + tabBarButtonTestID: LEGACY_TAB_TEST_IDS[NavigatorName.Portfolio], }} listeners={({ navigation }) => ({ tabPress: e => { @@ -53,10 +62,11 @@ export function LegacyTabNavigator({ ), + tabBarButtonTestID: LEGACY_TAB_TEST_IDS[NavigatorName.Earn], }} listeners={({ navigation }) => ({ tabPress: e => { @@ -101,6 +111,7 @@ export function LegacyTabNavigator({ tabBarIcon: props => ( ), + tabBarButtonTestID: LEGACY_TAB_TEST_IDS[NavigatorName.Web3HubTab], }} listeners={({ navigation }) => ({ tabPress: e => { @@ -120,6 +131,7 @@ export function LegacyTabNavigator({ tabBarIcon: props => ( ), + tabBarButtonTestID: LEGACY_TAB_TEST_IDS[NavigatorName.Discover], }} listeners={({ navigation }) => ({ tabPress: e => { @@ -139,7 +151,7 @@ export function LegacyTabNavigator({ options={{ headerShown: false, tabBarIcon: props => , - tabBarButtonTestID: "TabBarManager", + tabBarButtonTestID: LEGACY_TAB_TEST_IDS[NavigatorName.MyLedger], }} listeners={({ navigation }) => ({ tabPress: e => { diff --git a/apps/ledger-live-mobile/src/mvvm/components/MainTabBar/MainTabBarView.tsx b/apps/ledger-live-mobile/src/mvvm/components/MainTabBar/MainTabBarView.tsx index 3616aeb93ee..0ab319e1085 100644 --- a/apps/ledger-live-mobile/src/mvvm/components/MainTabBar/MainTabBarView.tsx +++ b/apps/ledger-live-mobile/src/mvvm/components/MainTabBar/MainTabBarView.tsx @@ -22,6 +22,7 @@ export const MainTabBarView: React.FC = ({ return ( = ({ label={item.label} icon={item.icon} activeIcon={item.activeIcon} + testID={item.testID} /> ))} diff --git a/apps/ledger-live-mobile/src/mvvm/components/MainTabBar/types.ts b/apps/ledger-live-mobile/src/mvvm/components/MainTabBar/types.ts index 9a56f2c9158..c2cf427ba5d 100644 --- a/apps/ledger-live-mobile/src/mvvm/components/MainTabBar/types.ts +++ b/apps/ledger-live-mobile/src/mvvm/components/MainTabBar/types.ts @@ -1,7 +1,10 @@ import type { BottomTabBarProps } from "@react-navigation/bottom-tabs"; import type { TabBarItemProps } from "@ledgerhq/lumen-ui-rnative"; -export type TabItemConfig = Pick; +export type TabItemConfig = Pick< + TabBarItemProps, + "value" | "label" | "icon" | "activeIcon" | "testID" +>; export interface MainTabBarViewProps { readonly activeRouteName: string; diff --git a/apps/ledger-live-mobile/src/mvvm/components/MainTabBar/useMainTabBarViewModel.ts b/apps/ledger-live-mobile/src/mvvm/components/MainTabBar/useMainTabBarViewModel.ts index a252857556b..fb20305c647 100644 --- a/apps/ledger-live-mobile/src/mvvm/components/MainTabBar/useMainTabBarViewModel.ts +++ b/apps/ledger-live-mobile/src/mvvm/components/MainTabBar/useMainTabBarViewModel.ts @@ -31,6 +31,14 @@ const TAB_ICONS: Partial> = { [NavigatorName.Earn]: { icon: Chart5, activeIcon: Chart5Fill }, [NavigatorName.CardTab]: { icon: CreditCard, activeIcon: CreditCardFill }, }; + +const TAB_TEST_IDS: Partial> = { + [NavigatorName.Portfolio]: "w40-tab-home", + [NavigatorName.Swap]: "w40-tab-swap", + [NavigatorName.Earn]: "w40-tab-earn", + [NavigatorName.CardTab]: "w40-tab-card", +}; + export const useMainTabBarViewModel = ({ state, navigation, @@ -43,6 +51,7 @@ export const useMainTabBarViewModel = ({ state.routes.map(route => ({ value: route.name, label: t(LABELKEY_MAP[route.name] ?? route.name), + testID: TAB_TEST_IDS[route.name], ...TAB_ICONS[route.name], })), [state.routes, t], diff --git a/apps/ledger-live-mobile/src/mvvm/features/Portfolio/screens/Portfolio/index.tsx b/apps/ledger-live-mobile/src/mvvm/features/Portfolio/screens/Portfolio/index.tsx index 32605ec8606..90ad17e1b61 100644 --- a/apps/ledger-live-mobile/src/mvvm/features/Portfolio/screens/Portfolio/index.tsx +++ b/apps/ledger-live-mobile/src/mvvm/features/Portfolio/screens/Portfolio/index.tsx @@ -158,7 +158,7 @@ export const PortfolioScreen = ({ navigation }: NavigationProps) => { <> - + } diff --git a/apps/ledger-live-mobile/src/screens/MyLedgerChooseDevice/wallet40HeaderOptions.tsx b/apps/ledger-live-mobile/src/screens/MyLedgerChooseDevice/wallet40HeaderOptions.tsx index 651adf749b4..6cf633e1922 100644 --- a/apps/ledger-live-mobile/src/screens/MyLedgerChooseDevice/wallet40HeaderOptions.tsx +++ b/apps/ledger-live-mobile/src/screens/MyLedgerChooseDevice/wallet40HeaderOptions.tsx @@ -2,8 +2,10 @@ import React from "react"; import HeaderBackButton from "LLM/components/Navigation/HeaderBackButton"; import HeaderTitle from "LLM/components/Navigation/HeaderTitle"; +export const HEADER_BACK_BUTTON_TEST_ID = "header-back-button"; + export const wallet40HeaderOptions = { headerShown: true, headerTitle: () => , - headerLeft: () => , + headerLeft: () => , }; diff --git a/apps/ledger-live-mobile/src/screens/PTX/Earn/EarnV2Webview/index.tsx b/apps/ledger-live-mobile/src/screens/PTX/Earn/EarnV2Webview/index.tsx index cbfa0d940ea..a00f3231475 100644 --- a/apps/ledger-live-mobile/src/screens/PTX/Earn/EarnV2Webview/index.tsx +++ b/apps/ledger-live-mobile/src/screens/PTX/Earn/EarnV2Webview/index.tsx @@ -55,7 +55,7 @@ export const EarnV2Webview = ({ }; return ( - + {isPtxUiV2 && !hideMainNavigator && } {manifest ? ( diff --git a/apps/ledger-live-mobile/src/screens/PTX/Earn/index.tsx b/apps/ledger-live-mobile/src/screens/PTX/Earn/index.tsx index b3888285261..62a90c1f9b7 100644 --- a/apps/ledger-live-mobile/src/screens/PTX/Earn/index.tsx +++ b/apps/ledger-live-mobile/src/screens/PTX/Earn/index.tsx @@ -142,7 +142,7 @@ function Earn({ route }: Props) { /** V1: no background */ if (manifest) { return ( - + diff --git a/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx b/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx index 58b9fdffcc0..c18bfad3af7 100644 --- a/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx +++ b/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx @@ -252,7 +252,7 @@ function PortfolioScreen({ navigation }: NavigationProps) { <> - + } diff --git a/e2e/mobile/page/index.ts b/e2e/mobile/page/index.ts index 7b443cd74b1..56fd269b62d 100644 --- a/e2e/mobile/page/index.ts +++ b/e2e/mobile/page/index.ts @@ -25,6 +25,7 @@ import StakePage from "./trade/stake.page"; import SwapPage from "./trade/swap.page"; import SwapLiveAppPage from "./liveApps/swapLiveApp"; import WalletTabNavigatorPage from "./wallet/walletTabNavigator.page"; +import MainNavigationPage from "./wallet/mainNavigation.page"; import CeloManageAssetsPage from "./trade/celoManageAssets.page"; import TransferMenuDrawer from "./wallet/transferMenu.drawer"; import BuySellPage from "./trade/buySell.page"; @@ -76,6 +77,7 @@ export class Application { private swapLiveAppInstance = lazyInit(SwapLiveAppPage); private swapPageInstance = lazyInit(SwapPage); private walletTabNavigatorPageInstance = lazyInit(WalletTabNavigatorPage); + private mainNavigationPageInstance = lazyInit(MainNavigationPage); private celoManageAssetsPageInstance = lazyInit(CeloManageAssetsPage); private TransferMenuDrawerInstance = lazyInit(TransferMenuDrawer); private buySellPageInstance = lazyInit(BuySellPage); @@ -195,6 +197,10 @@ export class Application { return this.walletTabNavigatorPageInstance(); } + public get mainNavigation() { + return this.mainNavigationPageInstance(); + } + public get celoManageAssets() { return this.celoManageAssetsPageInstance(); } diff --git a/e2e/mobile/page/wallet/mainNavigation.page.ts b/e2e/mobile/page/wallet/mainNavigation.page.ts new file mode 100644 index 00000000000..5f11b802fb7 --- /dev/null +++ b/e2e/mobile/page/wallet/mainNavigation.page.ts @@ -0,0 +1,203 @@ +import { element, by } from "detox"; +import { Step } from "jest-allure2-reporter/api"; +import { openDeeplink } from "../../helpers/commonHelpers"; + +type Wallet40TabName = "home" | "swap" | "earn" | "card"; + +export default class MainNavigationPage { + // --- Wallet 4.0 bottom tabs --- + wallet40Tab = (tabName: Wallet40TabName) => element(by.id(`w40-tab-${tabName}`)); + + // --- Wallet 4.0 top bar buttons --- + topBarMyLedgerId = "topbar-myledger"; + topBarDiscoverId = "topbar-discover"; + topBarNotificationsId = "topbar-notifications"; + topBarSettingsId = "topbar-settings"; + + // --- Legacy bottom tabs --- + legacyPortfolioTabId = "tab-bar-portfolio"; + legacyEarnTabId = "tab-bar-earn"; + legacyTransferButtonId = "transfer-button"; + legacyDiscoverTabId = "tab-bar-discover"; + legacyMyLedgerTabId = "TabBarManager"; + + // --- Destination page verification IDs --- + portfolioScreenId = "portfolio-screen"; + swapFormId = "swap-form-tab"; + earnScreenId = "earn-screen"; + cardScreenId = "card-landing-screen"; + managerHeaderTitle = "My Ledger"; + discoverHeaderTitle = "Discover"; + notificationsHeaderTitle = "Notifications"; + settingsCardId = "general-settings-card"; + + // ===================== + // Wait helpers + // ===================== + + @Step("Wait for Wallet 4.0 navigation to be ready") + async waitForWallet40Ready() { + await waitForElementById(this.topBarSettingsId); + } + + @Step("Wait for Legacy navigation to be ready") + async waitForLegacyReady() { + await waitForElementById(this.legacyMyLedgerTabId); + } + + // ===================== + // Wallet 4.0 Tab Actions + // ===================== + + @Step("Tap W40 tab") + async tapWallet40Tab(tabName: Wallet40TabName) { + await this.wallet40Tab(tabName).tap(); + } + + // ===================== + // Wallet 4.0 Top Bar Actions + // ===================== + + @Step("Tap My Ledger in top bar") + async tapTopBarMyLedger() { + await tapById(this.topBarMyLedgerId); + } + + @Step("Tap Discover in top bar") + async tapTopBarDiscover() { + await tapById(this.topBarDiscoverId); + } + + @Step("Tap Notifications in top bar") + async tapTopBarNotifications() { + await tapById(this.topBarNotificationsId); + } + + @Step("Tap Settings in top bar") + async tapTopBarSettings() { + await tapById(this.topBarSettingsId); + } + + // ===================== + // Legacy Tab Actions + // ===================== + + @Step("Tap Portfolio tab (Legacy)") + async tapLegacyPortfolioTab() { + await tapById(this.legacyPortfolioTabId); + } + + @Step("Tap Earn tab (Legacy)") + async tapLegacyEarnTab() { + await tapById(this.legacyEarnTabId); + } + + @Step("Tap Discover tab (Legacy)") + async tapLegacyDiscoverTab() { + await tapById(this.legacyDiscoverTabId); + } + + @Step("Tap My Ledger tab (Legacy)") + async tapLegacyMyLedgerTab() { + await tapById(this.legacyMyLedgerTabId); + } + + // ===================== + // Wallet 4.0 Expectations + // ===================== + + @Step("Expect Wallet 4.0 bottom tabs to be visible") + async expectWallet40BottomTabsVisible() { + await detoxExpect(this.wallet40Tab("home")).toBeVisible(); + await detoxExpect(this.wallet40Tab("swap")).toBeVisible(); + await detoxExpect(this.wallet40Tab("earn")).toBeVisible(); + await detoxExpect(this.wallet40Tab("card")).toBeVisible(); + } + + @Step("Expect Wallet 4.0 top bar to be visible") + async expectWallet40TopBarVisible() { + await detoxExpect(getElementById(this.topBarMyLedgerId)).toBeVisible(); + await detoxExpect(getElementById(this.topBarDiscoverId)).toBeVisible(); + await detoxExpect(getElementById(this.topBarNotificationsId)).toBeVisible(); + await detoxExpect(getElementById(this.topBarSettingsId)).toBeVisible(); + } + + @Step("Expect legacy bottom tabs NOT visible") + async expectLegacyTabsNotVisible() { + await detoxExpect(getElementById(this.legacyTransferButtonId)).not.toBeVisible(); + await detoxExpect(getElementById(this.legacyMyLedgerTabId)).not.toBeVisible(); + } + + // ===================== + // Legacy Expectations + // ===================== + + @Step("Expect legacy bottom tabs to be visible") + async expectLegacyBottomTabsVisible() { + await detoxExpect(getElementById(this.legacyPortfolioTabId)).toBeVisible(); + await detoxExpect(getElementById(this.legacyEarnTabId)).toBeVisible(); + await detoxExpect(getElementById(this.legacyTransferButtonId)).toBeVisible(); + await detoxExpect(getElementById(this.legacyDiscoverTabId)).toBeVisible(); + await detoxExpect(getElementById(this.legacyMyLedgerTabId)).toBeVisible(); + } + + @Step("Expect Wallet 4.0 top bar NOT visible") + async expectWallet40TopBarNotVisible() { + await detoxExpect(getElementById(this.topBarMyLedgerId)).not.toBeVisible(); + } + + // ===================== + // Destination Page Expectations + // ===================== + + @Step("Open Portfolio via deeplink (W40)") + async openPortfolioViaDeeplink() { + await openDeeplink("portfolio"); + await this.waitForWallet40Ready(); + } + + @Step("Expect Portfolio page visible") + async expectPortfolioPageVisible() { + await waitForElementById(this.portfolioScreenId); + } + + @Step("Expect Swap page visible") + async expectSwapPageVisible() { + await waitForElementById(this.swapFormId); + } + + @Step("Expect Earn page visible") + async expectEarnPageVisible() { + await waitForElementById(this.earnScreenId); + } + + @Step("Expect Card page visible") + async expectCardPageVisible() { + await waitForElementById(this.cardScreenId); + } + + @Step("Expect My Ledger page visible") + async expectMyLedgerPageVisible() { + await detoxExpect(element(by.text(this.managerHeaderTitle)).atIndex(0)).toBeVisible(); + } + + @Step("Go back via header back button") + async tapHeaderBackButton() { + await tapById("header-back-button"); + } + + @Step("Expect Discover page visible") + async expectDiscoverPageVisible() { + await detoxExpect(element(by.text(this.discoverHeaderTitle)).atIndex(0)).toBeVisible(); + } + + @Step("Expect Notifications page visible") + async expectNotificationsPageVisible() { + await detoxExpect(element(by.text(this.notificationsHeaderTitle)).atIndex(0)).toBeVisible(); + } + + @Step("Expect Settings page visible") + async expectSettingsPageVisible() { + await waitForElementById(this.settingsCardId); + } +} diff --git a/e2e/mobile/specs/mainNavigationWallet40.spec.ts b/e2e/mobile/specs/mainNavigationWallet40.spec.ts new file mode 100644 index 00000000000..b73ee1dfd16 --- /dev/null +++ b/e2e/mobile/specs/mainNavigationWallet40.spec.ts @@ -0,0 +1,80 @@ +$TmsLink("B2CQA-4383"); +$TmsLink("B2CQA-4385"); +const tags: string[] = ["@NanoSP", "@LNS", "@NanoX", "@Stax", "@Flex", "@NanoGen5"]; +tags.forEach(tag => $Tag(tag)); + +describe("Main Navigation", () => { + beforeAll(async () => { + await app.init({ + userdata: "skip-onboarding", + featureFlags: { + lwmWallet40: { + enabled: true, + params: { + mainNavigation: true, + marketBanner: true, + graphRework: true, + quickActionCtas: true, + }, + }, + }, + }); + await app.mainNavigation.waitForWallet40Ready(); + }); + + it("should show Portfolio with Wallet 4.0 navigation layout", async () => { + await app.mainNavigation.expectPortfolioPageVisible(); + await app.mainNavigation.expectWallet40BottomTabsVisible(); + await app.mainNavigation.expectWallet40TopBarVisible(); + await app.mainNavigation.expectLegacyTabsNotVisible(); + }); + + it("should navigate to Swap via bottom tab", async () => { + await app.mainNavigation.tapWallet40Tab("swap"); + await app.mainNavigation.expectWallet40BottomTabsVisible(); + }); + + it("should navigate to Earn via bottom tab and show Earn page", async () => { + await app.mainNavigation.tapWallet40Tab("earn"); + await app.mainNavigation.expectEarnPageVisible(); + await app.mainNavigation.expectWallet40BottomTabsVisible(); + }); + + it("should navigate to Card via bottom tab and show Card page", async () => { + await app.mainNavigation.tapWallet40Tab("card"); + await app.mainNavigation.expectCardPageVisible(); + await app.mainNavigation.expectWallet40BottomTabsVisible(); + }); + + it("should navigate back to Portfolio via Home tab", async () => { + await app.mainNavigation.tapWallet40Tab("home"); + await app.mainNavigation.expectPortfolioPageVisible(); + await app.mainNavigation.expectWallet40BottomTabsVisible(); + await app.mainNavigation.expectWallet40TopBarVisible(); + }); + + it("should navigate to Discover via top bar and show Web3Hub page", async () => { + await app.mainNavigation.openPortfolioViaDeeplink(); + await app.mainNavigation.tapTopBarDiscover(); + await app.mainNavigation.expectDiscoverPageVisible(); + }); + + it("should navigate to My Ledger via top bar and show Manager page", async () => { + await app.mainNavigation.openPortfolioViaDeeplink(); + await app.mainNavigation.tapTopBarMyLedger(); + await app.mainNavigation.expectMyLedgerPageVisible(); + await app.mainNavigation.tapHeaderBackButton(); + }); + + it("should navigate to Notifications via top bar and show Notifications page", async () => { + await app.mainNavigation.openPortfolioViaDeeplink(); + await app.mainNavigation.tapTopBarNotifications(); + await app.mainNavigation.expectNotificationsPageVisible(); + }); + + it("should navigate to Settings via top bar and show Settings page", async () => { + await app.mainNavigation.openPortfolioViaDeeplink(); + await app.mainNavigation.tapTopBarSettings(); + await app.mainNavigation.expectSettingsPageVisible(); + }); +});