diff --git a/apps/extension/src/content/record-capture.ts b/apps/extension/src/content/record-capture.ts index b21edcb..77123d6 100644 --- a/apps/extension/src/content/record-capture.ts +++ b/apps/extension/src/content/record-capture.ts @@ -422,6 +422,10 @@ export function startRecordCapture( const onMouseOver = (event: MouseEvent) => { const target = eventTarget(event); if (isOverlayTarget(target)) return; + if (pendingHover && target instanceof Element && pendingHover.element.contains(target)) { + if (Date.now() - pendingHover.recordedAt <= HOVER_BEFORE_CLICK_MAX_MS) return; + pendingHover = null; + } const hover = hoverTargetFromEvent(target); if (!hover) return; const now = Date.now(); diff --git a/apps/extension/src/tools/__tests__/dispatcher.test.ts b/apps/extension/src/tools/__tests__/dispatcher.test.ts index 36badac..cb4e5ea 100644 --- a/apps/extension/src/tools/__tests__/dispatcher.test.ts +++ b/apps/extension/src/tools/__tests__/dispatcher.test.ts @@ -456,6 +456,41 @@ describe("ToolDispatcher", () => { ); }); + it("transfers hover overlay bypass ownership between sessions", async () => { + vi.stubGlobal("chrome", { + tabs: { + sendMessage: vi.fn(async () => undefined), + }, + }); + const { transport } = fakeTransport(); + const sessions = new SessionManager({ + agentWindow: { + create: vi.fn(async () => 1), + remove: vi.fn(async () => {}), + ensureActiveTab: vi.fn(async () => {}), + }, + }); + const dispatcher = new ToolDispatcher({ transport, sessions }); + const helpers = dispatcher as unknown as { + setHoverBypass: (sessionId: string, tabId: number, enabled: boolean) => Promise; + }; + + await helpers.setHoverBypass("aa11", 7, true); + await helpers.setHoverBypass("bb22", 7, true); + await helpers.setHoverBypass("aa11", 7, false); + expect(chrome.tabs.sendMessage).not.toHaveBeenCalledWith( + 7, + expect.objectContaining({ enabled: false }), + ); + + await helpers.setHoverBypass("bb22", 7, false); + expect(chrome.tabs.sendMessage).toHaveBeenCalledWith( + 7, + expect.objectContaining({ enabled: false }), + ); + expect(chrome.tabs.sendMessage).toHaveBeenCalledTimes(2); + }); + it("limits default-target hover reassertion to the active tab", async () => { vi.stubGlobal("chrome", { tabs: { diff --git a/apps/extension/src/tools/__tests__/observation.test.ts b/apps/extension/src/tools/__tests__/observation.test.ts index 89a4ec3..a85fd64 100644 --- a/apps/extension/src/tools/__tests__/observation.test.ts +++ b/apps/extension/src/tools/__tests__/observation.test.ts @@ -1566,6 +1566,7 @@ describe("buildVomScene", () => { surfaceProbes: [ { triggerBackendNodeId: 20, triggerAction: "hover", subItems: ["My profile"] }, { triggerBackendNodeId: 20, triggerAction: "hover", subItems: ["Sign out"] }, + { triggerBackendNodeId: 21, triggerAction: "hover", subItems: ["Settings"] }, ], nodes: [ { diff --git a/apps/extension/src/tools/dispatcher.ts b/apps/extension/src/tools/dispatcher.ts index 389c9d3..26ec56d 100644 --- a/apps/extension/src/tools/dispatcher.ts +++ b/apps/extension/src/tools/dispatcher.ts @@ -534,12 +534,13 @@ export class ToolDispatcher { } private async setHoverBypass(sessionId: string, tabId: number, enabled: boolean): Promise { + const owner = this.hoverBypassTabs.get(tabId); if (enabled) { - if (this.hoverBypassTabs.has(tabId)) return; - await bypassOverlay(tabId, true); + if (owner === sessionId) return; + if (owner === undefined) await bypassOverlay(tabId, true); this.hoverBypassTabs.set(tabId, sessionId); } else { - if (!this.hoverBypassTabs.has(tabId)) return; + if (owner !== sessionId) return; await bypassOverlay(tabId, false); this.hoverBypassTabs.delete(tabId); } diff --git a/apps/extension/src/tools/observation.ts b/apps/extension/src/tools/observation.ts index a1da513..bde04f9 100644 --- a/apps/extension/src/tools/observation.ts +++ b/apps/extension/src/tools/observation.ts @@ -927,6 +927,7 @@ function buildConditionalSurfaces(nodes: VomNode[], captured: CapturedViewModel) probe.triggerPoint, ); if (!match) continue; + if (used.has(match.id)) continue; used.add(probe.triggerBackendNodeId); used.add(match.id); surfaces.push({