mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebad0ed1b5 | ||
|
|
1498cd425c |
@@ -215,21 +215,29 @@ function makeCdp(snapshot: unknown) {
|
|||||||
if (method === "Runtime.evaluate") {
|
if (method === "Runtime.evaluate") {
|
||||||
return {
|
return {
|
||||||
result: {
|
result: {
|
||||||
value: {
|
deepSerializedValue: {
|
||||||
controls: [
|
type: "array",
|
||||||
|
value: [
|
||||||
{
|
{
|
||||||
state: "filled",
|
type: "array",
|
||||||
sensitive: true,
|
value: [
|
||||||
defaultValue: "hunter2",
|
{ type: "node", value: { backendNodeId: 13 } },
|
||||||
value: "hunter2",
|
{
|
||||||
placeholder: "secret",
|
type: "string",
|
||||||
|
value: JSON.stringify({
|
||||||
|
state: "filled",
|
||||||
|
sensitive: true,
|
||||||
|
placeholder: "secret",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
childFrames: [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (method === "Runtime.releaseObjectGroup") return {};
|
||||||
throw new Error(`unexpected ${method}`);
|
throw new Error(`unexpected ${method}`);
|
||||||
}) as unknown as <T>(tabId: number, method: string, params?: object) => Promise<T>,
|
}) as unknown as <T>(tabId: number, method: string, params?: object) => Promise<T>,
|
||||||
};
|
};
|
||||||
@@ -273,8 +281,11 @@ describe("captureViewModel", () => {
|
|||||||
expect(cdp.send).toHaveBeenCalledWith(
|
expect(cdp.send).toHaveBeenCalledWith(
|
||||||
4,
|
4,
|
||||||
"Runtime.evaluate",
|
"Runtime.evaluate",
|
||||||
expect.objectContaining({ returnByValue: true }),
|
expect.objectContaining({
|
||||||
|
serializationOptions: expect.objectContaining({ serialization: "deep" }),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
expect(cdp.send).toHaveBeenCalledWith(4, "Runtime.releaseObjectGroup", expect.anything());
|
||||||
expect(cdp.send).not.toHaveBeenCalledWith(4, "DOM.resolveNode", expect.anything());
|
expect(cdp.send).not.toHaveBeenCalledWith(4, "DOM.resolveNode", expect.anything());
|
||||||
expect(cdp.send).not.toHaveBeenCalledWith(4, "Runtime.callFunctionOn", expect.anything());
|
expect(cdp.send).not.toHaveBeenCalledWith(4, "Runtime.callFunctionOn", expect.anything());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { type CdpRunner, cdpRunnerForTarget } from "../../shared";
|
||||||
|
import { captureViewModel } from "../capture";
|
||||||
|
|
||||||
|
interface Control {
|
||||||
|
id: number;
|
||||||
|
value: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshot(groups: Control[][]) {
|
||||||
|
const strings: string[] = [];
|
||||||
|
const str = (value: string) => {
|
||||||
|
let index = strings.indexOf(value);
|
||||||
|
if (index < 0) index = strings.push(value) - 1;
|
||||||
|
return index;
|
||||||
|
};
|
||||||
|
const documents = groups.map((controls, groupIndex) => ({
|
||||||
|
frameId: `frame-${groupIndex}`,
|
||||||
|
nodes: {
|
||||||
|
parentIndex: [-1, 0, ...controls.map(() => 1)],
|
||||||
|
nodeName: [str("html"), str("body"), ...controls.map(() => str("input"))],
|
||||||
|
backendNodeId: [8000 + groupIndex * 2, 8001 + groupIndex * 2, ...controls.map((c) => c.id)],
|
||||||
|
attributes: [
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
...controls.map((c) => [
|
||||||
|
str("type"),
|
||||||
|
str(c.type ?? "text"),
|
||||||
|
str("placeholder"),
|
||||||
|
str("snapshot placeholder"),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
inputValue: {
|
||||||
|
index: controls.map((_, i) => i + 2),
|
||||||
|
value: controls.map((c) => str(c.value)),
|
||||||
|
},
|
||||||
|
contentDocumentIndex: { index: [] as number[], value: [] as number[] },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
const root = documents[0].nodes;
|
||||||
|
for (let i = 1; i < documents.length; i++) {
|
||||||
|
root.contentDocumentIndex.index.push(root.backendNodeId.length);
|
||||||
|
root.contentDocumentIndex.value.push(i);
|
||||||
|
root.parentIndex.push(1);
|
||||||
|
root.nodeName.push(str("iframe"));
|
||||||
|
root.backendNodeId.push(9000 + i);
|
||||||
|
root.attributes.push([]);
|
||||||
|
}
|
||||||
|
return { strings, documents };
|
||||||
|
}
|
||||||
|
|
||||||
|
function state(value: string) {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
defaultValue: "",
|
||||||
|
placeholder: "live placeholder",
|
||||||
|
state: value ? "filled" : "empty",
|
||||||
|
sensitive: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function reply(entries: Array<[number, unknown]>) {
|
||||||
|
return {
|
||||||
|
result: {
|
||||||
|
deepSerializedValue: {
|
||||||
|
type: "array",
|
||||||
|
value: entries.map(([backendNodeId, fields]) => ({
|
||||||
|
type: "array",
|
||||||
|
value: [
|
||||||
|
{ type: "node", value: { backendNodeId } },
|
||||||
|
{ type: "string", value: JSON.stringify(fields) },
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fakeCdp(groups: Control[][], evaluate: (params: Record<string, unknown>) => unknown) {
|
||||||
|
const snap = snapshot(groups);
|
||||||
|
const send = vi.fn(async (_tabId: number, method: string, params?: object) => {
|
||||||
|
if (method === "DOMSnapshot.captureSnapshot") return snap;
|
||||||
|
if (method === "Runtime.evaluate") return evaluate(params as Record<string, unknown>);
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
return { send, cdp: { send: send as CdpRunner["send"] } };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("form state identity", () => {
|
||||||
|
it("matches reordered controls by identity and preserves controls missing from the batch", async () => {
|
||||||
|
// The snapshot includes a shadow input. The runtime query skips it,
|
||||||
|
// encounters a new input, and sees the remaining inputs in a new order.
|
||||||
|
const { cdp } = fakeCdp(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{ id: 10, value: "" },
|
||||||
|
{ id: 11, value: "old email" },
|
||||||
|
{ id: 12, value: "old city" },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
() =>
|
||||||
|
reply([
|
||||||
|
[99, state("new control")],
|
||||||
|
[12, state("上海")],
|
||||||
|
[11, state("alice@example.com")],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const captured = await captureViewModel(cdp, 7);
|
||||||
|
expect(captured.nodes.find((n) => n.backendNodeId === 10)).toMatchObject({
|
||||||
|
formValue: "",
|
||||||
|
formState: "empty",
|
||||||
|
formPlaceholder: "snapshot placeholder",
|
||||||
|
});
|
||||||
|
expect(captured.nodes.find((n) => n.backendNodeId === 11)).toMatchObject({
|
||||||
|
formValue: "alice@example.com",
|
||||||
|
});
|
||||||
|
expect(captured.nodes.find((n) => n.backendNodeId === 12)).toMatchObject({ formValue: "上海" });
|
||||||
|
expect(captured.nodes.some((n) => n.backendNodeId === 99)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not shift state between frames when one frame is absent from the runtime batch", async () => {
|
||||||
|
const { cdp } = fakeCdp(
|
||||||
|
[
|
||||||
|
[],
|
||||||
|
[{ id: 11, value: "inaccessible frame" }],
|
||||||
|
[{ id: 21, value: "old second" }],
|
||||||
|
[{ id: 31, value: "old third" }],
|
||||||
|
],
|
||||||
|
() =>
|
||||||
|
reply([
|
||||||
|
[31, state("third frame")],
|
||||||
|
[21, state("second frame")],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const captured = await captureViewModel(cdp, 7);
|
||||||
|
const controls = [...captured.iframeNodes.values()].flat();
|
||||||
|
expect(controls.find((n) => n.backendNodeId === 11)?.formValue).toBe("inaccessible frame");
|
||||||
|
expect(controls.find((n) => n.backendNodeId === 21)?.formValue).toBe("second frame");
|
||||||
|
expect(controls.find((n) => n.backendNodeId === 31)?.formValue).toBe("third frame");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps identical backend ids in different OOPIF targets separate", async () => {
|
||||||
|
const sendToTarget = vi.fn(async (target, method) => {
|
||||||
|
if (method === "DOMSnapshot.captureSnapshot") return snapshot([[{ id: 11, value: "old" }]]);
|
||||||
|
if (method === "Runtime.evaluate") return reply([[11, state(target.sessionId)]]);
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
const send = vi.fn(async () => {
|
||||||
|
throw new Error("unexpected top-level target");
|
||||||
|
});
|
||||||
|
const cdp = { send, sendToTarget } as unknown as CdpRunner;
|
||||||
|
for (const sessionId of ["left-frame", "right-frame"]) {
|
||||||
|
const captured = await captureViewModel(cdpRunnerForTarget(cdp, { tabId: 7, sessionId }), 7);
|
||||||
|
expect(captured.nodes.find((n) => n.backendNodeId === 11)?.formValue).toBe(sessionId);
|
||||||
|
expect(sendToTarget).toHaveBeenCalledWith(
|
||||||
|
{ tabId: 7, sessionId },
|
||||||
|
"Runtime.releaseObjectGroup",
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expect(send).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
"unsupported",
|
||||||
|
"legacy",
|
||||||
|
"malformed",
|
||||||
|
"invalid-state",
|
||||||
|
])("preserves snapshot data when enrichment is %s", async (mode) => {
|
||||||
|
const { cdp, send } = fakeCdp([[{ id: 11, value: "snapshot value" }]], () => {
|
||||||
|
if (mode === "unsupported") throw new Error("serialization unsupported");
|
||||||
|
if (mode === "legacy") return { result: { value: { controls: [state("wrong value")] } } };
|
||||||
|
const result = reply([
|
||||||
|
[11, mode === "invalid-state" ? { value: "wrong value" } : state("wrong value")],
|
||||||
|
]);
|
||||||
|
if (mode === "malformed")
|
||||||
|
result.result.deepSerializedValue.value[0].value[1].value = "not json";
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
const captured = await captureViewModel(cdp, 7);
|
||||||
|
expect(captured.nodes.find((n) => n.backendNodeId === 11)).toMatchObject({
|
||||||
|
formValue: "snapshot value",
|
||||||
|
formPlaceholder: "snapshot placeholder",
|
||||||
|
});
|
||||||
|
expect(send).toHaveBeenCalledWith(7, "Runtime.releaseObjectGroup", expect.anything());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("releases the batch's remote objects when capture is cancelled", async () => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const { cdp, send } = fakeCdp([[{ id: 11, value: "old" }]], () => {
|
||||||
|
controller.abort();
|
||||||
|
return reply([[11, state("new")]]);
|
||||||
|
});
|
||||||
|
await expect(captureViewModel(cdp, 7, { signal: controller.signal })).rejects.toMatchObject({
|
||||||
|
name: "AbortError",
|
||||||
|
});
|
||||||
|
expect(send).toHaveBeenCalledWith(7, "Runtime.releaseObjectGroup", expect.anything());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips entries without node identity while still applying valid entries", async () => {
|
||||||
|
const { cdp } = fakeCdp(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{ id: 11, value: "snapshot value" },
|
||||||
|
{ id: 12, value: "old value" },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
const result = reply([
|
||||||
|
[11, state("wrong value")],
|
||||||
|
[12, state("live value")],
|
||||||
|
]);
|
||||||
|
const element = result.result.deepSerializedValue.value[0].value[0];
|
||||||
|
delete (element.value as { backendNodeId?: number }).backendNodeId;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const captured = await captureViewModel(cdp, 7);
|
||||||
|
expect(captured.nodes.find((n) => n.backendNodeId === 11)?.formValue).toBe("snapshot value");
|
||||||
|
expect(captured.nodes.find((n) => n.backendNodeId === 12)?.formValue).toBe("live value");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
"password",
|
||||||
|
"text",
|
||||||
|
])("redacts a sensitive %s control after identity matching", async (type) => {
|
||||||
|
const { cdp } = fakeCdp([[{ id: 11, value: "secret", type }]], () =>
|
||||||
|
reply([[11, { ...state("secret"), sensitive: true }]]),
|
||||||
|
);
|
||||||
|
const captured = await captureViewModel(cdp, 7);
|
||||||
|
const control = captured.nodes.find((n) => n.backendNodeId === 11);
|
||||||
|
expect(control?.formState).toBe("filled");
|
||||||
|
expect(control?.formValue).toBeUndefined();
|
||||||
|
expect(control?.formDefaultValue).toBeUndefined();
|
||||||
|
expect(control?.attrs.value).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps one bounded batch without per-control CDP queries", async () => {
|
||||||
|
const controls = Array.from({ length: 251 }, (_, i) => ({ id: i + 1, value: "old" }));
|
||||||
|
const elements = controls.map((c) => ({
|
||||||
|
backendNodeId: c.id,
|
||||||
|
tagName: "INPUT",
|
||||||
|
type: "text",
|
||||||
|
value: `live ${c.id}`,
|
||||||
|
defaultValue: "",
|
||||||
|
placeholder: "",
|
||||||
|
}));
|
||||||
|
const { cdp, send } = fakeCdp([controls], (params) => {
|
||||||
|
const doc = {
|
||||||
|
querySelectorAll: (selector: string) =>
|
||||||
|
selector === "input,textarea,select" ? elements : [],
|
||||||
|
};
|
||||||
|
const entries = new Function("document", `return ${params.expression}`)(doc) as Array<
|
||||||
|
[{ backendNodeId: number }, string]
|
||||||
|
>;
|
||||||
|
expect(entries).toHaveLength(250);
|
||||||
|
return reply(entries.map(([element, json]) => [element.backendNodeId, JSON.parse(json)]));
|
||||||
|
});
|
||||||
|
const captured = await captureViewModel(cdp, 7);
|
||||||
|
expect(captured.nodes.find((n) => n.backendNodeId === 250)?.formValue).toBe("live 250");
|
||||||
|
expect(captured.nodes.find((n) => n.backendNodeId === 251)?.formValue).toBe("old");
|
||||||
|
expect(send.mock.calls.filter(([, method]) => method === "Runtime.evaluate")).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
send.mock.calls.filter(([, method]) => method === "Runtime.releaseObjectGroup"),
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(send).not.toHaveBeenCalledWith(7, "DOM.resolveNode", expect.anything());
|
||||||
|
expect(send).not.toHaveBeenCalledWith(7, "Runtime.callFunctionOn", expect.anything());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -190,9 +190,9 @@ interface CapturedFormState {
|
|||||||
sensitive?: boolean;
|
sensitive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CapturedFrameFormState {
|
interface DeepSerializedValue {
|
||||||
controls: CapturedFormState[];
|
type: string;
|
||||||
childFrames: CapturedFrameFormState[];
|
value?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formStateBatchExpression(maxControls: number): string {
|
function formStateBatchExpression(maxControls: number): string {
|
||||||
@@ -215,43 +215,63 @@ function formStateBatchExpression(maxControls: number): string {
|
|||||||
...(sensitive ? {} : { value: rawValue, defaultValue }),
|
...(sensitive ? {} : { value: rawValue, defaultValue }),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
const controls = [];
|
||||||
const collect = (doc) => {
|
const collect = (doc) => {
|
||||||
const controls = [];
|
|
||||||
for (const el of Array.from(doc.querySelectorAll(controlSelector))) {
|
for (const el of Array.from(doc.querySelectorAll(controlSelector))) {
|
||||||
if (remaining <= 0) break;
|
if (remaining <= 0) break;
|
||||||
controls.push(controlState(el));
|
// Deep serialization supplies the node's backend id. Keep the
|
||||||
|
// state as JSON so decoding needs no general-purpose V8 deserializer.
|
||||||
|
controls.push([el, JSON.stringify(controlState(el))]);
|
||||||
remaining -= 1;
|
remaining -= 1;
|
||||||
}
|
}
|
||||||
const childFrames = [];
|
|
||||||
for (const frame of Array.from(doc.querySelectorAll("iframe"))) {
|
for (const frame of Array.from(doc.querySelectorAll("iframe"))) {
|
||||||
|
if (remaining <= 0) break;
|
||||||
let childDoc = null;
|
let childDoc = null;
|
||||||
try { childDoc = frame.contentDocument; } catch { childDoc = null; }
|
try { childDoc = frame.contentDocument; } catch { childDoc = null; }
|
||||||
childFrames.push(childDoc && remaining > 0 ? collect(childDoc) : { controls: [], childFrames: [] });
|
if (childDoc) collect(childDoc);
|
||||||
}
|
}
|
||||||
return { controls, childFrames };
|
|
||||||
};
|
};
|
||||||
return collect(document);
|
collect(document);
|
||||||
|
return controls;
|
||||||
})()`;
|
})()`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenFrameFormStates(
|
function formStatesByBackendId(result: RuntimeEvaluateReply): Map<number, CapturedFormState> {
|
||||||
frame: CapturedFrameFormState | undefined,
|
const states = new Map<number, CapturedFormState>();
|
||||||
out: CapturedFormState[][] = [],
|
const serialized = result.result?.deepSerializedValue;
|
||||||
): CapturedFormState[][] {
|
if (serialized?.type !== "array" || !Array.isArray(serialized.value)) return states;
|
||||||
if (!frame) return out;
|
for (const entry of serialized.value as DeepSerializedValue[]) {
|
||||||
out.push(Array.isArray(frame.controls) ? frame.controls : []);
|
if (entry?.type !== "array" || !Array.isArray(entry.value)) continue;
|
||||||
for (const child of Array.isArray(frame.childFrames) ? frame.childFrames : []) {
|
const [element, json] = entry.value as DeepSerializedValue[];
|
||||||
flattenFrameFormStates(child, out);
|
if (element?.type !== "node" || json?.type !== "string" || typeof json.value !== "string") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const backendNodeId = (element.value as { backendNodeId?: number } | undefined)?.backendNodeId;
|
||||||
|
if (typeof backendNodeId !== "number" || !Number.isSafeInteger(backendNodeId)) continue;
|
||||||
|
try {
|
||||||
|
const state = JSON.parse(json.value) as CapturedFormState | null;
|
||||||
|
if (
|
||||||
|
!state ||
|
||||||
|
!["empty", "filled", "default"].includes(state.state ?? "") ||
|
||||||
|
typeof state.sensitive !== "boolean" ||
|
||||||
|
typeof state.placeholder !== "string" ||
|
||||||
|
(!state.sensitive &&
|
||||||
|
(typeof state.value !== "string" || typeof state.defaultValue !== "string"))
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
states.set(backendNodeId, state);
|
||||||
|
} catch {
|
||||||
|
// A malformed entry must not overwrite the snapshot's own state.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return states;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyFormStates(nodes: CapturedNode[], states: CapturedFormState[]): void {
|
function applyFormStates(nodes: CapturedNode[], states: Map<number, CapturedFormState>): void {
|
||||||
let index = 0;
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (!isFormControlTag(node.tag)) continue;
|
if (!isFormControlTag(node.tag)) continue;
|
||||||
const state = states[index];
|
const state = states.get(node.backendNodeId);
|
||||||
index += 1;
|
|
||||||
if (!state) continue;
|
if (!state) continue;
|
||||||
node.formState = state.state;
|
node.formState = state.state;
|
||||||
node.formPlaceholder = state.placeholder ?? "";
|
node.formPlaceholder = state.placeholder ?? "";
|
||||||
@@ -276,26 +296,37 @@ async function enrichFormControlStates(
|
|||||||
nodes.some((node) => isFormControlTag(node.tag)),
|
nodes.some((node) => isFormControlTag(node.tag)),
|
||||||
);
|
);
|
||||||
if (!hasControls) return;
|
if (!hasControls) return;
|
||||||
|
const objectGroup = `bsk-vom-forms-${crypto.randomUUID()}`;
|
||||||
try {
|
try {
|
||||||
throwIfAborted(signal);
|
throwIfAborted(signal);
|
||||||
const result = await cdp.send<RuntimeEvaluateReply>(tabId, "Runtime.evaluate", {
|
const result = await cdp.send<RuntimeEvaluateReply>(tabId, "Runtime.evaluate", {
|
||||||
expression: formStateBatchExpression(MAX_FORM_ENRICH_CONTROLS),
|
expression: formStateBatchExpression(MAX_FORM_ENRICH_CONTROLS),
|
||||||
returnByValue: true,
|
objectGroup,
|
||||||
|
serializationOptions: {
|
||||||
|
serialization: "deep",
|
||||||
|
maxDepth: 3,
|
||||||
|
additionalParameters: { maxNodeDepth: 0, includeShadowTree: "none" },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
throwIfAborted(signal);
|
throwIfAborted(signal);
|
||||||
const frameStates = flattenFrameFormStates(runtimeValue<CapturedFrameFormState>(result));
|
// Backend ids are scoped to this capture's CDP target. OOPIF targets
|
||||||
for (let i = 0; i < frameNodeGroups.length; i += 1) {
|
// use their own captureViewModel call and never share this lookup.
|
||||||
applyFormStates(frameNodeGroups[i], frameStates[i] ?? []);
|
const states = formStatesByBackendId(result);
|
||||||
|
for (const nodes of frameNodeGroups) {
|
||||||
|
applyFormStates(nodes, states);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isAbortError(error)) throw error;
|
if (isAbortError(error)) throw error;
|
||||||
// Best-effort enrichment. DOMSnapshot/AX data still carries the nodes.
|
// Best-effort enrichment. DOMSnapshot/AX data still carries the nodes.
|
||||||
|
} finally {
|
||||||
|
await cdp.send(tabId, "Runtime.releaseObjectGroup", { objectGroup }).catch(() => undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RuntimeEvaluateReply {
|
interface RuntimeEvaluateReply {
|
||||||
result?: {
|
result?: {
|
||||||
value?: unknown;
|
value?: unknown;
|
||||||
|
deepSerializedValue?: DeepSerializedValue;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user