fix: stabilize browser and Android verification
This commit is contained in:
parent
2d83b393f7
commit
4323ee1a7b
|
|
@ -64,7 +64,10 @@ RUN android --no-metrics --sdk="${ANDROID_HOME}" sdk install \
|
||||||
"${ANDROID_EMULATOR_SYSTEM_IMAGE}" \
|
"${ANDROID_EMULATOR_SYSTEM_IMAGE}" \
|
||||||
&& chown -R gradle:gradle "${ANDROID_HOME}"
|
&& chown -R gradle:gradle "${ANDROID_HOME}"
|
||||||
|
|
||||||
ARG ANDROID_EMULATOR_DATA_PARTITION_SIZE=4G
|
# The device suite stores only the two APKs and short-lived test data. A 1 GiB
|
||||||
|
# userdata partition passed the complete API 30/34/37 matrix on 2026-07-20;
|
||||||
|
# 4 GiB made API 34 exceed Docker's writable-layer space before boot.
|
||||||
|
ARG ANDROID_EMULATOR_DATA_PARTITION_SIZE=1G
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
|
|
||||||
|
|
@ -41,4 +41,7 @@ services:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
volumes:
|
volumes:
|
||||||
- ${E2E_OUTPUT_DIR:?Set an ignored E2E_OUTPUT_DIR}:/work/output
|
- ${E2E_OUTPUT_DIR:?Set an ignored E2E_OUTPUT_DIR}:/work/output
|
||||||
networks: [internal]
|
# The browser needs the application/data plane on `internal` and the
|
||||||
|
# Traefik service on `ingress`. Both networks are internal in compose.yaml,
|
||||||
|
# so the isolated E2E runner still has no route to the public Internet.
|
||||||
|
networks: [internal, ingress]
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,13 @@ export function captureBrowserFailures(
|
||||||
options: { recoverableOneTimeNavigations?: Set<string> } = {},
|
options: { recoverableOneTimeNavigations?: Set<string> } = {},
|
||||||
): () => void {
|
): () => void {
|
||||||
const failures: string[] = [];
|
const failures: string[] = [];
|
||||||
|
const pendingTransientNavigations = new Map<string, string>();
|
||||||
|
|
||||||
|
page.on("framenavigated", (frame) => {
|
||||||
|
if (frame === page.mainFrame()) {
|
||||||
|
pendingTransientNavigations.delete(frame.url());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
page.on("console", (message) => {
|
page.on("console", (message) => {
|
||||||
if (message.type() === "error") {
|
if (message.type() === "error") {
|
||||||
|
|
@ -179,12 +186,25 @@ export function captureBrowserFailures(
|
||||||
const recoverableOneTimeNavigation =
|
const recoverableOneTimeNavigation =
|
||||||
options.recoverableOneTimeNavigations?.has(request.url()) === true &&
|
options.recoverableOneTimeNavigations?.has(request.url()) === true &&
|
||||||
isTransientNetworkError(failure);
|
isTransientNetworkError(failure);
|
||||||
|
const pendingTransientNavigation =
|
||||||
|
request.isNavigationRequest() &&
|
||||||
|
new URL(request.url()).origin === new URL(baseURL).origin &&
|
||||||
|
isTransientNetworkError(failure);
|
||||||
|
|
||||||
// MapLibre cancels in-flight raster tiles when a LiveView navigation
|
// MapLibre cancels in-flight raster tiles when a LiveView navigation
|
||||||
// destroys or re-centres a map. Chromium also aborts the old document's
|
// destroys or re-centres a map. Chromium also aborts the old document's
|
||||||
// static assets when a bounded navigation retry replaces that document.
|
// static assets when a bounded navigation retry replaces that document.
|
||||||
// Actual tile/network failures retain their engine error and remain test
|
// A same-origin document navigation may fail once while the public tunnel
|
||||||
// failures.
|
// changes networks. Keep that failure pending until the same URL actually
|
||||||
|
// commits; an unrecovered navigation remains a test failure.
|
||||||
|
if (pendingTransientNavigation) {
|
||||||
|
pendingTransientNavigations.set(
|
||||||
|
request.url(),
|
||||||
|
`requestfailed: ${request.method()} ${request.url()} ${failure}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(rasterTile && cancelled) ||
|
(rasterTile && cancelled) ||
|
||||||
replacedDocumentAsset ||
|
replacedDocumentAsset ||
|
||||||
|
|
@ -198,7 +218,10 @@ export function captureBrowserFailures(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => expect(failures, failures.join("\n")).toEqual([]);
|
return () => {
|
||||||
|
const unresolved = [...failures, ...pendingTransientNavigations.values()];
|
||||||
|
expect(unresolved, unresolved.join("\n")).toEqual([]);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function latestMessageID(
|
export async function latestMessageID(
|
||||||
|
|
@ -350,18 +373,12 @@ export async function loginWithPassword(
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||||
await gotoWithTransientRetry(page, "/users/log-in");
|
|
||||||
const form = page.locator("#login_form_password");
|
|
||||||
await form.getByLabel("Email").fill(email);
|
|
||||||
await form.getByLabel("Password").fill(password);
|
|
||||||
await expect(form.getByLabel("Password")).toHaveValue(password);
|
|
||||||
|
|
||||||
let navigationFailure = "";
|
let navigationFailure = "";
|
||||||
const recordNavigationFailure = (request: Request): void => {
|
const recordNavigationFailure = (request: Request): void => {
|
||||||
if (
|
if (
|
||||||
request.isNavigationRequest() &&
|
request.isNavigationRequest() &&
|
||||||
request.method() === "POST" &&
|
new URL(request.url()).origin === new URL(baseURL).origin &&
|
||||||
new URL(request.url()).pathname === "/users/log-in"
|
isTransientNetworkError(request.failure()?.errorText ?? "")
|
||||||
) {
|
) {
|
||||||
navigationFailure = request.failure()?.errorText ?? "";
|
navigationFailure = request.failure()?.errorText ?? "";
|
||||||
}
|
}
|
||||||
|
|
@ -370,8 +387,16 @@ export async function loginWithPassword(
|
||||||
page.on("requestfailed", recordNavigationFailure);
|
page.on("requestfailed", recordNavigationFailure);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await gotoWithTransientRetry(page, "/users/log-in");
|
||||||
|
const form = page.locator("#login_form_password");
|
||||||
|
await form.getByLabel("Email").fill(email);
|
||||||
|
await form.getByLabel("Password").fill(password);
|
||||||
|
await expect(form.getByLabel("Password")).toHaveValue(password);
|
||||||
await form.getByRole("button", { name: "Log in only this time" }).click();
|
await form.getByRole("button", { name: "Log in only this time" }).click();
|
||||||
await expect(page).not.toHaveURL(/\/users\/log-in$/, { timeout: 10_000 });
|
await expect(page).not.toHaveURL(/\/users\/log-in$/, { timeout: 10_000 });
|
||||||
|
await expect(page.getByText(email, { exact: true })).toBeVisible({
|
||||||
|
timeout: 10_000,
|
||||||
|
});
|
||||||
return { context, page, email };
|
return { context, page, email };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (await page.getByText("Invalid email or password").isVisible()) {
|
if (await page.getByText("Invalid email or password").isVisible()) {
|
||||||
|
|
@ -380,7 +405,10 @@ export async function loginWithPassword(
|
||||||
|
|
||||||
if (
|
if (
|
||||||
attempt === 3 ||
|
attempt === 3 ||
|
||||||
!isTransientNetworkError(navigationFailure)
|
(!isTransientNetworkError(navigationFailure) &&
|
||||||
|
!isTransientNetworkError(
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
))
|
||||||
) {
|
) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -379,6 +379,11 @@ fi
|
||||||
|
|
||||||
run_browser_phase observe_stopped
|
run_browser_phase observe_stopped
|
||||||
run_fixture_tool verify >"$output_dir/fixture-verify.log"
|
run_fixture_tool verify >"$output_dir/fixture-verify.log"
|
||||||
|
docker run --rm \
|
||||||
|
--volume "$output_dir:/output" \
|
||||||
|
--entrypoint sh \
|
||||||
|
"$tools_image" \
|
||||||
|
-euc "chown $(id -u):$(id -g) /output/fixture-verification.json; chmod 600 /output/fixture-verification.json"
|
||||||
|
|
||||||
adb logcat -d -s WhoNeedHelpWebView:D AndroidRuntime:E '*:S' \
|
adb logcat -d -s WhoNeedHelpWebView:D AndroidRuntime:E '*:S' \
|
||||||
>"$output_dir/logcat-before-cleanup.txt"
|
>"$output_dir/logcat-before-cleanup.txt"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ ANDROID_ENV="$ROOT/.env"
|
||||||
TEST_ENV="$ROOT/.env.android-test"
|
TEST_ENV="$ROOT/.env.android-test"
|
||||||
run_id=$(date -u +%Y%m%d%H%M%S)-$$
|
run_id=$(date -u +%Y%m%d%H%M%S)-$$
|
||||||
android_api=${WNH_ANDROID_TEST_API:-37.0}
|
android_api=${WNH_ANDROID_TEST_API:-37.0}
|
||||||
android_data_partition_size=${WNH_ANDROID_TEST_DATA_PARTITION_SIZE:-4G}
|
# 1G is measured against the complete API 30/34/37 suite. It can still be
|
||||||
|
# overridden for a future test that intentionally stores more device data.
|
||||||
|
android_data_partition_size=${WNH_ANDROID_TEST_DATA_PARTITION_SIZE:-1G}
|
||||||
|
|
||||||
if ! printf '%s\n' "$android_data_partition_size" |
|
if ! printf '%s\n' "$android_data_partition_size" |
|
||||||
grep -Eq '^[1-9][0-9]*[GKM]$'; then
|
grep -Eq '^[1-9][0-9]*[GKM]$'; then
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user