diff --git a/frontend/src/shared/browserCommandHandler.ts b/frontend/src/shared/browserCommandHandler.ts index 47560ee1..74cccd84 100644 --- a/frontend/src/shared/browserCommandHandler.ts +++ b/frontend/src/shared/browserCommandHandler.ts @@ -272,13 +272,13 @@ const STUCK_EVAL_LIMIT_MS = 9000; // A/B via window.__OSW_CDP_EVAL__ = false) we fall back to the executeJavaScript path unchanged, // so behavior never regresses where CDP can't run. Both paths keep the same contract: return the // value, throw on a page-side error, mark dom-ready on success. -async function evalInPage(wv: BrowserWebview, code: string): Promise { +async function evalInPage(wv: BrowserWebview, code: string, sessionId?: string): Promise { const cdpBridge = (window as any).openswarm?.sendCdpCommand; if (cdpBridge && (window as any).__OSW_CDP_EVAL__ !== false) { let cdp: any; try { cdp = await sendCdp(wv, 'Runtime.evaluate', - { expression: code, returnByValue: true, awaitPromise: true }); + { expression: code, returnByValue: true, awaitPromise: true }, sessionId); } catch { // CDP INFRA failure (the debugger can't attach because DevTools or a remote-debugging // port already holds this webContents, or the bridge errored). Never worse than today: @@ -732,7 +732,25 @@ async function handleFindComposer(wv: BrowserWebview, params: Record Promise, + children: Array<{ sessionId: string; url: string }>, + inFrame: (sessionId: string) => Promise, +): Promise { + let result = await top(); + if (!result || !result.found) { + for (const child of children) { + let r: Found; + try { r = await inFrame(child.sessionId); } catch { continue; } + if (r && r.found) return { ...r, frameSessionId: child.sessionId, frameUrl: child.url }; + } + } + return result; +} + +const NONE = async (): Promise => ({ found: false }); + +test('a composer in the top document short-circuits, no frames are touched', async () => { + let framesTried = 0; + const r = await findWithFrames( + async () => ({ found: true, selector: '[data-osw-composer="1"]' }), + [{ sessionId: 'S1', url: 'https://disqus.com/embed' }], + async () => { framesTried++; return { found: true }; }, + ); + assert.equal(r.found, true); + assert.equal(framesTried, 0, 'the common case must pay nothing for this fallback'); + assert.equal(r.frameSessionId, undefined, 'a top-document hit belongs to no frame'); +}); + +test('the disqus shape: nothing up top, the composer is in a child frame', async () => { + const r = await findWithFrames( + NONE, + [{ sessionId: 'S_disqus', url: 'https://disqus.com/embed/comments/' }], + async () => ({ found: true, selector: '[data-osw-composer="1"]' }), + ); + assert.equal(r.found, true); + assert.equal(r.frameSessionId, 'S_disqus'); +}); + +test('the frame is REPORTED, because a selector is meaningless without its document', async () => { + // The caller fills `result.selector` afterwards. Handing back a selector that resolves in a + // frame the caller does not know about is how you get "filled it" with nothing filled. + const r = await findWithFrames(NONE, [{ sessionId: 'S9', url: 'https://x.test/f' }], + async () => ({ found: true, selector: '[data-osw-composer="1"]' })); + assert.equal(r.frameSessionId, 'S9'); + assert.equal(r.frameUrl, 'https://x.test/f'); +}); + +test('a frame that throws is skipped, not fatal', async () => { + // A cross-origin frame can detach mid-sweep; one dead frame must not lose a live one behind it. + const r = await findWithFrames(NONE, + [{ sessionId: 'DEAD', url: 'about:blank' }, { sessionId: 'LIVE', url: 'https://ok.test' }], + async (sid) => { if (sid === 'DEAD') throw new Error('target closed'); return { found: true }; }); + assert.equal(r.found, true); + assert.equal(r.frameSessionId, 'LIVE'); +}); + +test('no composer anywhere still reports a clean miss', async () => { + const r = await findWithFrames(NONE, [{ sessionId: 'A', url: 'u' }], NONE); + assert.equal(r.found, false); + assert.equal(r.frameSessionId, undefined); +});