From b5e82065f3b7af37bd4874e1af661e0fdb6f9c8d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 21 Sep 2026 07:41:09 -0400 Subject: [PATCH] test(mobile): time the artifact image against the frame's attachment CI's second read showed the frame did issue the request -- it has a resource-timing entry and decode rejected with EncodingError -- while the rig saw only the CSS background, and a fresh image created later from the same frame was both issued and seen. The remaining question is whether the entry starts before anything was listening to that frame. So the entry is now reported in full for the element under test: responseStatus, transferSize, encodedBodySize, nextHopProtocol, startTime and duration. A zero status with a zero transferSize is a fetch that reached the network stack and came back with nothing, which is what an unintercepted request looks like once `.invalid` fails to resolve. Both sides of the comparison get a wall clock: `Target.attachedToTarget` and Playwright's own `frameattached` now carry the moment they fired, and every recorded request carries the moment it was seen. An entry that starts before the attachment is the race stated rather than inferred. Abort path only; the passing run is unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- .../mobile-web-app-preview-image-evidence.mjs | 15 ++++++++++++++ .../mobile-web-app-preview-request-log.mjs | 20 +++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/config/scripts/mobile-web-app-preview-image-evidence.mjs b/config/scripts/mobile-web-app-preview-image-evidence.mjs index 223101e0b31..3a623856f09 100644 --- a/config/scripts/mobile-web-app-preview-image-evidence.mjs +++ b/config/scripts/mobile-web-app-preview-image-evidence.mjs @@ -61,6 +61,21 @@ async function describeImageEvidence(page, frame, { originPrefix, requestLog, pa // Every subresource this document actually fetched, from the document's own side. An entry // here for a URL the rig never saw would mean the request left the frame and died before it. resources: performance.getEntriesByType('resource').map((one) => one.name), + // The same entry in full for the element under test. A zero `responseStatus` with a zero + // `transferSize` is a fetch that reached the network stack and came back with no response, + // which is what a request the rig never intercepted looks like once the host cannot resolve; + // `startTime` is what an attachment time is early or late against. + remoteTiming: performance + .getEntriesByType('resource') + .filter((one) => one.name === remote?.src) + .map((one) => ({ + responseStatus: one.responseStatus ?? null, + transferSize: one.transferSize, + encodedBodySize: one.encodedBodySize, + nextHopProtocol: one.nextHopProtocol, + startTime: Math.round(one.startTime), + duration: Math.round(one.duration) + })), navigations: performance.getEntriesByType('navigation').map((one) => one.type), remote: remote ? { diff --git a/config/scripts/mobile-web-app-preview-request-log.mjs b/config/scripts/mobile-web-app-preview-request-log.mjs index 5ee208d7774..77464c6fa26 100644 --- a/config/scripts/mobile-web-app-preview-request-log.mjs +++ b/config/scripts/mobile-web-app-preview-request-log.mjs @@ -27,9 +27,15 @@ export async function recordRequestsTo(page, originPrefix) { page.on('request', (request) => { if (request.url().startsWith(originPrefix)) { - asked.push(request.url()) + asked.push({ url: request.url(), at: Math.round(performance.now()) }) } }) + // When this page first had any frame at all, so an attachment time has something to be early or + // late against. + const frameAttached = [] + page.on('frameattached', (frame) => { + frameAttached.push({ url: frame.url(), at: Math.round(performance.now()) }) + }) page.on('requestfailed', (request) => { if (request.url().startsWith(originPrefix)) { failed.push({ url: request.url(), errorText: request.failure()?.errorText ?? null }) @@ -47,7 +53,13 @@ export async function recordRequestsTo(page, originPrefix) { // Playwright did record. Flattened auto-attach puts each child target on this same connection, // and `Network.enable` on the child is what makes its requests visible here. cdp.on('Target.attachedToTarget', (event) => { - attached.push({ type: event.targetInfo?.type ?? null, url: event.targetInfo?.url ?? null }) + // The moment, not just the fact: a request whose resource-timing entry starts before this was + // issued by a frame nothing was listening to yet, which is a different bug from a refusal. + attached.push({ + type: event.targetInfo?.type ?? null, + url: event.targetInfo?.url ?? null, + at: Math.round(performance.now()) + }) cdp.send('Network.enable', {}, event.sessionId).catch(() => {}) }) await cdp @@ -83,8 +95,8 @@ export async function recordRequestsTo(page, originPrefix) { } return { - asked: () => [...asked], + asked: () => asked.map((one) => one.url), describe: () => - `asked ${JSON.stringify(asked)}; failed ${JSON.stringify(failed)}; cdp ${cdp ? 'on' : 'off'} attached ${JSON.stringify(attached)} sent ${JSON.stringify(sent)}; cdp loadingFailed ${JSON.stringify(loadingFailed)}` + `asked ${JSON.stringify(asked)}; failed ${JSON.stringify(failed)}; frameAttached ${JSON.stringify(frameAttached)}; cdp ${cdp ? 'on' : 'off'} attached ${JSON.stringify(attached)} sent ${JSON.stringify(sent)}; cdp loadingFailed ${JSON.stringify(loadingFailed)}` } }