diff --git a/config/scripts/mobile-web-app/preview/mobile-web-app-preview-arm-driver.mjs b/config/scripts/mobile-web-app/preview/mobile-web-app-preview-arm-driver.mjs index 5cc5b98f249..81e04c015a7 100644 --- a/config/scripts/mobile-web-app/preview/mobile-web-app-preview-arm-driver.mjs +++ b/config/scripts/mobile-web-app/preview/mobile-web-app-preview-arm-driver.mjs @@ -3,18 +3,20 @@ */ import { recordRequestsTo } from './mobile-web-app-preview-request-log.mjs' import { watchImageEvidence } from './mobile-web-app-preview-image-evidence.mjs' -import { artifact } from './mobile-web-app-preview-artifact-fixture.mjs' +import { ARTIFACT_RGB, artifact } from './mobile-web-app-preview-artifact-fixture.mjs' import { pollReportsUntil } from './mobile-web-app-preview-csp-reports.mjs' import { describePreviewFrame, untilAborted } from './mobile-web-app-preview-frame-diagnosis.mjs' import { previewFrame, settleAfterMount, + waitForFrameInputRouting, waitForLoadedFrame } from './mobile-web-app-preview-frame-readiness.mjs' import { probePreviewPixel, readPreviewArm, - readPreviewToggles + readPreviewToggles, + waitForPaintedPreview } from './mobile-web-app-preview-frame-readings.mjs' /** @@ -129,6 +131,19 @@ export async function openPreviewArm( document.addEventListener('securitypolicyviolation', (event) => { window.__violations.push(`${event.violatedDirective} ${event.blockedURI || 'inline'}`) }) + // Pointer input the embedder kept for itself over the frame; see `waitForFrameInputRouting`. + if (window === window.top) { + window.__embedderFramePointer = 0 + document.addEventListener( + 'mousemove', + (event) => { + if (event.target instanceof HTMLIFrameElement) { + window.__embedderFramePointer += 1 + } + }, + true + ) + } }) // The nonce in the document's own URL: the policy this response carries names a report endpoint // with the same nonce, which is how a report from a `srcdoc` frame with no URL of its own is @@ -179,7 +194,11 @@ export async function openPreviewArm( // produced, and by then the top frame is mid-navigation and the iframe has blanked to its own // background. So the precondition "there was a rendered artifact to tap" is this reading, and the // one below is only meaningful for a case that did nothing. - const pixelBefore = await probePreviewPixel(page, clip) + // A `'load'` arm's frame leaves the artifact on purpose, so only the others wait for its paint. + const pixelBefore = + frameReady === 'load' + ? await probePreviewPixel(page, clip) + : await waitForPaintedPreview(page, clip, ARTIFACT_RGB, signal) // Sampled before the action as well, because the toggle's whole claim is that it changes. const togglesBefore = await readPreviewToggles(page) let actError = null @@ -189,6 +208,7 @@ export async function openPreviewArm( if (!artifactFrame || artifactFrame.isDetached()) { throw new Error('The loaded preview frame is missing before its action') } + await waitForFrameInputRouting(page, clip, signal, arm) await act({ page, frame: artifactFrame }).catch((error) => { actError = String(error).split('\n')[0] }) diff --git a/config/scripts/mobile-web-app/preview/mobile-web-app-preview-frame-readiness.mjs b/config/scripts/mobile-web-app/preview/mobile-web-app-preview-frame-readiness.mjs index c50c4d390a3..f36a971bd9e 100644 --- a/config/scripts/mobile-web-app/preview/mobile-web-app-preview-frame-readiness.mjs +++ b/config/scripts/mobile-web-app/preview/mobile-web-app-preview-frame-readiness.mjs @@ -18,7 +18,7 @@ import { describePreviewFrame, untilAborted } from './mobile-web-app-preview-frame-diagnosis.mjs' import { pollReportsUntil } from './mobile-web-app-preview-csp-reports.mjs' -const POLL_MS = 25 +export const POLL_MS = 25 const EVALUATE_MS = 1000 /** The mounted preview frame, or null before one exists. */ @@ -26,7 +26,7 @@ export function previewFrame(page) { return page.frames().find((one) => one !== page.mainFrame()) ?? null } -const abandonAfter = (ms) => +export const abandonAfter = (ms) => new Promise((resolve) => { const timer = setTimeout(() => resolve(false), ms) timer.unref?.() @@ -152,6 +152,40 @@ export async function waitForLoadedFrame( return previewFrame(page) } +/** How long a painted frame may go on handing its input to the embedder before the arm gives up. */ +const INPUT_ROUTING_MS = 10_000 + +/** + * Returns once a pointer over the frame reaches the frame rather than the embedder. + * + * Painted is not routed either. Chromium sends input into the frame's own process only once the + * browser holds that frame's hit-test region, and until then an event at the frame's coordinates + * is the embedder's, targeted at its `