fix(record): rebase and dto policy

This commit is contained in:
Ljy-0827
2026-08-23 21:26:40 +08:00
committed by GitHub
parent 87cdcfe5ca
commit 6300eda0aa
5 changed files with 50 additions and 57 deletions
@@ -18,22 +18,13 @@ function sessionWithInput(redactValues = false): RecordingObservationSession {
rootFrameId: "root", rootFrameId: "root",
index: new ObservationNodeIndex({ index: new ObservationNodeIndex({
rootFrameId: "root", rootFrameId: "root",
frameDocuments: [ matchNodes: [
{ {
frameId: "root", frameId: "root",
domNodes: [ backendNodeId: 42,
{ tag: "input",
backendNodeId: 42, rect: { x: 20, y: 40, w: 200, h: 30 },
parentBackendNodeId: null, localRect: { x: 20, y: 40, w: 200, h: 30 },
frameId: "root",
tag: "input",
attrs: {},
rect: { x: 20, y: 40, w: 200, h: 30 },
paintOrder: 1,
position: "static",
pointerEvents: "auto",
},
],
}, },
], ],
refs: [{ ref: "e1", backendNodeId: 42, role: "textbox", name: "Password", line: 1 }], refs: [{ ref: "e1", backendNodeId: 42, role: "textbox", name: "Password", line: 1 }],
@@ -8,7 +8,7 @@ import type { RecordingDraftStep } from "../recording/types";
const OBSERVATION: RegisteredObservation = { const OBSERVATION: RegisteredObservation = {
stateId: "s-next", stateId: "s-next",
rootFrameId: "root", rootFrameId: "root",
index: new ObservationNodeIndex({ rootFrameId: "root", frameDocuments: [], refs: [] }), index: new ObservationNodeIndex({ rootFrameId: "root", matchNodes: [], refs: [] }),
url: "https://example.com/next", url: "https://example.com/next",
}; };
@@ -1,40 +1,50 @@
import type { RenderedRef } from "@browser-skill/vom"; import type { RenderedRef } from "@browser-skill/vom";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import type { CapturedNode } from "@/tools/vom/capture"; import type { CaptureVomMatchNode } from "@/tools/capture-vom-observation";
import { ObservationNodeIndex, type RegisteredObservation } from "../recording/observation-capture"; import { ObservationNodeIndex, type RegisteredObservation } from "../recording/observation-capture";
import { matchObservationTarget } from "../recording/target-matcher"; import { matchObservationTarget } from "../recording/target-matcher";
function node(backendNodeId: number, frameId: string, x = 10): CapturedNode { function node(backendNodeId: number, frameId: string, x = 10): CaptureVomMatchNode {
return { return {
backendNodeId, backendNodeId,
parentBackendNodeId: null,
frameId, frameId,
tag: "button", tag: "button",
attrs: {},
rect: { x, y: 20, w: 100, h: 30 }, rect: { x, y: 20, w: 100, h: 30 },
paintOrder: 1, localRect: { x, y: 20, w: 100, h: 30 },
position: "static",
pointerEvents: "auto",
}; };
} }
function observation( function observation(
documents: Array<{ frameId: string; domNodes: CapturedNode[] }>, matchNodes: CaptureVomMatchNode[],
refs: RenderedRef[], refs: RenderedRef[],
): RegisteredObservation { ): RegisteredObservation {
return { return {
stateId: "s1", stateId: "s1",
rootFrameId: "root", rootFrameId: "root",
index: new ObservationNodeIndex({ rootFrameId: "root", frameDocuments: documents, refs }), index: new ObservationNodeIndex({ rootFrameId: "root", matchNodes, refs }),
url: "https://example.com", url: "https://example.com",
}; };
} }
describe("matchObservationTarget", () => { describe("matchObservationTarget", () => {
it("indexes the safe geometry DTO without discarding frame-local geometry", () => {
const geometry = node(42, "child");
geometry.localRect = { x: 5, y: 6, w: 100, h: 30 };
const ref: RenderedRef = {
ref: "e1",
backendNodeId: 42,
frameId: "child",
line: 1,
};
const candidate = observation([geometry], [ref]).index.candidates("child", "button")[0];
expect(candidate).toEqual({ frameId: "child", geometry, ref });
});
it("matches a unique node using canonical top-level viewport geometry", () => { it("matches a unique node using canonical top-level viewport geometry", () => {
const target = matchObservationTarget({ const target = matchObservationTarget({
observation: observation( observation: observation(
[{ frameId: "root", domNodes: [node(42, "root")] }], [node(42, "root")],
[{ ref: "e1", backendNodeId: 42, role: "button", name: "发布", line: 1 }], [{ ref: "e1", backendNodeId: 42, role: "button", name: "发布", line: 1 }],
), ),
hint: { geometry: { rect: { x: 10, y: 20, w: 100, h: 30 }, tag: "button" } }, hint: { geometry: { rect: { x: 10, y: 20, w: 100, h: 30 }, tag: "button" } },
@@ -45,10 +55,7 @@ describe("matchObservationTarget", () => {
it("uses frame id with backend node id so sibling frames cannot collide", () => { it("uses frame id with backend node id so sibling frames cannot collide", () => {
const target = matchObservationTarget({ const target = matchObservationTarget({
observation: observation( observation: observation(
[ [node(42, "left"), node(42, "right")],
{ frameId: "left", domNodes: [node(42, "left")] },
{ frameId: "right", domNodes: [node(42, "right")] },
],
[ [
{ ref: "e1", backendNodeId: 42, frameId: "left", line: 1 }, { ref: "e1", backendNodeId: 42, frameId: "left", line: 1 },
{ ref: "e2", backendNodeId: 42, frameId: "right", line: 2 }, { ref: "e2", backendNodeId: 42, frameId: "right", line: 2 },
@@ -65,10 +72,7 @@ describe("matchObservationTarget", () => {
it("restricts a missing frame hint to the root frame", () => { it("restricts a missing frame hint to the root frame", () => {
const target = matchObservationTarget({ const target = matchObservationTarget({
observation: observation( observation: observation(
[ [node(42, "child")],
{ frameId: "root", domNodes: [] },
{ frameId: "child", domNodes: [node(42, "child")] },
],
[{ ref: "e1", backendNodeId: 42, frameId: "child", line: 1 }], [{ ref: "e1", backendNodeId: 42, frameId: "child", line: 1 }],
), ),
hint: { geometry: { rect: { x: 10, y: 20, w: 100, h: 30 }, tag: "button" } }, hint: { geometry: { rect: { x: 10, y: 20, w: 100, h: 30 }, tag: "button" } },
@@ -80,7 +84,7 @@ describe("matchObservationTarget", () => {
it("returns unmatched for ambiguous geometry", () => { it("returns unmatched for ambiguous geometry", () => {
const target = matchObservationTarget({ const target = matchObservationTarget({
observation: observation( observation: observation(
[{ frameId: "root", domNodes: [node(42, "root"), node(43, "root")] }], [node(42, "root"), node(43, "root")],
[ [
{ ref: "e1", backendNodeId: 42, line: 1 }, { ref: "e1", backendNodeId: 42, line: 1 },
{ ref: "e2", backendNodeId: 43, line: 2 }, { ref: "e2", backendNodeId: 43, line: 2 },
@@ -94,10 +98,7 @@ describe("matchObservationTarget", () => {
it("uses semantics when geometry is unavailable without crossing frame boundaries", () => { it("uses semantics when geometry is unavailable without crossing frame boundaries", () => {
const target = matchObservationTarget({ const target = matchObservationTarget({
observation: observation( observation: observation(
[ [node(41, "root"), node(42, "child")],
{ frameId: "root", domNodes: [node(41, "root")] },
{ frameId: "child", domNodes: [node(42, "child")] },
],
[ [
{ ref: "e1", backendNodeId: 41, role: "button", name: "保存", line: 1 }, { ref: "e1", backendNodeId: 41, role: "button", name: "保存", line: 1 },
{ ref: "e2", backendNodeId: 42, frameId: "child", role: "button", name: "保存", line: 2 }, { ref: "e2", backendNodeId: 42, frameId: "child", role: "button", name: "保存", line: 2 },
@@ -112,7 +113,7 @@ describe("matchObservationTarget", () => {
it("uses semantics to disambiguate equal geometry in one frame", () => { it("uses semantics to disambiguate equal geometry in one frame", () => {
const target = matchObservationTarget({ const target = matchObservationTarget({
observation: observation( observation: observation(
[{ frameId: "root", domNodes: [node(42, "root"), node(43, "root")] }], [node(42, "root"), node(43, "root")],
[ [
{ ref: "e1", backendNodeId: 42, role: "button", name: "保存", line: 1 }, { ref: "e1", backendNodeId: 42, role: "button", name: "保存", line: 1 },
{ ref: "e2", backendNodeId: 43, role: "button", name: "取消", line: 2 }, { ref: "e2", backendNodeId: 43, role: "button", name: "取消", line: 2 },
@@ -1,11 +1,14 @@
import type { RenderedRef } from "@browser-skill/vom"; import type { RenderedRef } from "@browser-skill/vom";
import { captureVomObservation } from "@/tools/capture-vom-observation"; import {
type CaptureVomMatchNode,
type CaptureVomObservationResult,
captureVomObservation,
} from "@/tools/capture-vom-observation";
import type { CdpRunner, ChromeTabsApi } from "@/tools/shared"; import type { CdpRunner, ChromeTabsApi } from "@/tools/shared";
import type { CapturedNode } from "@/tools/vom/capture";
export interface IndexedObservationNode { export interface IndexedObservationNode {
frameId: string; frameId: string;
node: CapturedNode; geometry: CaptureVomMatchNode;
ref?: RenderedRef; ref?: RenderedRef;
} }
@@ -38,11 +41,7 @@ export class ObservationNodeIndex {
readonly #refById = new Map<string, RenderedRef>(); readonly #refById = new Map<string, RenderedRef>();
readonly #refsByFrame = new Map<string, RenderedRef[]>(); readonly #refsByFrame = new Map<string, RenderedRef[]>();
constructor(input: { constructor(input: Pick<CaptureVomObservationResult, "rootFrameId" | "matchNodes" | "refs">) {
rootFrameId: string;
frameDocuments: Array<{ frameId: string; domNodes: CapturedNode[] }>;
refs: RenderedRef[];
}) {
const refByNode = new Map<string, RenderedRef>(); const refByNode = new Map<string, RenderedRef>();
for (const ref of input.refs) { for (const ref of input.refs) {
const frameId = ref.frameId ?? input.rootFrameId; const frameId = ref.frameId ?? input.rootFrameId;
@@ -52,15 +51,17 @@ export class ObservationNodeIndex {
frameRefs.push(ref); frameRefs.push(ref);
this.#refsByFrame.set(frameId, frameRefs); this.#refsByFrame.set(frameId, frameRefs);
} }
for (const document of input.frameDocuments) { for (const geometry of input.matchNodes) {
for (const node of document.domNodes) { const { frameId } = geometry;
const frameId = node.frameId ?? document.frameId; const entry = {
const entry = { frameId, node, ref: refByNode.get(nodeKey(frameId, node.backendNodeId)) }; frameId,
const key = frameTagKey(frameId, node.tag); geometry,
const bucket = this.#nodesByFrameTag.get(key) ?? []; ref: refByNode.get(nodeKey(frameId, geometry.backendNodeId)),
bucket.push(entry); };
this.#nodesByFrameTag.set(key, bucket); const key = frameTagKey(frameId, geometry.tag);
} const bucket = this.#nodesByFrameTag.get(key) ?? [];
bucket.push(entry);
this.#nodesByFrameTag.set(key, bucket);
} }
} }
@@ -15,7 +15,7 @@ function rectMatches(a: TargetGeometry["rect"], b: TargetGeometry["rect"]): bool
} }
function candidateMatches(candidate: IndexedObservationNode, geometry: TargetGeometry): boolean { function candidateMatches(candidate: IndexedObservationNode, geometry: TargetGeometry): boolean {
return candidate.node.rect !== null && rectMatches(geometry.rect, candidate.node.rect); return candidate.geometry.rect !== null && rectMatches(geometry.rect, candidate.geometry.rect);
} }
export function unmatchedTarget(fallback?: CaptureTargetDescriptor): TargetDescriptorV3 { export function unmatchedTarget(fallback?: CaptureTargetDescriptor): TargetDescriptorV3 {