Files
rea/tests/boundary/browser/cdpRuntimeFrameIdentity.test.ts
T
Can 0154141304 fix(browser): preserve frame identity across captures (#4040)
* fix(browser): preserve capture evidence fidelity

* fix(browser): align frame identity comparisons

* fix(browser): retain historical frame evidence

* fix(electron): retain same-document script owners

* fix(electron): bind frame proof to tree reads

* fix(browser): retain malformed frame scope

* fix(browser): retain WebMCP event scope

* fix(browser): retain malformed timeline identity

* fix(browser): unify frame identity and scope

* fix(electron): prove enclosing frame ancestry

* fix(electron): separate ancestry from file policy

* fix(browser): preserve transient fragment semantics

* fix(browser): retry uncertain initial frame scope

* fix(browser): preserve runtime frame identity

* refactor(browser): separate independent capture fixes

* fix(electron): detect tree-first document replacement

* fix(electron): retain same-document tree authority

---------

Co-authored-by: eco-null <eco-null@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-23 17:19:35 +08:00

169 lines
6.4 KiB
TypeScript

import { BrowserObservationError } from "../../../src/domain/browserObservationError.js";
import { expect, it } from "vitest";
import { CdpWebRuntimeProvider } from "../../../src/browser/execution/CdpWebRuntimeProvider.js";
import { observeWebExecutionInputSchema } from "../../../src/domain/webExecution.js";
import { inspectWebEventListenersInputSchema } from "../../../src/domain/webEventListeners.js";
import { startRuntimeBrowser } from "../../fixtures/webRuntime.js";
import type { FakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
import { trackBrowser } from "./cdpBrowserProvider.support.js";
const actions = ["execution", "listeners"] as const;
const run = async (
browser: FakeCdpBrowser,
action: (typeof actions)[number],
) => {
const provider = new CdpWebRuntimeProvider();
const input = {
cdp_endpoint: browser.endpoint,
target_id: "allowed-page",
allowed_origins: [browser.allowedOrigin],
};
return action === "execution"
? provider.observeExecution(
observeWebExecutionInputSchema.parse({ ...input, observation_ms: 1 }),
)
: provider.inspectEventListeners(
inspectWebEventListenersInputSchema.parse({
...input,
selector: "#selected",
}),
);
};
it.each(
actions.flatMap((action) =>
[false, true].flatMap((loader) =>
["split", "clear", "inline", "credentials"].map((kind) => ({
action,
loader,
kind,
})),
),
),
)(
"preserves full $kind frame identity in $action evidence with loader=$loader",
async ({ action, loader, kind }) => {
const browser = await startRuntimeBrowser({
commandResult: ({ method }, origin) =>
method === "Page.getFrameTree"
? {
frameTree: {
frame: {
id: "runtime-main",
...(loader ? { loaderId: "document-1" } : {}),
url: `${kind === "credentials" ? origin.replace("://", "://user:password@") : origin}/app?caller=retained#old`,
...(kind === "inline"
? {}
: { urlFragment: kind === "clear" ? "" : "#details" }),
},
},
}
: undefined,
});
trackBrowser(browser);
const result = await run(browser, action);
if (!result.ok) throw result.error;
expect(result.value.target).toMatchObject({
initial_url: `${browser.allowedOrigin}/app?caller=retained${kind === "inline" ? "#old" : kind === "clear" ? "" : "#details"}`,
origin: browser.allowedOrigin,
loader_id: loader ? "document-1" : null,
});
},
);
const transitions = [
{ kind: "equivalent", loader: false, reason: null },
{ kind: "fragment-change", loader: false, reason: "target_changed" },
{ kind: "fragment-change", loader: true, reason: null },
{ kind: "malformed", loader: false, reason: "protocol_error" },
{ kind: "malformed", loader: true, reason: "protocol_error" },
{ kind: "foreign-malformed", loader: true, reason: "target_not_allowed" },
{ kind: "foreign-loader-change", loader: true, reason: "target_changed" },
{ kind: "foreign", loader: false, reason: "target_changed" },
{ kind: "foreign", loader: true, reason: "target_not_allowed" },
{ kind: "missing", loader: true, reason: "protocol_error" },
{ kind: "frame-change", loader: true, reason: "target_changed" },
{ kind: "loader-change", loader: true, reason: "target_changed" },
{ kind: "unsupported", loader: true, reason: "target_not_allowed" },
] as const;
it.each(
actions.flatMap((action) => transitions.map((item) => ({ action, ...item }))),
)(
"checks parsed $kind final identity in $action with loader=$loader",
async ({ action, kind, loader, reason }) => {
const browser = await startRuntimeBrowser({
commandResult: ({ method }, origin, reads) => {
if (method !== "Page.getFrameTree") return undefined;
const changed = reads > 1;
const url = `${origin}/app?caller=retained`;
return {
frameTree: {
frame: {
id:
changed && kind === "frame-change"
? "replacement"
: "runtime-main",
...(loader
? {
loaderId:
changed &&
(kind === "loader-change" ||
kind === "foreign-loader-change")
? "replacement"
: "document-1",
}
: {}),
...(changed && kind === "missing"
? {}
: {
url:
changed &&
(kind === "foreign-malformed" ||
kind === "foreign" ||
kind === "foreign-loader-change")
? "https://foreign.example/app"
: changed && kind === "unsupported"
? "about:blank"
: changed && kind === "equivalent"
? `${url}#details`
: url,
}),
...(changed && kind === "equivalent"
? {}
: {
urlFragment:
changed &&
(kind === "malformed" || kind === "foreign-malformed")
? 7
: changed && kind === "fragment-change"
? "#changed"
: "#details",
}),
},
},
};
},
});
trackBrowser(browser);
const result = await run(browser, action);
if (reason === null) {
if (!result.ok) throw result.error;
expect(result.value.target.initial_url).toBe(
`${browser.allowedOrigin}/app?caller=retained#details`,
);
} else {
expect(result).toMatchObject({ ok: false, error: { reason } });
if (kind === "foreign" && loader && !result.ok) {
if (!(result.error instanceof BrowserObservationError))
throw result.error;
expect(result.error.detail).toBe(
`Selected target allowed-page moved to an unselected origin during ${result.error.operation}.`,
);
}
}
const methods = browser.commands.map(({ method }) => method);
expect(methods).toContain("Debugger.disable");
expect(methods).toContain("Target.detachFromTarget");
},
);