Skip to content

Commit e47b76d

Browse files
perf(e2e): speed up Appium smoke login flow (#32821)
## **Description** Ports the **login performance fixes** from `MMQA-1995-networks-appium-migration` into a focused branch so Appium smoke tests unlock faster without pulling in the full networks migration. **Why:** Appium smoke specs spend ~20s per test probing the developer menu even when the login screen is already visible after fixture bootstrap. That adds up across large smoke shards and slows local/CI feedback. **What changed:** - Skip redundant dev-menu waits when the login screen is already visible - Tighter timeouts for dev-menu probes, login view assertions, and login-button settle time - Extract `wallet-home-readiness.ts` and use iOS-specific wallet detection in `waitForAppReady` for faster session-persisted fast paths - Improve push-notification sheet dismissal after login (tap "Not now" instead of confirm) - Harden Playwright assertions for missing elements during login/dev-menu flows **Files touched:** - `tests/flows/general.flow.ts` - `tests/flows/wallet.flow.ts` - `tests/flows/wallet-home-readiness.ts` (new) - `tests/flows/general.flow.test.ts` - `tests/page-objects/wallet/LoginView.ts` - `tests/framework/PlaywrightAdapter.ts` - `tests/framework/PlaywrightAssertions.ts` ## **Changelog** CHANGELOG entry: null ## **Related issues** Refs: MMQA-1995 — Appium smoke login performance (split from networks Appium migration work) ## **Manual testing steps** ```gherkin Feature: Appium smoke login performance Scenario: login unlock completes without redundant dev-menu waits Given an Appium smoke test that uses loginToAppPlaywright with fixture bootstrap And the app restarts with a persisted session that lands on the login screen When the login flow runs on Android or iOS Then the developer menu is not probed when the login screen is already visible And the wallet home is reached with reduced settle and assertion timeouts ``` - [x] `yarn jest tests/flows/general.flow.test.ts` - [ ] Appium smoke login path validated on Android (local emulator) - [ ] Appium smoke login path validated on iOS (when available) ## **Screenshots/Recordings** N/A — E2E test infrastructure only; no end-user UI changes. CI performance/login job output can be used as validation evidence. ### **Before** N/A ### **After** N/A ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [x] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [x] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [x] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Changes are limited to E2E test infrastructure and timeouts; no production app logic, with modest risk of flakiness if timeouts are too aggressive on slow CI devices. > > **Overview** > **Speeds up Appium smoke login** by cutting redundant waits and tightening probes when the app is already on login or wallet home. > > `loginToAppPlaywright` now tries a **2s wallet-home fast path** first, **skips developer-menu dismissal** when the login screen is visible (avoiding ~20s of absent Continue/xmark probes), uses **shorter login view assertions**, and runs **post-login modal dismissal after** wallet home is ready. Dev-menu Playwright flows use a shared **800ms probe** and **2s** close assertions; unit expectations were updated accordingly. > > **iOS wallet readiness** moves into new `wallet-home-readiness.ts` and is reused in `waitForAppReady` (Appium iOS) and `waitForWalletHomePlaywright`, replacing duplicated logic removed from `wallet.flow.ts`. > > **Push notification existing-user sheet** dismissal now waits for the sheet, taps **Not now** (id or text), and asserts the sheet closes instead of tapping confirm. > > **Framework hardening:** `PlaywrightAdapter.waitForDisplayed` no-ops on missing elements when `reverse` is set; `expectElementToNotBeVisible` treats non-existent elements and certain `TypeError`s as success. **Login** Appium tap uses **10s** timeout, fewer enabled-stable reads, and **300ms** post-enable settle. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f57da7a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: javiergarciavera <javiergarciavera@users.noreply.github.com>
1 parent a96da3f commit e47b76d

7 files changed

Lines changed: 171 additions & 98 deletions

File tree

tests/flows/general.flow.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,10 @@ describe('general.flow Playwright dev screens', () => {
130130
PlaywrightAssertions.expectElementToNotBeVisible,
131131
).toHaveBeenCalledWith(
132132
closeButton,
133-
expect.objectContaining({ timeout: 5000 }),
133+
expect.objectContaining({
134+
timeout: 2000,
135+
description: 'Dev Menu Close Button should not be visible',
136+
}),
134137
);
135138
expect(
136139
(PlaywrightAssertions.expectElementToNotBeVisible as jest.Mock).mock

tests/flows/general.flow.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createLogger } from '../framework/logger';
22
import Assertions from '../framework/Assertions';
33
import {
4+
FrameworkDetector,
45
Gestures,
56
PlaywrightAssertions,
67
PlaywrightGestures,
@@ -12,13 +13,16 @@ import LoginView from '../page-objects/wallet/LoginView';
1213
import WalletView from '../page-objects/wallet/WalletView';
1314
import { PlatformDetector } from '../framework/PlatformLocator';
1415
import { resolveE2EWaitTimeoutMs } from '../framework/Constants';
16+
import { isWalletHomeReadyOnIOS } from './wallet-home-readiness';
1517
// eslint-disable-next-line import-x/no-nodejs-modules
1618
import { execSync } from 'node:child_process';
1719

1820
const logger = createLogger({
1921
name: 'GeneralFlow',
2022
});
2123

24+
const DEV_MENU_PROBE_TIMEOUT_MS = 800;
25+
2226
/**
2327
* Dismisses development build screens.
2428
* Handles 'Development servers' and 'Developer menu' screens.
@@ -119,12 +123,12 @@ const closeDeveloperMenuPlaywright = async (): Promise<void> => {
119123
exact: true,
120124
});
121125
await PlaywrightAssertions.expectElementToBeVisible(closeButton, {
122-
timeout: 2000,
126+
timeout: DEV_MENU_PROBE_TIMEOUT_MS,
123127
description: 'Dev Menu Close Button should be visible',
124128
});
125129
await PlaywrightGestures.waitAndTap(closeButton);
126130
await PlaywrightAssertions.expectElementToNotBeVisible(closeButton, {
127-
timeout: 5000,
131+
timeout: 2000,
128132
description: 'Dev Menu Close Button should not be visible',
129133
});
130134
return;
@@ -141,12 +145,12 @@ const closeDeveloperMenuPlaywright = async (): Promise<void> => {
141145
try {
142146
const closeButton = await PlaywrightMatchers.getElementByText('Close');
143147
await PlaywrightAssertions.expectElementToBeVisible(closeButton, {
144-
timeout: 2000,
148+
timeout: DEV_MENU_PROBE_TIMEOUT_MS,
145149
description: 'Dev Menu Close Button should be visible',
146150
});
147151
await PlaywrightGestures.waitAndTap(closeButton);
148152
await PlaywrightAssertions.expectElementToNotBeVisible(closeButton, {
149-
timeout: 5000,
153+
timeout: 2000,
150154
description: 'Dev Menu Close Button should not be visible',
151155
});
152156
return;
@@ -166,7 +170,7 @@ const dismissDeveloperMenuOnboardingPlaywright = async (): Promise<void> => {
166170
const continueButton =
167171
await PlaywrightMatchers.getElementByText('Continue');
168172
await PlaywrightAssertions.expectElementToBeVisible(continueButton, {
169-
timeout: 5000,
173+
timeout: DEV_MENU_PROBE_TIMEOUT_MS,
170174
description: 'Dev Menu Continue Button should be visible',
171175
});
172176

@@ -238,28 +242,37 @@ export const waitForAppReady = async (
238242
logger.debug('Waiting for app to reach login or wallet home...');
239243

240244
while (Date.now() < deadline) {
241-
try {
242-
await Assertions.expectElementToBeVisible(WalletView.container, {
243-
description: 'Wallet home should be visible',
244-
timeout: 3000,
245-
});
246-
logger.debug(
247-
`App on wallet home after ${Date.now() - startTime}ms — skipping login wait`,
248-
);
249-
return;
250-
} catch {
251-
// Not on wallet yet.
245+
if (FrameworkDetector.isAppium() && PlatformDetector.isIOS()) {
246+
if (await isWalletHomeReadyOnIOS()) {
247+
logger.debug(
248+
`App on wallet home after ${Date.now() - startTime}ms (iOS readiness) — skipping login wait`,
249+
);
250+
return;
251+
}
252+
} else {
253+
try {
254+
await Assertions.expectElementToBeVisible(WalletView.container, {
255+
description: 'Wallet home should be visible',
256+
timeout: 3000,
257+
});
258+
logger.debug(
259+
`App on wallet home after ${Date.now() - startTime}ms — skipping login wait`,
260+
);
261+
return;
262+
} catch {
263+
// Not on wallet yet.
264+
}
252265
}
253266

254267
try {
255268
await Assertions.expectElementToBeVisible(LoginView.container, {
256269
description: 'Login view should be stable',
257270
timeout: 3000,
258271
});
259-
await sleep(1500);
272+
await sleep(500);
260273
await Assertions.expectElementToBeVisible(LoginView.container, {
261274
description: 'Login view should remain visible',
262-
timeout: 2000,
275+
timeout: 1500,
263276
});
264277
logger.debug(`App ready on login after ${Date.now() - startTime}ms`);
265278
return;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import PlaywrightMatchers from '../framework/PlaywrightMatchers';
2+
import { withImplicitWait } from '../framework/PlaywrightUtilities';
3+
import { WalletViewSelectorsIDs } from '../../app/components/Views/Wallet/WalletView.testIds';
4+
import { LoginViewSelectors } from '../../app/components/Views/Login/LoginView.testIds';
5+
6+
const IOS_WALLET_HOME_INDICATOR_IDS = [
7+
WalletViewSelectorsIDs.WALLET_HEADER_ROOT,
8+
WalletViewSelectorsIDs.WALLET_HAMBURGER_MENU_BUTTON,
9+
WalletViewSelectorsIDs.ACCOUNT_ICON,
10+
WalletViewSelectorsIDs.WALLET_SCROLL_VIEW,
11+
WalletViewSelectorsIDs.ACTION_BUTTONS_CONTAINER,
12+
] as const;
13+
14+
const isElementDisplayedById = async (testId: string): Promise<boolean> => {
15+
try {
16+
return await withImplicitWait(500, async () => {
17+
const el = await PlaywrightMatchers.getElementById(testId, {
18+
exact: true,
19+
});
20+
return await el.isVisible();
21+
});
22+
} catch {
23+
return false;
24+
}
25+
};
26+
27+
const isAnyWalletHomeIndicatorDisplayedOnIOS = async (): Promise<boolean> => {
28+
for (const testId of IOS_WALLET_HOME_INDICATOR_IDS) {
29+
if (await isElementDisplayedById(testId)) {
30+
return true;
31+
}
32+
}
33+
return false;
34+
};
35+
36+
const isWalletScreenExistsWithLoginHiddenOnIOS = async (): Promise<boolean> => {
37+
try {
38+
return await withImplicitWait(500, async () => {
39+
const walletScreen = await PlaywrightMatchers.getElementById(
40+
WalletViewSelectorsIDs.WALLET_CONTAINER,
41+
{ exact: true },
42+
);
43+
if (!(await walletScreen.unwrap().isExisting())) {
44+
return false;
45+
}
46+
const loginContainer = await PlaywrightMatchers.getElementById(
47+
LoginViewSelectors.CONTAINER,
48+
{ exact: true },
49+
);
50+
return !(await loginContainer.isVisible());
51+
});
52+
} catch {
53+
return false;
54+
}
55+
};
56+
57+
/**
58+
* iOS Appium wallet readiness — `wallet-screen` may exist but report
59+
* `displayed === false` while child indicators are visible.
60+
*/
61+
export const isWalletHomeReadyOnIOS = async (): Promise<boolean> => {
62+
if (await isAnyWalletHomeIndicatorDisplayedOnIOS()) {
63+
return true;
64+
}
65+
return isWalletScreenExistsWithLoginHiddenOnIOS();
66+
};

tests/flows/wallet.flow.ts

Lines changed: 46 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ import { getPasswordForScenario } from '../framework/utils/TestConstants';
4747
import { resolveE2EWaitTimeoutMs } from '../framework/Constants';
4848
import PlaywrightUtilities, {
4949
getDriver,
50-
withImplicitWait,
5150
} from '../framework/PlaywrightUtilities';
5251
import AccountListBottomSheet from '../page-objects/wallet/AccountListBottomSheet';
5352
import MetaMetricsOptInView from '../page-objects/Onboarding/MetaMetricsOptInView';
@@ -57,73 +56,14 @@ import OnboardingInterestQuestionnaireView from '../page-objects/Onboarding/Onbo
5756
import ExperienceEnhancerBottomSheet from '../page-objects/Onboarding/ExperienceEnhancerBottomSheet';
5857
import { fetchProductionFeatureFlags } from '../performance/feature-flag-helper';
5958
import { ExistingUserSheetSelectorsIDs } from '../../app/components/Views/Notifications/PushNotificationOnboarding/ExistingUserSheet/ExistingUserSheet.testIds';
60-
import { WalletViewSelectorsIDs } from '../../app/components/Views/Wallet/WalletView.testIds';
61-
import { LoginViewSelectors } from '../../app/components/Views/Login/LoginView.testIds';
59+
import { isWalletHomeReadyOnIOS } from './wallet-home-readiness';
6260

6361
const logger = createLogger({
6462
name: 'WalletFlow',
6563
});
6664

67-
const IOS_WALLET_HOME_INDICATOR_IDS = [
68-
WalletViewSelectorsIDs.WALLET_HEADER_ROOT,
69-
WalletViewSelectorsIDs.WALLET_HAMBURGER_MENU_BUTTON,
70-
WalletViewSelectorsIDs.ACCOUNT_ICON,
71-
WalletViewSelectorsIDs.WALLET_SCROLL_VIEW,
72-
WalletViewSelectorsIDs.ACTION_BUTTONS_CONTAINER,
73-
] as const;
74-
7565
const WALLET_HOME_POLL_INTERVAL_MS = 250;
7666

77-
const isElementDisplayedById = async (testId: string): Promise<boolean> => {
78-
try {
79-
return await withImplicitWait(500, async () => {
80-
const el = await PlaywrightMatchers.getElementById(testId, {
81-
exact: true,
82-
});
83-
return await el.isVisible();
84-
});
85-
} catch {
86-
return false;
87-
}
88-
};
89-
90-
const isAnyWalletHomeIndicatorDisplayedOnIOS = async (): Promise<boolean> => {
91-
for (const testId of IOS_WALLET_HOME_INDICATOR_IDS) {
92-
if (await isElementDisplayedById(testId)) {
93-
return true;
94-
}
95-
}
96-
return false;
97-
};
98-
99-
const isWalletScreenExistsWithLoginHiddenOnIOS = async (): Promise<boolean> => {
100-
try {
101-
return await withImplicitWait(500, async () => {
102-
const walletScreen = await PlaywrightMatchers.getElementById(
103-
WalletViewSelectorsIDs.WALLET_CONTAINER,
104-
{ exact: true },
105-
);
106-
if (!(await walletScreen.unwrap().isExisting())) {
107-
return false;
108-
}
109-
const loginContainer = await PlaywrightMatchers.getElementById(
110-
LoginViewSelectors.CONTAINER,
111-
{ exact: true },
112-
);
113-
return !(await loginContainer.isVisible());
114-
});
115-
} catch {
116-
return false;
117-
}
118-
};
119-
120-
const isWalletHomeReadyOnIOS = async (): Promise<boolean> => {
121-
if (await isAnyWalletHomeIndicatorDisplayedOnIOS()) {
122-
return true;
123-
}
124-
return isWalletScreenExistsWithLoginHiddenOnIOS();
125-
};
126-
12767
/**
12868
* Waits for the wallet home screen to be ready after login.
12969
* On iOS, `wallet-screen` may exist but report `displayed === false` while
@@ -659,25 +599,46 @@ export const loginToApp = async (password?: string): Promise<void> => {
659599
export const dismissPushNotificationExistingUserSheet =
660600
async (): Promise<void> => {
661601
try {
662-
await withImplicitWait(500, async () => {
663-
const btn = await asPlaywrightElement(
602+
const sheetTitle = await asPlaywrightElement(
603+
encapsulated({
604+
detox: () =>
605+
Matchers.getElementByID(ExistingUserSheetSelectorsIDs.TITLE),
606+
appium: () =>
607+
PlaywrightMatchers.getElementByText('Never miss a move', true),
608+
}),
609+
);
610+
await PlaywrightAssertions.expectElementToBeVisible(sheetTitle, {
611+
timeout: 5_000,
612+
description: 'Push notification existing user sheet',
613+
});
614+
615+
try {
616+
const notNowById = await asPlaywrightElement(
664617
encapsulated({
665618
detox: () =>
666619
Matchers.getElementByID(
667-
ExistingUserSheetSelectorsIDs.BUTTON_CONFIRM,
620+
ExistingUserSheetSelectorsIDs.BUTTON_NOT_NOW,
668621
),
669622
appium: () =>
670623
PlaywrightMatchers.getElementById(
671-
ExistingUserSheetSelectorsIDs.BUTTON_CONFIRM,
624+
ExistingUserSheetSelectorsIDs.BUTTON_NOT_NOW,
672625
{ exact: true },
673626
),
674627
}),
675628
);
676-
if (await btn.unwrap().isDisplayed()) {
677-
await PlaywrightGestures.waitAndTap(btn, { timeout: 5_000 });
678-
logger.debug('Dismissed push notification existing user sheet');
679-
}
629+
await PlaywrightGestures.waitAndTap(notNowById, { timeout: 5_000 });
630+
} catch {
631+
const notNowByText = await asPlaywrightElement(
632+
PlaywrightMatchers.getElementByText('Not now', true),
633+
);
634+
await PlaywrightGestures.waitAndTap(notNowByText, { timeout: 5_000 });
635+
}
636+
637+
await PlaywrightAssertions.expectElementToNotBeVisible(sheetTitle, {
638+
timeout: 10_000,
639+
description: 'Push notification existing user sheet should close',
680640
});
641+
logger.debug('Dismissed push notification existing user sheet');
681642
} catch {
682643
// Sheet not present — no-op
683644
}
@@ -710,29 +671,39 @@ export const loginToAppPlaywright = async (
710671

711672
await dismissAndroidSystemOverlaysPlaywright();
712673
await waitForAppReady(resolveE2EWaitTimeoutMs(60_000));
713-
await dismissDeveloperMenuPlaywright();
714-
await dismissAndroidSystemOverlaysPlaywright();
715674

675+
// Fast path: already on wallet home (e.g. session persisted across tests).
716676
try {
717-
await waitForWalletHomePlaywright(resolveE2EWaitTimeoutMs(3_000));
677+
await waitForWalletHomePlaywright(2_000);
718678
await dismissPostLoginModals();
719679
return;
720680
} catch {
721681
// Login screen expected — continue below.
722682
}
723683

684+
// Dev menu overlays wallet/explore, not the login screen. Probing it while
685+
// login is visible wastes ~20s on absent Continue/xmark/Close elements.
686+
const onLoginScreen = await Utilities.isElementVisible(
687+
LoginView.container,
688+
1500,
689+
);
690+
if (!onLoginScreen) {
691+
await dismissDeveloperMenuPlaywright();
692+
await dismissAndroidSystemOverlaysPlaywright();
693+
}
694+
724695
await PlaywrightAssertions.expectElementToBeVisible(
725696
asPlaywrightElement(LoginView.container),
726697
{
727698
description: 'Login view container',
728-
timeout: resolveE2EWaitTimeoutMs(30_000),
699+
timeout: 5_000,
729700
},
730701
);
731702
await PlaywrightAssertions.expectElementToBeVisible(
732703
asPlaywrightElement(LoginView.passwordInput),
733704
{
734705
description: 'Login password input',
735-
timeout: resolveE2EWaitTimeoutMs(10_000),
706+
timeout: 3_000,
736707
},
737708
);
738709

@@ -741,9 +712,8 @@ export const loginToAppPlaywright = async (
741712
await LoginView.enterPassword(password ?? '');
742713
await LoginView.tapLoginButton();
743714

744-
await dismissPostLoginModals();
745-
746715
await waitForWalletHomePlaywright(resolveE2EWaitTimeoutMs(30_000));
716+
await dismissPostLoginModals();
747717
};
748718

749719
/**

0 commit comments

Comments
 (0)