fix(browser): preserve uncertain main-frame navigation identity

Own CDP frame applicability and identity at the parsed event boundary. Preserve malformed or incomplete main-frame facts, recognize replacement roots, and carry the same document identity through captures, screenshots, timelines, and execution windows.

Validation: full check:ci and docs:check; 763 browser and dispatch tests passed, 6 skipped. Independent source and fixture-lifecycle review passed. Local real-Chrome behavior was not exercised.

Closes #5665
This commit is contained in:
morluto committed 2026-10-21 07:16:47 +08:00
1 parent 9dee989a79
commit 0bd14ca16c
16 files changed
+962 -179

No files matched your search

+32 -7
View File
@@ -25,7 +25,7 @@ import {
type UnknownRecord,
} from "./CdpCaptureValues.js";
import { mainFrameUrl } from "./CdpCaptureDocuments.js";
import { isMainFrameNavigation } from "./CdpCaptureEventHelpers.js";
import { cdpFrameIdentity } from "./CdpCaptureEventHelpers.js";
import { cdpTargetEventMatches } from "./CdpTargetEvents.js";
interface ObservationContext {
@@ -184,7 +184,7 @@ class TimelineCapture {
constructor(
private readonly allowedOrigins: ReadonlySet<string>,
private readonly mainFrameId: string,
private mainFrameId: string,
initialUrl: string,
) {
this.finalUrl =
@@ -204,7 +204,15 @@ class TimelineCapture {
#ingestArmed(event: CdpTransportEvent, bufferedBeforeArm = false): void {
if (this.#endReason !== undefined || this.#scopeExcluded) return;
const params = recordValue(event.params);
if (params === undefined) return;
if (params === undefined) {
if (
event.method === "Page.frameRequestedNavigation" ||
event.method === "Page.frameNavigated" ||
event.method === "Page.navigatedWithinDocument"
)
this.completeness.exclude("timeline", "invalid_protocol_value");
return;
}
const contextOnly =
bufferedBeforeArm && hasNoComparableProducerTimestamp(event.method);
if (contextOnly) this.#hasUnknownEventTime = true;
@@ -326,7 +334,10 @@ class TimelineCapture {
params: UnknownRecord,
contextOnly: boolean,
): void {
if (!isMainFrameNavigation(event, this.mainFrameId)) return;
const identity = cdpFrameIdentity(event, this.mainFrameId);
if (identity.identityIssue !== null || identity.parentIssue !== null)
this.completeness.exclude("timeline", "invalid_protocol_value");
if (identity.selection !== "main") return;
const reason = safeDetail(params.reason);
const rawUrl = cdpStringValue(params.url);
const destination =
@@ -349,12 +360,23 @@ class TimelineCapture {
contextOnly: boolean,
): void {
const frame = recordValue(params.frame);
if (!isMainFrameNavigation(event, this.mainFrameId)) return;
const identity = cdpFrameIdentity(event, this.mainFrameId);
if (identity.identityIssue !== null || identity.parentIssue !== null)
this.completeness.exclude("timeline", "invalid_protocol_value");
if (identity.selection !== "main" && !identity.isRootReplacement) return;
const rawUrl = cdpStringValue(frame?.url);
const destination = scopedUrl(rawUrl, this.allowedOrigins);
if (identity.isRootReplacement) {
this.mainFrameId = identity.frameId ?? this.mainFrameId;
this.#pendingReload = false;
if (contextOnly) {
this.#stopIfOutOfScope(destination);
return;
}
}
this.#add({
type:
!contextOnly && this.#pendingReload
!identity.isRootReplacement && !contextOnly && this.#pendingReload
? "same_origin_reload"
: "navigation_committed",
params: { ...params, frameId: frame?.id, loaderId: frame?.loaderId },
@@ -374,7 +396,10 @@ class TimelineCapture {
params: UnknownRecord,
contextOnly: boolean,
): void {
if (!isMainFrameNavigation(event, this.mainFrameId)) return;
const identity = cdpFrameIdentity(event, this.mainFrameId);
if (identity.identityIssue !== null || identity.parentIssue !== null)
this.completeness.exclude("timeline", "invalid_protocol_value");
if (identity.selection !== "main") return;
const rawUrl = cdpStringValue(params.url);
const destination = scopedUrl(rawUrl, this.allowedOrigins);
this.#add({