Skip to content

Commit fb013c6

Browse files
committed
Fix wait navigation detection for pruned frames
1 parent 07370f2 commit fb013c6

2 files changed

Lines changed: 53 additions & 1 deletion

File tree

packages/agent/src/translator/browser-wait.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,13 @@ export async function waitForBrowserExpectation(runtime: BrowserWaitRuntime, opt
123123
catch (error) { return failedObservation(started, now(), error, initial, final); }
124124
if (observation.targetId !== targetId) return terminal("interrupted", "unverifiable", initial, final, started, now(), "target_changed");
125125
if (runtime.dialogCount() > dialogs) return terminal("interrupted", "unverifiable", initial, final, started, now(), "dialog");
126-
const navigated = observation.navigationEpoch !== baseline.navigationEpoch || [...observation.generations].some(([key, generation]) => generation !== runtime.liveGeneration(key) || (baseline.generations.has(key) && baseline.generations.get(key) !== generation));
126+
const generationOutOfDate = [...observation.generations].some(([key, generation]) => generation !== runtime.liveGeneration(key));
127+
const droppedBaselineFrame = observation.complete && [...baseline.generations.keys()].some((key) => !observation.generations.has(key));
128+
const generationAdvanced = [...observation.generations].some(([key, generation]) => {
129+
const baselineGeneration = baseline.generations.get(key);
130+
return baselineGeneration !== undefined && generation > baselineGeneration;
131+
});
132+
const navigated = observation.navigationEpoch !== baseline.navigationEpoch || generationOutOfDate || droppedBaselineFrame || generationAdvanced;
127133
if (navigated) {
128134
if (isLocationExpectation(options.expect)) {
129135
final = evaluateBrowserExpectation(options.expect, observation, baseline, runtime.resolveRef);

packages/agent/test/browser-wait.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,52 @@ describe("waitForBrowserExpectation", () => {
9999
expect(result).toMatchObject({ status: reason === "observation_failed" ? "unverifiable" : "interrupted", reason });
100100
});
101101

102+
it("does not treat a reset frame generation as navigation when epochs match", async () => {
103+
let time = 0, reads = 0;
104+
const generations = new Map<string, number>();
105+
const result = await waitForBrowserExpectation({
106+
selectTarget: async () => "page",
107+
observeTarget: async () => {
108+
const current = reads++ === 0
109+
? observation([], true, { generations: new Map([["page", 0], ["frame", 2]]) })
110+
: observation([], true, { generations: new Map([["page", 0], ["frame", 0]]) });
111+
generations.clear();
112+
for (const [key, generation] of current.generations) generations.set(key, generation);
113+
return current;
114+
},
115+
dialogCount: () => 0,
116+
targetExists: async () => true,
117+
liveGeneration: (key) => generations.get(key) ?? 0,
118+
resolveRef: missingRef,
119+
now: () => time,
120+
delay: async (ms) => { time += ms; },
121+
}, { expect: { type: "text", text: "Ready" }, timeoutMs: 20, pollMs: 10 });
122+
expect(result).toMatchObject({ status: "timed_out", evidence: "failed" });
123+
});
124+
125+
it("interrupts absence waits when complete observations drop baseline frames", async () => {
126+
let time = 0, reads = 0;
127+
const generations = new Map<string, number>();
128+
const result = await waitForBrowserExpectation({
129+
selectTarget: async () => "page",
130+
observeTarget: async () => {
131+
const current = reads++ === 0
132+
? observation(["Ready"], true, { generations: new Map([["page", 0], ["frame", 1]]) })
133+
: observation([], true, { generations: new Map([["page", 0]]) });
134+
generations.clear();
135+
for (const [key, generation] of current.generations) generations.set(key, generation);
136+
return current;
137+
},
138+
dialogCount: () => 0,
139+
targetExists: async () => true,
140+
liveGeneration: (key) => generations.get(key) ?? 0,
141+
resolveRef: missingRef,
142+
now: () => time,
143+
delay: async (ms) => { time += ms; },
144+
}, { expect: { type: "text", text: "Ready", exists: false }, timeoutMs: 20, pollMs: 10 });
145+
expect(result).toMatchObject({ status: "interrupted", reason: "navigation" });
146+
});
147+
102148
it("interrupts when a ref becomes stale after the baseline", async () => {
103149
let time = 0, resolves = 0;
104150
const resolveRef: BrowserRefResolver = () => resolves++ === 0 ? { truth: false, details: ["not checked"] } : { truth: undefined, details: ["stale"], reason: "stale_ref" };

0 commit comments

Comments
 (0)