diff --git a/src/viewer/sharing/summarize-with-chatgpt.js b/src/viewer/sharing/summarize-with-chatgpt.js index 51bd00e..55d3b95 100644 --- a/src/viewer/sharing/summarize-with-chatgpt.js +++ b/src/viewer/sharing/summarize-with-chatgpt.js @@ -71,8 +71,15 @@ function registerPdfTransfer(requestId, filename) { let awaitingAck; const expire = setTimeout(() => port.disconnect(), 120000); port.onDisconnect.addListener(() => { + // Chrome reports a missing receiver through lastError on disconnect. + // Reading it here also prevents an "Unchecked runtime.lastError" warning. + const disconnectError = chrome.runtime.lastError?.message; stopped = true; - if (awaitingAck) awaitingAck.reject(new Error("PDF transfer disconnected")); + clearTimeout(expire); + if (awaitingAck) { + awaitingAck.reject(new Error(disconnectError || "PDF transfer disconnected")); + awaitingAck = null; + } }); port.onMessage.addListener((message) => { if (message?.type === "ack" && awaitingAck) { diff --git a/tests/chatgpt-summary-popup.test.mjs b/tests/chatgpt-summary-popup.test.mjs index 49731e1..ff0bc2f 100644 --- a/tests/chatgpt-summary-popup.test.mjs +++ b/tests/chatgpt-summary-popup.test.mjs @@ -10,6 +10,8 @@ function setup({ width = 1080, height = 1920, popupFails = false } = {}) { let resolveUrl; const listeners = new Map(); const calls = []; + let onDisconnect; + let lastErrorReads = 0; const button = { title: "Summarize with ChatGPT", attributes: {}, @@ -25,10 +27,20 @@ function setup({ width = 1080, height = 1920, popupFails = false } = {}) { window: { screen: { availWidth: width, availHeight: height } }, resolveDocumentReferenceUrl: () => new Promise((resolve) => { resolveUrl = resolve; }), chrome: { - runtime: { connect(options) { - calls.push(["connect", options]); - return { onMessage: { addListener() {} }, onDisconnect: { addListener() {} }, disconnect() {} }; - } }, + runtime: { + get lastError() { + lastErrorReads += 1; + return { message: "Could not establish connection. Receiving end does not exist." }; + }, + connect(options) { + calls.push(["connect", options]); + return { + onMessage: { addListener() {} }, + onDisconnect: { addListener(listener) { onDisconnect = listener; } }, + disconnect() {}, + }; + }, + }, windows: { async create(options) { calls.push(["popup", options]); if (popupFails) throw new Error("Popup unavailable"); @@ -46,7 +58,15 @@ function setup({ width = 1080, height = 1920, popupFails = false } = {}) { }, }; vm.runInNewContext(source, context); - return { click: () => listeners.get("click")(), dispatch: (event) => listeners.get(event)(), resolveUrl, calls, button }; + return { + click: () => listeners.get("click")(), + dispatch: (event) => listeners.get(event)(), + resolveUrl, + calls, + button, + disconnect: () => onDisconnect?.(), + get lastErrorReads() { return lastErrorReads; }, + }; } test("opens the portrait popup before PDF metadata resolves, then stores the prompt before navigation", async () => { @@ -120,3 +140,14 @@ test("local file URLs are not substituted for PDF attachments", async () => { assert.ok(!data.prompt.includes("file:///Users/")); assert.equal(fixture.calls.find(([action]) => action === "connect")[1].name, "pdf-summary-source:request"); }); + +test("consumes Chrome's missing-receiver error on PDF summary disconnection", async () => { + const fixture = setup(); + const pendingClick = fixture.click(); + fixture.resolveUrl("https://www.sec.gov/files/rules/other/34-16900.pdf"); + await pendingClick; + + assert.equal(fixture.lastErrorReads, 0); + fixture.disconnect(); + assert.equal(fixture.lastErrorReads, 1); +});