Skip to content

Commit b26ed7a

Browse files
fix(test): harden MMConnect Android deeplink chooser handling
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b747f6a commit b26ed7a

3 files changed

Lines changed: 122 additions & 7 deletions

File tree

tests/framework/ChromeCdpHelpers.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,14 @@ export default class ChromeCdpHelpers {
185185
const el = document.querySelector('[data-testid=${JSON.stringify(testId)}]');
186186
if (!el) return false;
187187
el.scrollIntoView({ block: 'center', inline: 'center' });
188-
el.click();
188+
// Prefer a trusted-style activation path for deeplink buttons.
189+
if (typeof el.focus === 'function') el.focus();
190+
el.dispatchEvent(new MouseEvent('click', {
191+
bubbles: true,
192+
cancelable: true,
193+
view: window,
194+
}));
195+
if (typeof el.click === 'function') el.click();
189196
return true;
190197
})()`,
191198
);
Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,134 @@
1-
import { encapsulatedAction } from '../../framework';
1+
import { encapsulatedAction, sleep, createLogger } from '../../framework';
22
import {
33
encapsulated,
44
EncapsulatedElementType,
55
} from '../../framework/EncapsulatedElement';
6+
import { APP_PACKAGE_IDS } from '../../framework/Constants';
67
import PlaywrightMatchers from '../../framework/PlaywrightMatchers';
7-
import UnifiedGestures from '../../framework/UnifiedGestures';
8+
import PlaywrightGestures from '../../framework/PlaywrightGestures';
9+
import PlaywrightUtilities, {
10+
getDriver,
11+
} from '../../framework/PlaywrightUtilities';
12+
import { ConnectAccountBottomSheetSelectorsIDs } from '../../../app/components/Views/MultichainAccounts/shared/ConnectAccountBottomSheet.testIds';
13+
14+
const logger = createLogger({
15+
name: 'AndroidScreenHelpers',
16+
});
17+
18+
/** Exact MetaMask label outside Chrome WebView (Android app chooser). */
19+
const CHOOSER_METAMASK_XPATHS = [
20+
'//android.widget.TextView[@text="MetaMask" and not(ancestor::android.webkit.WebView)]',
21+
'//*[@resource-id="android:id/text1" and @text="MetaMask"]',
22+
'//android.widget.Button[@text="MetaMask"]',
23+
'//*[@content-desc="MetaMask"]',
24+
];
25+
26+
const JUST_ONCE_XPATHS = [
27+
'//android.widget.Button[@resource-id="android:id/button_once"]',
28+
'//android.widget.Button[@text="Just once"]',
29+
'//*[@text="Just once"]',
30+
];
31+
32+
const CHOOSER_TIMEOUT_MS = 20_000;
33+
const POLL_MS = 500;
834

935
class AndroidScreenHelpers {
1036
get openDeeplinkWithMetaMask(): EncapsulatedElementType {
1137
return encapsulated({
1238
appium: () =>
13-
PlaywrightMatchers.getElementByXPath(
14-
'//android.widget.TextView[@text="MetaMask"]',
15-
),
39+
PlaywrightMatchers.getElementByXPath(CHOOSER_METAMASK_XPATHS[0]),
1640
});
1741
}
1842

43+
/**
44+
* After a dapp deeplink, select MetaMask from the Android app chooser.
45+
* On CI emulators MetaMask is often the only handler, so Android skips the
46+
* chooser and opens the app directly — treat that as success. Also collapse
47+
* the status bar so Play services heads-up toasts do not block taps.
48+
*/
1949
async tapOpenDeeplinkWithMetaMask(): Promise<void> {
2050
await encapsulatedAction({
2151
appium: async () => {
22-
await UnifiedGestures.waitAndTap(this.openDeeplinkWithMetaMask);
52+
PlaywrightUtilities.collapseStatusBar();
53+
54+
const deadline = Date.now() + CHOOSER_TIMEOUT_MS;
55+
while (Date.now() < deadline) {
56+
PlaywrightUtilities.collapseStatusBar();
57+
58+
if (await this.isMetaMaskForeground()) {
59+
logger.debug(
60+
'MetaMask already foreground after deeplink; skipping chooser',
61+
);
62+
return;
63+
}
64+
65+
for (const xpath of CHOOSER_METAMASK_XPATHS) {
66+
try {
67+
const option = await PlaywrightMatchers.getElementByXPath(xpath);
68+
if (await option.isVisible()) {
69+
await PlaywrightGestures.waitAndTap(option, {
70+
timeout: 3_000,
71+
delay: 200,
72+
});
73+
await this.tapJustOnceIfPresent();
74+
return;
75+
}
76+
} catch {
77+
// Try next selector
78+
}
79+
}
80+
81+
await sleep(POLL_MS);
82+
}
83+
84+
throw new Error(
85+
`Android MetaMask deeplink chooser not shown (and MetaMask not foreground) after ${CHOOSER_TIMEOUT_MS}ms`,
86+
);
2387
},
2488
});
2589
}
90+
91+
private async isMetaMaskForeground(): Promise<boolean> {
92+
try {
93+
const currentPackage = (await getDriver().execute(
94+
'mobile: getCurrentPackage',
95+
)) as string;
96+
if (
97+
currentPackage === APP_PACKAGE_IDS.ANDROID ||
98+
currentPackage?.startsWith(`${APP_PACKAGE_IDS.ANDROID}.`)
99+
) {
100+
return true;
101+
}
102+
} catch {
103+
// Ignore package probe failures
104+
}
105+
106+
try {
107+
const connectButton = await PlaywrightMatchers.getElementById(
108+
ConnectAccountBottomSheetSelectorsIDs.CONNECT_BUTTON,
109+
);
110+
return await connectButton.isVisible();
111+
} catch {
112+
return false;
113+
}
114+
}
115+
116+
private async tapJustOnceIfPresent(): Promise<void> {
117+
for (const xpath of JUST_ONCE_XPATHS) {
118+
try {
119+
const button = await PlaywrightMatchers.getElementByXPath(xpath);
120+
if (await button.isVisible()) {
121+
await PlaywrightGestures.waitAndTap(button, {
122+
timeout: 2_000,
123+
delay: 100,
124+
});
125+
return;
126+
}
127+
} catch {
128+
// Not present
129+
}
130+
}
131+
}
26132
}
27133

28134
export default new AndroidScreenHelpers();

tests/smoke-appium/mm-connect/connection-multichain.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import AndroidScreenHelpers from '../../page-objects/MMConnect/AndroidScreenHelp
66
import DappConnectionModal from '../../page-objects/MMConnect/DappConnectionModal.js';
77
import PlaywrightContextHelpers from '../../framework/PlaywrightContextHelpers.js';
88
import ChromeCdpHelpers from '../../framework/ChromeCdpHelpers.js';
9+
import PlaywrightUtilities from '../../framework/PlaywrightUtilities.js';
910
import { DappServer, DappVariants, TestDapps } from '../../framework/index.js';
1011
import {
1112
getDappUrlForBrowser,
@@ -66,6 +67,7 @@ appiumTest.describe(SmokeMMConnect('Multichain browser connect'), () => {
6667

6768
// Emulator Chrome 113: Appium WEBVIEW_chrome switch hangs in Chromedriver
6869
// session creation. Drive the dapp via CDP instead.
70+
PlaywrightUtilities.collapseStatusBar();
6971
await ChromeCdpHelpers.waitAndClickTestId(
7072
DAPP_URL,
7173
MMConnectDappTestIds.CONNECT_BUTTON,

0 commit comments

Comments
 (0)