diff --git a/config/scripts/mobile-web-app-html-preview-render.test.mjs b/config/scripts/mobile-web-app-html-preview-render.test.mjs
index 16943fd7f39..b1d34290b4b 100644
--- a/config/scripts/mobile-web-app-html-preview-render.test.mjs
+++ b/config/scripts/mobile-web-app-html-preview-render.test.mjs
@@ -493,7 +493,10 @@ for (const engine of ['chromium', 'webkit']) {
const inherited = await open(browser(), {
signal: ctx.signal,
extra: { body: script() },
- sandbox: 'allow-scripts allow-top-navigation-by-user-activation'
+ sandbox: 'allow-scripts allow-top-navigation-by-user-activation',
+ // The refusal below is this arm's oracle, and it is queued behind the frame's load, so the
+ // arm waits for it instead of reading whatever the list happens to hold.
+ frameReady: 'refusal'
})
expect(inherited.pixel).toBe(ARTIFACT_RGB)
expect(inherited.inside?.ran).toBe(0)
@@ -503,6 +506,8 @@ for (const engine of ['chromium', 'webkit']) {
// every line above under a name that says the policy held. A violation raised inside the
// frame can only happen if the sandbox let the script start, so this is the reading that
// separates the two -- and it is the frame's own list, since the embedder's never sees it.
+ // Waited for, not hoped for: `frameReady: 'refusal'` above is what makes this line arrive
+ // after the entry rather than beside the image refusal that happened to be first.
expect(String(inherited.inside?.violations)).toContain('script-src')
// The sealed arm is the contrast: no policy refused anything there, the sandbox simply never
// let the script begin.
@@ -729,8 +734,9 @@ describe('the HTML preview needs no policy change', () => {
* `frameReady` is which of those an arm is waiting for, because the marker is not always the right
* one. `'script'` waits for what the inline script writes: the marker element exists from parse
* time, so an arm whose oracle is "the script ran" would otherwise read `window.__ran` before it
- * had. `'load'` is for the one arm whose artifact deliberately navigates the frame somewhere else,
- * where no marker is ever coming.
+ * had. `'refusal'` waits for the frame's own `script-src` violation, which is queued and can land
+ * after `load`. `'load'` is for the one arm whose artifact deliberately navigates the frame
+ * somewhere else, where no marker is ever coming.
*/
async function waitForLoadedFrame(page, frameReady = 'artifact', signal, browserVersion, arm) {
const element = await page.waitForSelector('iframe', { timeout: 0 })
@@ -747,6 +753,24 @@ async function waitForLoadedFrame(page, frameReady = 'artifact', signal, browser
`the artifact's script never ran inside the frame: ${arm} | ${await describePreviewFrame(page, frame, browserVersion)}`
)
}
+ if (frameReady === 'refusal') {
+ // The violation is dispatched as a queued task, so its order against the frame's `load` is not
+ // guaranteed: on the runner's Chrome the list held only the blocked background image when the
+ // reading was taken, and the arm that needs the script-src entry read it before it landed. So
+ // the arm waits for its own evidence rather than hoping to be later than a task queue. A frame
+ // that was never widened never raises it at all, which is what makes this the arm's precondition
+ // and not a convenience: the wait ends in the diagnosis below rather than in a passing read.
+ await untilAborted(
+ frame.waitForFunction(
+ () => (window.__violations ?? []).some((one) => String(one).includes('script-src')),
+ undefined,
+ { timeout: 0 }
+ ),
+ signal,
+ async () =>
+ `the frame never reported a script-src refusal: ${arm} | ${await describePreviewFrame(page, frame, browserVersion)}`
+ )
+ }
if (frameReady !== 'load') {
await untilAborted(
frame.waitForSelector('#marker', { state: 'attached', timeout: 0 }),