Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/run-appium-e2e-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ on:
required: false
type: string
default: 'current'
android-tag:
description: 'Android system image tag (default = AOSP; google_apis includes Chrome for browser dApp tests)'
required: false
type: string
default: 'default'

permissions:
contents: read
Expand Down Expand Up @@ -87,7 +92,7 @@ jobs:
setup-simulator: 'true'
android-avd-name: appium_smoke_avd
android-api-level: '34'
android-tag: default
android-tag: ${{ inputs.android-tag }}
android-abi: x86_64
android-device: pixel_5
configure-keystores: 'false'
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/run-appium-smoke-tests-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,31 @@ jobs:
runner_provider: ${{ inputs.runner_provider }}
secrets: inherit

appium-mmconnect-android-smoke:
if: >-
${{
!cancelled() &&
(contains(fromJson(inputs.selected_tags), 'ALL') ||
contains(fromJson(inputs.selected_tags), 'SmokeMMConnect'))
}}
strategy:
matrix:
split: [1]
fail-fast: false
uses: ./.github/workflows/run-appium-e2e-workflow.yml
with:
test-suite-name: appium-mmconnect-android-smoke-${{ matrix.split }}
platform: android
test_suite_tag: SmokeMMConnect
split_number: ${{ matrix.split }}
total_splits: 1
test-timeout-minutes: 35
android-tag: google_apis
build_type: ${{ inputs.build_type }}
metamask_environment: ${{ inputs.metamask_environment }}
runner_provider: ${{ inputs.runner_provider }}
secrets: inherit

