Skip to content

Commit 450219c

Browse files
Ljy-0827iuyo5678
authored andcommitted
fix(recorder): bug fix, oopif load too late
1 parent 6c7bbb5 commit 450219c

5 files changed

Lines changed: 319 additions & 21 deletions

File tree

apps/extension/src/lib/__tests__/record-frame-coordinator.test.ts

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
2-
import { RECORD_FRAME_PORT } from "../recording/frame-bridge";
2+
import { RECORD_FRAME_PORT, RECORD_FRAME_QUERY } from "../recording/frame-bridge";
33
import { RecordFrameCoordinator } from "../recording/frame-coordinator";
44

55
class ListenerSet<T extends (...args: never[]) => unknown> {
@@ -46,6 +46,98 @@ describe("RecordFrameCoordinator", () => {
4646
});
4747
});
4848

49+
it("answers child-document queries for the initial tab before it is armed", () => {
50+
const coordinator = new RecordFrameCoordinator({
51+
getAllFrames: async () => [],
52+
sendToDocument: async () => ({ ok: true }),
53+
});
54+
coordinator.attach();
55+
coordinator.begin("rec-1", 10, 3);
56+
57+
const query = (tabId: number) => {
58+
let response: unknown;
59+
for (const listener of onMessage.listeners) {
60+
listener(
61+
{ type: RECORD_FRAME_QUERY },
62+
{
63+
tab: { id: tabId },
64+
frameId: 7,
65+
documentId: "child-document",
66+
} as chrome.runtime.MessageSender,
67+
(value) => {
68+
response = value;
69+
},
70+
);
71+
}
72+
return response;
73+
};
74+
75+
expect(query(3)).toEqual({ active: true, requestId: "rec-1", startedAtMs: 10 });
76+
expect(query(4)).toEqual({ active: false });
77+
});
78+
79+
it("starts a child Document that appears after the initial frame snapshot", async () => {
80+
let notifyFrameNavigation:
81+
| ((frame: {
82+
tabId: number;
83+
frameId: number;
84+
documentId?: string;
85+
lifecycle: "committed" | "completed";
86+
}) => void)
87+
| undefined;
88+
const sendToDocument = vi.fn(async () => ({ ok: true }));
89+
const coordinator = new RecordFrameCoordinator({
90+
getAllFrames: async () => [{ frameId: 0, documentId: "top-document" }],
91+
sendToDocument,
92+
subscribeFrameNavigation(listener) {
93+
notifyFrameNavigation = listener;
94+
return () => {};
95+
},
96+
});
97+
coordinator.attach();
98+
const onDocumentReady = vi.fn();
99+
coordinator.begin("rec-1", 10, 3, onDocumentReady);
100+
await coordinator.armTab("rec-1", 3);
101+
sendToDocument.mockClear();
102+
103+
notifyFrameNavigation?.({
104+
tabId: 3,
105+
frameId: 7,
106+
documentId: "late-child",
107+
lifecycle: "completed",
108+
});
109+
await vi.waitFor(() => {
110+
expect(sendToDocument).toHaveBeenCalledWith(
111+
3,
112+
{ type: "bsk-record-frame-start", requestId: "rec-1", startedAtMs: 10 },
113+
{ documentId: "late-child" },
114+
);
115+
});
116+
expect(onDocumentReady).not.toHaveBeenCalled();
117+
118+
const child = fakePort({
119+
tab: { id: 3 },
120+
frameId: 7,
121+
documentId: "late-child",
122+
} as chrome.runtime.MessageSender);
123+
for (const listener of onConnect.listeners) listener(child.port);
124+
child.receive({ type: "ready", requestId: "rec-1", producerId: "producer-1" });
125+
notifyFrameNavigation?.({
126+
tabId: 3,
127+
frameId: 7,
128+
documentId: "late-child",
129+
lifecycle: "completed",
130+
});
131+
await vi.waitFor(() => {
132+
expect(onDocumentReady).toHaveBeenCalledWith({
133+
tabId: 3,
134+
documentId: "late-child",
135+
browserFrameId: 7,
136+
producerId: "producer-1",
137+
});
138+
});
139+
});
140+
49141
it("binds a producer to its sender Document and keeps final steps valid while stopping", async () => {
50142
const sendToDocument = vi.fn(async () => ({ ok: true }));
51143
const coordinator = new RecordFrameCoordinator({
@@ -56,7 +148,7 @@ describe("RecordFrameCoordinator", () => {
56148
sendToDocument,
57149
});
58150
coordinator.attach();
59-
coordinator.begin("rec-1", 10);
151+
coordinator.begin("rec-1", 10, 3);
60152
await expect(coordinator.armTab("rec-1", 3)).resolves.toBe(true);
61153

62154
const sender = {
@@ -100,7 +192,7 @@ describe("RecordFrameCoordinator", () => {
100192
sendToDocument: async () => ({ ok: true }),
101193
});
102194
coordinator.attach();
103-
coordinator.begin("rec-1", 10);
195+
coordinator.begin("rec-1", 10, 3);
104196
await coordinator.armTab("rec-1", 3);
105197
const sender = {
106198
tab: { id: 3 },

apps/extension/src/lib/__tests__/recording-runtime.test.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ import { afterEach, describe, expect, it, vi } from "vitest";
22
import type { CdpRunner, ChromeTabsApi } from "@/tools/shared";
33

44
const captureRecordingObservation = vi.hoisted(() => vi.fn());
5+
const waitForDocumentSettled = vi.hoisted(() => vi.fn(async () => "quiet" as const));
56

67
vi.mock("../recording/observation-capture", async (importOriginal) => {
78
const actual = await importOriginal<typeof import("../recording/observation-capture")>();
89
return { ...actual, captureRecordingObservation };
910
});
1011

12+
vi.mock("../recording/document-settle", async (importOriginal) => {
13+
const actual = await importOriginal<typeof import("../recording/document-settle")>();
14+
return { ...actual, waitForDocumentSettled };
15+
});
16+
1117
import { ObservationNodeIndex } from "../recording/observation-capture";
1218
import { RecordingObservationRuntime } from "../recording/recording-runtime";
1319
import type { RecordingDraftStep } from "../recording/types";
@@ -31,7 +37,10 @@ function runtime(): RecordingObservationRuntime {
3137
}
3238

3339
describe("RecordingObservationRuntime", () => {
34-
afterEach(() => captureRecordingObservation.mockReset());
40+
afterEach(() => {
41+
captureRecordingObservation.mockReset();
42+
waitForDocumentSettled.mockClear();
43+
});
3544

3645
it("shares one initial capture and includes it in flush", async () => {
3746
let release!: (value: ReturnType<typeof observation>) => void;
@@ -199,4 +208,82 @@ describe("RecordingObservationRuntime", () => {
199208
});
200209
expect(draft.matchedTarget?.ref).toBe("e1");
201210
});
211+
212+
it("refreshes a late iframe before matching its first action", async () => {
213+
const iframeObservation = {
214+
...observation(),
215+
index: new ObservationNodeIndex({
216+
rootFrameId: "root",
217+
frames: [
218+
{
219+
frameId: "child",
220+
target: { tabId: 7, sessionId: "oopif-session" },
221+
recordingDocumentId: "producer-1",
222+
},
223+
],
224+
matchNodes: [
225+
{
226+
backendNodeId: 42,
227+
frameId: "child",
228+
tag: "button",
229+
rect: { x: 410, y: 20, w: 100, h: 30 },
230+
localRect: { x: 10, y: 20, w: 100, h: 30 },
231+
},
232+
],
233+
refs: [
234+
{
235+
ref: "e1",
236+
backendNodeId: 42,
237+
frameId: "child",
238+
role: "button",
239+
name: "表格视图",
240+
line: 1,
241+
},
242+
],
243+
}),
244+
};
245+
captureRecordingObservation
246+
.mockResolvedValueOnce(observation())
247+
.mockResolvedValueOnce(iframeObservation)
248+
.mockResolvedValueOnce(iframeObservation);
249+
const recording = runtime();
250+
251+
await recording.captureInitial(7);
252+
await recording.refreshDocument(7, "producer-1");
253+
const drafts: RecordingDraftStep[] = [
254+
{
255+
op: "click",
256+
captureTarget: { tag: "button", role: "button", name: "表格视图" },
257+
targetHint: {
258+
geometry: { rect: { x: 10, y: 20, w: 100, h: 30 }, tag: "button" },
259+
},
260+
},
261+
];
262+
await recording.processDraft(7, drafts, 0, "producer-1");
263+
recording.cancel();
264+
265+
expect(captureRecordingObservation).toHaveBeenCalledTimes(3);
266+
expect(waitForDocumentSettled).toHaveBeenCalledWith(
267+
expect.anything(),
268+
{ frameId: "child", target: { tabId: 7, sessionId: "oopif-session" } },
269+
{ signal: expect.any(AbortSignal) },
270+
);
271+
const draft = drafts[0];
272+
expect(draft?.op).toBe("click");
273+
if (!draft || draft.op !== "click") throw new Error("expected click draft");
274+
expect(draft.matchedTarget).toMatchObject({ ref: "e1", name: "表格视图" });
275+
});
276+
277+
it("does not poison final flush when a frame readiness refresh fails", async () => {
278+
captureRecordingObservation
279+
.mockResolvedValueOnce(observation())
280+
.mockRejectedValueOnce(new Error("child Document replaced"));
281+
const recording = runtime();
282+
283+
await recording.captureInitial(7);
284+
await expect(recording.refreshDocument(7, "producer-1")).rejects.toThrow(
285+
"child Document replaced",
286+
);
287+
await expect(recording.flush()).resolves.toBeUndefined();
288+
});
202289
});

0 commit comments

Comments
 (0)