|
1 | | -import { encapsulatedAction } from '../../framework'; |
| 1 | +import { encapsulatedAction, sleep, createLogger } from '../../framework'; |
2 | 2 | import { |
3 | 3 | encapsulated, |
4 | 4 | EncapsulatedElementType, |
5 | 5 | } from '../../framework/EncapsulatedElement'; |
| 6 | +import { APP_PACKAGE_IDS } from '../../framework/Constants'; |
6 | 7 | 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; |
8 | 34 |
|
9 | 35 | class AndroidScreenHelpers { |
10 | 36 | get openDeeplinkWithMetaMask(): EncapsulatedElementType { |
11 | 37 | return encapsulated({ |
12 | 38 | appium: () => |
13 | | - PlaywrightMatchers.getElementByXPath( |
14 | | - '//android.widget.TextView[@text="MetaMask"]', |
15 | | - ), |
| 39 | + PlaywrightMatchers.getElementByXPath(CHOOSER_METAMASK_XPATHS[0]), |
16 | 40 | }); |
17 | 41 | } |
18 | 42 |
|
| 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 | + */ |
19 | 49 | async tapOpenDeeplinkWithMetaMask(): Promise<void> { |
20 | 50 | await encapsulatedAction({ |
21 | 51 | 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 | + ); |
23 | 87 | }, |
24 | 88 | }); |
25 | 89 | } |
| 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 | + } |
26 | 132 | } |
27 | 133 |
|
28 | 134 | export default new AndroidScreenHelpers(); |
0 commit comments