appium-money-android-smoke:
if: >-
${{
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
"run-system-tests:ios-login-sim": "yarn playwright test --project system-ios-login-sim --config tests/playwright.system-emulator.config.ts",
"run-system-tests:ios-onboarding-sim": "yarn playwright test --project system-ios-onboarding-sim --config tests/playwright.system-emulator.config.ts",
"appium-smoke:android": "yarn playwright test --config tests/playwright.smoke-appium.config.ts --project android-smoke",
"appium-smoke:mmconnect:android": "yarn playwright test --config tests/playwright.smoke-appium.config.ts --project android-smoke --grep SmokeMMConnect",
"appium-smoke:ios": "yarn playwright test --config tests/playwright.smoke-appium.config.ts --project ios-smoke",
"capture-visual-baselines:android": "CAPTURE_BASELINES=true AI_VISUAL_TESTING_ENABLED=true yarn playwright test --project system-android-login-emu --config tests/playwright.system-emulator.config.ts",
"capture-visual-baselines:ios": "CAPTURE_BASELINES=true AI_VISUAL_TESTING_ENABLED=true yarn playwright test --project system-ios-login-sim --config tests/playwright.system-emulator.config.ts",
Expand Down
141 changes: 127 additions & 14 deletions tests/flows/native-browser.flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,28 @@ const CHROME_DISMISS_TIMEOUT_MS = 5000;
/** Delay after dismissals so Chrome UI can settle before we interact with the URL bar. Kept short to avoid app auto-lock. */
const CHROME_UI_SETTLE_MS = 800;

/** Extra settle after VIEW-intent navigation so Chrome can finish loading the dapp. */
const CHROME_VIEW_INTENT_SETTLE_MS = 3000;

/**
* Dismisses the "Enhanced ad privacy" dialog if present.
* Dismisses common Chrome first-run / privacy / default-browser dialogs if present.
* @returns void
*/
const dismissChromeAdPrivacyIfPresent = async () => {
const dismissTexts = ['Got it', 'No thanks', 'Skip', 'Continue'];
const dismissTexts = [
'Got it',
'No thanks',
'Skip',
'Continue',
'Accept & continue',
'Accept and continue',
'Use without an account',
'More',
];
for (const text of dismissTexts) {
try {
const element = await PlaywrightMatchers.getElementByText(text);
await PlaywrightGestures.waitAndTap(element);
const dismissControl = await PlaywrightMatchers.getElementByText(text);
await PlaywrightGestures.waitAndTap(dismissControl);
return;
} catch {
// This text not found, try next
Expand All @@ -35,11 +47,12 @@ const dismissChromeAdPrivacyIfPresent = async () => {

/**
* Dismisses the "Chrome notifications make things easier" modal if present.
* Prefer text — resource IDs differ across Chrome versions on emulators.
* @returns void
*/
const dismissChromeNotificationsIfPresent = async () => {
const element = await PlaywrightMatchers.getElementByText('No thanks');
await PlaywrightGestures.waitAndTap(element);
const noThanks = await PlaywrightMatchers.getElementByText('No thanks');
await PlaywrightGestures.waitAndTap(noThanks);
};

/**
Expand Down Expand Up @@ -86,6 +99,43 @@ const safelyOnboardChromeBrowser = async () => {
}
};

/**
* Wait until Chrome NTP/omnibox is interactable, dismissing leftover dialogs.
* google_apis emulator Chrome often uses placeholder text instead of stable IDs.
*/
const waitForChromeNavigationReady = async () => {
const deadline = Date.now() + 20_000;
while (Date.now() < deadline) {
PlaywrightUtilities.collapseStatusBar();
try {
await withTimeout(
dismissChromeNotificationsIfPresent(),
2_000,
'dismissChromeNotificationsReady',
);
} catch {
// Modal not present
}

for (const probe of [
() => asPlaywrightElement(ChromeBrowserView.chromeHomePageSearchBox),
() => asPlaywrightElement(ChromeBrowserView.chromeUrlBar),
() =>
PlaywrightMatchers.getElementByText('Search or type web address', true),
]) {
try {
const chromeTarget = await probe();
if (await chromeTarget.isVisible()) {
return;
}
} catch {
// Try next probe
}
}
await new Promise((r) => setTimeout(r, 500));
}
};

/**
* Launches the mobile browser
* @returns A promise that resolves when the launch is complete
Expand All @@ -98,13 +148,17 @@ export const launchMobileBrowser = async ({
return;
}

PlaywrightUtilities.setupChromeDisableFre();
// Clear before disable-fre so the next cold start picks up chrome-command-line.
PlaywrightUtilities.clearChromeData();
PlaywrightUtilities.setupChromeDisableFre();
PlaywrightUtilities.grantChromeNotificationPermission();
PlaywrightUtilities.forceStopChrome();

await PlaywrightGestures.activateApp(undefined, CHROME_PACKAGE);
if (safelyOnboardChrome) {
await safelyOnboardChromeBrowser();
}
await waitForChromeNavigationReady();
await new Promise((r) => setTimeout(r, CHROME_UI_SETTLE_MS));
};

Expand All @@ -126,17 +180,76 @@ export const switchToMobileBrowser = async () => {
* @returns A promise that resolves when the navigation is complete
*/
export const navigateToDappAndroid = async (url: string) => {
PlaywrightUtilities.collapseStatusBar();

// Prefer VIEW intent — omnibox IDs/text are unreliable on fresh google_apis Chrome.
try {
PlaywrightUtilities.openUrlInChrome(url);
await new Promise((r) => setTimeout(r, CHROME_VIEW_INTENT_SETTLE_MS));
try {
await withTimeout(
dismissChromeAdPrivacyIfPresent(),
CHROME_DISMISS_TIMEOUT_MS,
'dismissChromeAfterViewIntent',
);
} catch {
// No post-navigation dialog
}
// If Chrome is still on the NTP, the intent was ignored — use omnibox.
try {
const ntpSearch = await PlaywrightMatchers.getElementByText(
'Search or type web address',
true,
);
if (!(await ntpSearch.isVisible())) {
return;
}
} catch {
// NTP placeholder absent — assume the dapp URL loaded.
return;
}
} catch {
// Fall back to omnibox UI navigation
}

try {
await ChromeBrowserView.tapSearchBox();
} catch {
// NTP search box not present — tap URL bar directly
try {
// Newer Chrome on google_apis images may not expose search_box_text.
await PlaywrightGestures.waitAndTap(
await PlaywrightMatchers.getElementByText(
'Search or type web address',
true,
),
);
} catch {
// NTP search box not present — tap URL bar directly
}
}
try {
await ChromeBrowserView.tapUrlBar();
} catch {
// Omnibox may already be focused after tapping the search placeholder.
}

try {
await PlaywrightGestures.typeText(
await asPlaywrightElement(ChromeBrowserView.chromeUrlBar),
url,
);
} catch {
const editText = await PlaywrightMatchers.getElementByXPath(
'//android.widget.EditText',
);
await PlaywrightGestures.typeText(editText, url);
}
try {
await ChromeBrowserView.tapSelectDappUrl();
} catch {
// Suggestion row resource IDs vary; Enter submits the omnibox URL.
await PlaywrightGestures.submitAndroidUrlBar();
}
await ChromeBrowserView.tapUrlBar();
await PlaywrightGestures.typeText(
await asPlaywrightElement(ChromeBrowserView.chromeUrlBar),
url,
);
await ChromeBrowserView.tapSelectDappUrl();
};

/**
Expand Down
Loading
Loading