From 01e9a5e9f076a6464a39f12d345a1d47c03ea762 Mon Sep 17 00:00:00 2001 From: Kuroome Date: Thu, 9 Jul 2026 00:42:57 +0800 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20dashboard=20?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=88=B7=E6=96=B0=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 改为 chrome.runtime.sendMessage 广播,命中 dashboard 与 popup 两个 runtime 监听。 - 测试:新增 background-notify.test.js 行为测试(广播通道、错误吞掉逻辑), 并修正 ui-regression.test.js 对注释中残留旧 API 的假阳性断言。 - manifest.json: 版本号 1.1.1->1.1.2。 --- extension/background-notify.test.js | 111 ++++++++++++++++++++++++++++ extension/background.js | 108 +++++++++++---------------- extension/manifest.json | 11 ++- extension/ui-regression.test.js | 7 +- 4 files changed, 168 insertions(+), 69 deletions(-) create mode 100644 extension/background-notify.test.js diff --git a/extension/background-notify.test.js b/extension/background-notify.test.js new file mode 100644 index 0000000..eae8094 --- /dev/null +++ b/extension/background-notify.test.js @@ -0,0 +1,111 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); + +const BACKGROUND_PATH = path.join(__dirname, 'background.js'); + +// background.js registers listeners at top level and caches its module, so we +// flush the cache and re-evaluate it for each scenario. +function loadBackground(chrome) { + global.chrome = chrome; + delete require.cache[require.resolve(BACKGROUND_PATH)]; + require(BACKGROUND_PATH); +} + +function buildChrome() { + const captured = {}; + const state = { + sendMessageCalls: [], + warnCalls: [], + }; + + const chrome = { + runtime: { + id: 'test-ext-id', + getURL: (p) => `chrome-extension://test-ext-id/${p}`, + onInstalled: { addListener: () => {} }, + onStartup: { addListener: () => {} }, + sendMessage: async () => {}, + }, + action: { + setBadgeText: async () => {}, + }, + tabs: { + query: async () => [], + remove: async () => {}, + onCreated: { addListener: (cb) => { captured.onCreated = cb; } }, + onRemoved: { addListener: (cb) => { captured.onRemoved = cb; } }, + onUpdated: { addListener: (cb) => { captured.onUpdated = cb; } }, + }, + storage: { + local: { get: async () => ({}) }, + }, + }; + + return { chrome, captured, state }; +} + +test('tab events broadcast via chrome.runtime.sendMessage, not content-script channel', async () => { + const { chrome, captured, state } = buildChrome(); + chrome.runtime.sendMessage = async (msg) => { + state.sendMessageCalls.push(msg); + }; + loadBackground(chrome); + assert.ok(captured.onCreated, 'background should register a tabs.onCreated listener'); + + await captured.onCreated({ id: 99 }); + + assert.strictEqual(state.sendMessageCalls.length, 1); + assert.deepStrictEqual(state.sendMessageCalls[0], { + action: 'tabs-changed', + source: 'tabs.onCreated', + triggerTabId: 99, + }); + + await captured.onRemoved(42); + const last = state.sendMessageCalls[state.sendMessageCalls.length - 1]; + assert.deepStrictEqual(last, { + action: 'tabs-changed', + source: 'tabs.onRemoved', + triggerTabId: 42, + }); +}); + +test('"Receiving end does not exist" rejection is swallowed silently', async () => { + const { chrome, captured, state } = buildChrome(); + chrome.runtime.sendMessage = async () => { + throw new Error('Could not establish connection. Receiving end does not exist.'); + }; + + const originalWarn = console.warn; + console.warn = (...args) => { state.warnCalls.push(args); }; + try { + loadBackground(chrome); + await captured.onCreated({ id: 7 }); + } finally { + console.warn = originalWarn; + } + + assert.strictEqual(state.warnCalls.length, 0); +}); + +test('non-connection errors from sendMessage are surfaced via console.warn', async () => { + const { chrome, captured, state } = buildChrome(); + chrome.runtime.sendMessage = async () => { + throw new Error('unexpected broadcast failure'); + }; + + const originalWarn = console.warn; + console.warn = (...args) => { state.warnCalls.push(args); }; + try { + loadBackground(chrome); + await captured.onCreated({ id: 7 }); + } finally { + console.warn = originalWarn; + } + + assert.strictEqual(state.warnCalls.length, 1); + assert.match(state.warnCalls[0].join(' '), /unexpected broadcast failure/); +}); diff --git a/extension/background.js b/extension/background.js index 5583249..bd2c225 100644 --- a/extension/background.js +++ b/extension/background.js @@ -6,114 +6,92 @@ */ const TAB_HARBOR_BG_DEBUG = false; -if (TAB_HARBOR_BG_DEBUG) console.log('[tab-harbor bg] Service worker loaded, registering event listeners...'); +if (TAB_HARBOR_BG_DEBUG) + console.log( + "[tab-harbor bg] Service worker loaded, registering event listeners...", + ); // ─── Auto-close duplicate new tabs ─────────────────────────────────────────── function getNewTabUrls() { return new Set([ - chrome.runtime.getURL('index.html'), - chrome.runtime.getURL('extension/index.html'), + chrome.runtime.getURL("index.html"), + chrome.runtime.getURL("extension/index.html"), ]); } function isNewTabBlank(tab, newTabUrls) { - const knownNewTabUrls = newTabUrls instanceof Set - ? newTabUrls - : new Set(Array.isArray(newTabUrls) ? newTabUrls : [newTabUrls]); - const url = tab?.url || ''; - const pendingUrl = tab?.pendingUrl || ''; - if (pendingUrl && !knownNewTabUrls.has(pendingUrl) && pendingUrl !== 'chrome://newtab/') { + const knownNewTabUrls = + newTabUrls instanceof Set + ? newTabUrls + : new Set(Array.isArray(newTabUrls) ? newTabUrls : [newTabUrls]); + const url = tab?.url || ""; + const pendingUrl = tab?.pendingUrl || ""; + if ( + pendingUrl && + !knownNewTabUrls.has(pendingUrl) && + pendingUrl !== "chrome://newtab/" + ) { return false; } return ( - url === 'chrome://newtab/' || + url === "chrome://newtab/" || knownNewTabUrls.has(url) || - pendingUrl === 'chrome://newtab/' || + pendingUrl === "chrome://newtab/" || knownNewTabUrls.has(pendingUrl) || - url === '' || - (tab.status === 'loading' && !url) + url === "" || + (tab.status === "loading" && !url) ); } async function closeDuplicateNewTabs() { try { - const stored = await chrome.storage.local.get('themePreferences'); + const stored = await chrome.storage.local.get("themePreferences"); const prefs = stored.themePreferences || {}; if (prefs.closeDuplicateNewTabsEnabled !== true) return; const newTabUrls = getNewTabUrls(); const allTabs = await chrome.tabs.query({}); - const blankTabs = allTabs.filter(tab => isNewTabBlank(tab, newTabUrls)); + const blankTabs = allTabs.filter((tab) => isNewTabBlank(tab, newTabUrls)); if (blankTabs.length <= 1) return; // Keep the active tab; if none is active, keep the one with the largest id (newest) - const activeTab = blankTabs.find(tab => tab.active); - const toKeep = activeTab || blankTabs.reduce((a, b) => (a.id > b.id ? a : b)); - const toClose = blankTabs.filter(tab => tab.id !== toKeep.id).map(tab => tab.id); + const activeTab = blankTabs.find((tab) => tab.active); + const toKeep = + activeTab || blankTabs.reduce((a, b) => (a.id > b.id ? a : b)); + const toClose = blankTabs + .filter((tab) => tab.id !== toKeep.id) + .map((tab) => tab.id); if (toClose.length > 0) await chrome.tabs.remove(toClose); } catch (err) { - console.warn('[tab-harbor bg] closeDuplicateNewTabs error:', err.message); + console.warn("[tab-harbor bg] closeDuplicateNewTabs error:", err.message); } } async function updateBadge() { try { - await chrome.action.setBadgeText({ text: '' }); + await chrome.action.setBadgeText({ text: "" }); } catch { - chrome.action.setBadgeText({ text: '' }); + chrome.action.setBadgeText({ text: "" }); } } -function getTabHarborDashboardUrls() { - const extensionId = chrome.runtime.id; - return new Set([ - `chrome-extension://${extensionId}/index.html`, - `chrome-extension://${extensionId}/extension/index.html`, - ]); -} - // ─── Event listeners ────────────────────────────────────────────────────────── // Notify Tab Harbor pages when tabs change so they can refresh async function notifyTabHarborPages(eventMeta = {}) { - try { - // Find all Tab Harbor dashboard pages - const dashboardUrls = getTabHarborDashboardUrls(); - - // Query all tabs and filter manually for more reliable matching - const allTabs = await chrome.tabs.query({}); + const message = { + action: "tabs-changed", + source: eventMeta.source || "tabs.changed", + triggerTabId: eventMeta.triggerTabId ?? null, + }; - const dashboardTabs = allTabs.filter(tab => { - if (!tab.url) return false; - // Tab Harbor can appear as either: - // 1. chrome-extension://EXTENSION_ID/index.html (direct access) - // 2. chrome-extension://EXTENSION_ID/extension/index.html (root manifest entry) - // 3. chrome://newtab/ with title "Tab Harbor" (new tab override) - return ( - dashboardUrls.has(tab.url) || - (tab.url === 'chrome://newtab/' && tab.title === 'Tab Harbor') - ); - }); - - if (dashboardTabs.length === 0) return; - - for (const tab of dashboardTabs) { - try { - await chrome.tabs.sendMessage(tab.id, { - action: 'tabs-changed', - source: eventMeta.source || 'tabs.changed', - triggerTabId: eventMeta.triggerTabId ?? null, - }); - } catch (err) { - // Tab might be closed or not ready, ignore - console.warn(`[tab-harbor bg] Failed to notify tab ${tab.id}:`, err.message); - } - } + try { + await chrome.runtime.sendMessage(message); } catch (err) { - console.warn('[tab-harbor bg] Error in notifyTabHarborPages:', err); + console.warn("[tab-harbor bg] Error notifying Tab Harbor pages:", err); } } @@ -130,20 +108,20 @@ chrome.runtime.onStartup.addListener(() => { // Update badge and notify Tab Harbor pages whenever a tab is opened chrome.tabs.onCreated.addListener((tab) => { updateBadge(); - notifyTabHarborPages({ source: 'tabs.onCreated', triggerTabId: tab?.id }); + notifyTabHarborPages({ source: "tabs.onCreated", triggerTabId: tab?.id }); closeDuplicateNewTabs(); }); // Update badge and notify Tab Harbor pages whenever a tab is closed chrome.tabs.onRemoved.addListener((tabId) => { updateBadge(); - notifyTabHarborPages({ source: 'tabs.onRemoved', triggerTabId: tabId }); + notifyTabHarborPages({ source: "tabs.onRemoved", triggerTabId: tabId }); }); // Update badge and notify Tab Harbor pages when a tab's URL changes (e.g. navigating to/from chrome://) chrome.tabs.onUpdated.addListener((tabId) => { updateBadge(); - notifyTabHarborPages({ source: 'tabs.onUpdated', triggerTabId: tabId }); + notifyTabHarborPages({ source: "tabs.onUpdated", triggerTabId: tabId }); }); // ─── Initial run ───────────────────────────────────────────────────────────── diff --git a/extension/manifest.json b/extension/manifest.json index bf6a807..6efc2d2 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,9 +1,16 @@ { "manifest_version": 3, "name": "Tab Harbor", - "version": "1.1.1", + "version": "1.1.2", "description": "A new tab dashboard for organizing open tabs, quick links, todos, and saved tab sessions in one calm workspace.", - "permissions": ["tabs", "storage", "search", "clipboardRead", "tabGroups", "favicon"], + "permissions": [ + "tabs", + "storage", + "search", + "clipboardRead", + "tabGroups", + "favicon" + ], "chrome_url_overrides": { "newtab": "index.html" }, "background": { "service_worker": "background.js" }, "action": { diff --git a/extension/ui-regression.test.js b/extension/ui-regression.test.js index 7a70d54..85efe37 100644 --- a/extension/ui-regression.test.js +++ b/extension/ui-regression.test.js @@ -1056,9 +1056,12 @@ test('dynamic animation styles are generated by JavaScript instead of hardcoded test('dashboard auto-refreshes when tabs change via background message', () => { const bgJs = fs.readFileSync(path.join(__dirname, 'background.js'), 'utf8'); - // Background should notify Tab Harbor pages + // Background should broadcast tab changes via chrome.runtime.sendMessage + // (the dashboard is an extension page, NOT a content script — sendMessage + // to a content script would always fail with "Receiving end does not exist"). assert.match(bgJs, /notifyTabHarborPages/); - assert.match(bgJs, /chrome\.tabs\.sendMessage/); + assert.match(bgJs, /chrome\.runtime\.sendMessage/); + assert.doesNotMatch(bgJs, /await chrome\.tabs\.sendMessage/); assert.match(bgJs, /action:\s*'tabs-changed'/); assert.match(bgJs, /triggerTabId/); assert.match(bgJs, /source:\s*'tabs\.onCreated'/); From 51febce62129542fea2bc9522dbbc421332dcda4 Mon Sep 17 00:00:00 2001 From: Kuroome Date: Thu, 9 Jul 2026 01:06:50 +0800 Subject: [PATCH 2/7] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8DCopilot=20Review?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extension/background.js | 6 +++++- extension/ui-regression.test.js | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/extension/background.js b/extension/background.js index bd2c225..8c24e6d 100644 --- a/extension/background.js +++ b/extension/background.js @@ -91,7 +91,11 @@ async function notifyTabHarborPages(eventMeta = {}) { try { await chrome.runtime.sendMessage(message); } catch (err) { - console.warn("[tab-harbor bg] Error notifying Tab Harbor pages:", err); + // 没有接收方(dashboard/popup 未打开)时的典型报错,广播无需接收方,静默吞掉 + const messageText = err && err.message ? err.message : String(err); + if (!messageText.includes("Receiving end does not exist")) { + console.warn("[tab-harbor bg] Error notifying Tab Harbor pages:", err); + } } } diff --git a/extension/ui-regression.test.js b/extension/ui-regression.test.js index 85efe37..0805f22 100644 --- a/extension/ui-regression.test.js +++ b/extension/ui-regression.test.js @@ -345,7 +345,7 @@ test('optional local config is loaded safely before app mount', () => { }); test('background keeps the toolbar badge empty', () => { - assert.match(backgroundJs, /await chrome\.action\.setBadgeText\(\{\s*text:\s*''\s*\}\)/); + assert.match(backgroundJs, /await chrome\.action\.setBadgeText\(\{\s*text:\s*["']{2}\s*\}\)/); assert.doesNotMatch(backgroundJs, /String\(count\)/); }); @@ -1062,10 +1062,10 @@ test('dashboard auto-refreshes when tabs change via background message', () => { assert.match(bgJs, /notifyTabHarborPages/); assert.match(bgJs, /chrome\.runtime\.sendMessage/); assert.doesNotMatch(bgJs, /await chrome\.tabs\.sendMessage/); - assert.match(bgJs, /action:\s*'tabs-changed'/); + assert.match(bgJs, /action:\s*["']tabs-changed["']/); assert.match(bgJs, /triggerTabId/); - assert.match(bgJs, /source:\s*'tabs\.onCreated'/); - assert.match(bgJs, /source:\s*'tabs\.onUpdated'/); + assert.match(bgJs, /source:\s*["']tabs\.onCreated["']/); + assert.match(bgJs, /source:\s*["']tabs\.onUpdated["']/); assert.match(bgJs, /chrome\.tabs\.onCreated\.addListener/); assert.match(bgJs, /chrome\.tabs\.onRemoved\.addListener/); From 9981fb8c015973de58d7cc764f3c2a0588da0793 Mon Sep 17 00:00:00 2001 From: Kuroome Date: Thu, 9 Jul 2026 01:18:07 +0800 Subject: [PATCH 3/7] =?UTF-8?q?fix(extension):=20=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=8C=96=E9=94=99=E8=AF=AF=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=B9=B6=E6=94=B6=E7=B4=A7=20tabs.sendMessage=20=E6=96=AD?= =?UTF-8?q?=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景广播无接收方错误消息匹配中英双语(/接收端不存在|Receiving end/i) 真正异常添加去重保护避免刷屏(notifyTabHarborPages._lastWarn) ui-regression 断言收紧禁止任何形式 chrome.tabs.sendMessage (此前仅排除 await 前缀写法) --- extension/background.js | 10 ++++++++-- extension/ui-regression.test.js | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/extension/background.js b/extension/background.js index 8c24e6d..02a22d4 100644 --- a/extension/background.js +++ b/extension/background.js @@ -91,9 +91,15 @@ async function notifyTabHarborPages(eventMeta = {}) { try { await chrome.runtime.sendMessage(message); } catch (err) { - // 没有接收方(dashboard/popup 未打开)时的典型报错,广播无需接收方,静默吞掉 const messageText = err && err.message ? err.message : String(err); - if (!messageText.includes("Receiving end does not exist")) { + const noReceiverPatterns = [ + /Receiving end does not exist/i, + /接收端不存在/, + ]; + if (noReceiverPatterns.some((p) => p.test(messageText))) return; + + if (notifyTabHarborPages._lastWarn !== messageText) { + notifyTabHarborPages._lastWarn = messageText; console.warn("[tab-harbor bg] Error notifying Tab Harbor pages:", err); } } diff --git a/extension/ui-regression.test.js b/extension/ui-regression.test.js index 0805f22..0308a54 100644 --- a/extension/ui-regression.test.js +++ b/extension/ui-regression.test.js @@ -1061,7 +1061,7 @@ test('dashboard auto-refreshes when tabs change via background message', () => { // to a content script would always fail with "Receiving end does not exist"). assert.match(bgJs, /notifyTabHarborPages/); assert.match(bgJs, /chrome\.runtime\.sendMessage/); - assert.doesNotMatch(bgJs, /await chrome\.tabs\.sendMessage/); + assert.doesNotMatch(bgJs, /chrome\.tabs\.sendMessage/); assert.match(bgJs, /action:\s*["']tabs-changed["']/); assert.match(bgJs, /triggerTabId/); assert.match(bgJs, /source:\s*["']tabs\.onCreated["']/); From d2528be20f1f62f966391691ef099632cc25cbf0 Mon Sep 17 00:00:00 2001 From: Kuroome Date: Thu, 9 Jul 2026 01:20:54 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix(extension):=20=E4=BF=AE=E5=A4=8D=20dash?= =?UTF-8?q?board=20=E8=87=AA=E5=8A=A8=E5=88=B7=E6=96=B0=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 tab 变更通知从 content script 通道改为 chrome.runtime.sendMessage 广播,命中 dashboard 与 popup 的 runtime.onMessage 监听。 - 静默吞掉无接收方时的连接错误(兼容中英双语消息) - 非连接类异常添加去重保护避免刷屏 - 新增 background-notify.test.js 行为测试 - 同步更新 ui-regression.test.js 断言,收紧 chrome.tabs.sendMessage 检查 - manifest.json 版本号 1.1.1 -> 1.1.2 --- extension/background-notify.test.js | 111 ++++++++++++++++++++++++++ extension/background.js | 118 +++++++++++++--------------- extension/manifest.json | 11 ++- extension/ui-regression.test.js | 15 ++-- 4 files changed, 182 insertions(+), 73 deletions(-) create mode 100644 extension/background-notify.test.js diff --git a/extension/background-notify.test.js b/extension/background-notify.test.js new file mode 100644 index 0000000..eae8094 --- /dev/null +++ b/extension/background-notify.test.js @@ -0,0 +1,111 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); + +const BACKGROUND_PATH = path.join(__dirname, 'background.js'); + +// background.js registers listeners at top level and caches its module, so we +// flush the cache and re-evaluate it for each scenario. +function loadBackground(chrome) { + global.chrome = chrome; + delete require.cache[require.resolve(BACKGROUND_PATH)]; + require(BACKGROUND_PATH); +} + +function buildChrome() { + const captured = {}; + const state = { + sendMessageCalls: [], + warnCalls: [], + }; + + const chrome = { + runtime: { + id: 'test-ext-id', + getURL: (p) => `chrome-extension://test-ext-id/${p}`, + onInstalled: { addListener: () => {} }, + onStartup: { addListener: () => {} }, + sendMessage: async () => {}, + }, + action: { + setBadgeText: async () => {}, + }, + tabs: { + query: async () => [], + remove: async () => {}, + onCreated: { addListener: (cb) => { captured.onCreated = cb; } }, + onRemoved: { addListener: (cb) => { captured.onRemoved = cb; } }, + onUpdated: { addListener: (cb) => { captured.onUpdated = cb; } }, + }, + storage: { + local: { get: async () => ({}) }, + }, + }; + + return { chrome, captured, state }; +} + +test('tab events broadcast via chrome.runtime.sendMessage, not content-script channel', async () => { + const { chrome, captured, state } = buildChrome(); + chrome.runtime.sendMessage = async (msg) => { + state.sendMessageCalls.push(msg); + }; + loadBackground(chrome); + assert.ok(captured.onCreated, 'background should register a tabs.onCreated listener'); + + await captured.onCreated({ id: 99 }); + + assert.strictEqual(state.sendMessageCalls.length, 1); + assert.deepStrictEqual(state.sendMessageCalls[0], { + action: 'tabs-changed', + source: 'tabs.onCreated', + triggerTabId: 99, + }); + + await captured.onRemoved(42); + const last = state.sendMessageCalls[state.sendMessageCalls.length - 1]; + assert.deepStrictEqual(last, { + action: 'tabs-changed', + source: 'tabs.onRemoved', + triggerTabId: 42, + }); +}); + +test('"Receiving end does not exist" rejection is swallowed silently', async () => { + const { chrome, captured, state } = buildChrome(); + chrome.runtime.sendMessage = async () => { + throw new Error('Could not establish connection. Receiving end does not exist.'); + }; + + const originalWarn = console.warn; + console.warn = (...args) => { state.warnCalls.push(args); }; + try { + loadBackground(chrome); + await captured.onCreated({ id: 7 }); + } finally { + console.warn = originalWarn; + } + + assert.strictEqual(state.warnCalls.length, 0); +}); + +test('non-connection errors from sendMessage are surfaced via console.warn', async () => { + const { chrome, captured, state } = buildChrome(); + chrome.runtime.sendMessage = async () => { + throw new Error('unexpected broadcast failure'); + }; + + const originalWarn = console.warn; + console.warn = (...args) => { state.warnCalls.push(args); }; + try { + loadBackground(chrome); + await captured.onCreated({ id: 7 }); + } finally { + console.warn = originalWarn; + } + + assert.strictEqual(state.warnCalls.length, 1); + assert.match(state.warnCalls[0].join(' '), /unexpected broadcast failure/); +}); diff --git a/extension/background.js b/extension/background.js index 5583249..02a22d4 100644 --- a/extension/background.js +++ b/extension/background.js @@ -6,114 +6,102 @@ */ const TAB_HARBOR_BG_DEBUG = false; -if (TAB_HARBOR_BG_DEBUG) console.log('[tab-harbor bg] Service worker loaded, registering event listeners...'); +if (TAB_HARBOR_BG_DEBUG) + console.log( + "[tab-harbor bg] Service worker loaded, registering event listeners...", + ); // ─── Auto-close duplicate new tabs ─────────────────────────────────────────── function getNewTabUrls() { return new Set([ - chrome.runtime.getURL('index.html'), - chrome.runtime.getURL('extension/index.html'), + chrome.runtime.getURL("index.html"), + chrome.runtime.getURL("extension/index.html"), ]); } function isNewTabBlank(tab, newTabUrls) { - const knownNewTabUrls = newTabUrls instanceof Set - ? newTabUrls - : new Set(Array.isArray(newTabUrls) ? newTabUrls : [newTabUrls]); - const url = tab?.url || ''; - const pendingUrl = tab?.pendingUrl || ''; - if (pendingUrl && !knownNewTabUrls.has(pendingUrl) && pendingUrl !== 'chrome://newtab/') { + const knownNewTabUrls = + newTabUrls instanceof Set + ? newTabUrls + : new Set(Array.isArray(newTabUrls) ? newTabUrls : [newTabUrls]); + const url = tab?.url || ""; + const pendingUrl = tab?.pendingUrl || ""; + if ( + pendingUrl && + !knownNewTabUrls.has(pendingUrl) && + pendingUrl !== "chrome://newtab/" + ) { return false; } return ( - url === 'chrome://newtab/' || + url === "chrome://newtab/" || knownNewTabUrls.has(url) || - pendingUrl === 'chrome://newtab/' || + pendingUrl === "chrome://newtab/" || knownNewTabUrls.has(pendingUrl) || - url === '' || - (tab.status === 'loading' && !url) + url === "" || + (tab.status === "loading" && !url) ); } async function closeDuplicateNewTabs() { try { - const stored = await chrome.storage.local.get('themePreferences'); + const stored = await chrome.storage.local.get("themePreferences"); const prefs = stored.themePreferences || {}; if (prefs.closeDuplicateNewTabsEnabled !== true) return; const newTabUrls = getNewTabUrls(); const allTabs = await chrome.tabs.query({}); - const blankTabs = allTabs.filter(tab => isNewTabBlank(tab, newTabUrls)); + const blankTabs = allTabs.filter((tab) => isNewTabBlank(tab, newTabUrls)); if (blankTabs.length <= 1) return; // Keep the active tab; if none is active, keep the one with the largest id (newest) - const activeTab = blankTabs.find(tab => tab.active); - const toKeep = activeTab || blankTabs.reduce((a, b) => (a.id > b.id ? a : b)); - const toClose = blankTabs.filter(tab => tab.id !== toKeep.id).map(tab => tab.id); + const activeTab = blankTabs.find((tab) => tab.active); + const toKeep = + activeTab || blankTabs.reduce((a, b) => (a.id > b.id ? a : b)); + const toClose = blankTabs + .filter((tab) => tab.id !== toKeep.id) + .map((tab) => tab.id); if (toClose.length > 0) await chrome.tabs.remove(toClose); } catch (err) { - console.warn('[tab-harbor bg] closeDuplicateNewTabs error:', err.message); + console.warn("[tab-harbor bg] closeDuplicateNewTabs error:", err.message); } } async function updateBadge() { try { - await chrome.action.setBadgeText({ text: '' }); + await chrome.action.setBadgeText({ text: "" }); } catch { - chrome.action.setBadgeText({ text: '' }); + chrome.action.setBadgeText({ text: "" }); } } -function getTabHarborDashboardUrls() { - const extensionId = chrome.runtime.id; - return new Set([ - `chrome-extension://${extensionId}/index.html`, - `chrome-extension://${extensionId}/extension/index.html`, - ]); -} - // ─── Event listeners ────────────────────────────────────────────────────────── // Notify Tab Harbor pages when tabs change so they can refresh async function notifyTabHarborPages(eventMeta = {}) { - try { - // Find all Tab Harbor dashboard pages - const dashboardUrls = getTabHarborDashboardUrls(); + const message = { + action: "tabs-changed", + source: eventMeta.source || "tabs.changed", + triggerTabId: eventMeta.triggerTabId ?? null, + }; - // Query all tabs and filter manually for more reliable matching - const allTabs = await chrome.tabs.query({}); - - const dashboardTabs = allTabs.filter(tab => { - if (!tab.url) return false; - // Tab Harbor can appear as either: - // 1. chrome-extension://EXTENSION_ID/index.html (direct access) - // 2. chrome-extension://EXTENSION_ID/extension/index.html (root manifest entry) - // 3. chrome://newtab/ with title "Tab Harbor" (new tab override) - return ( - dashboardUrls.has(tab.url) || - (tab.url === 'chrome://newtab/' && tab.title === 'Tab Harbor') - ); - }); - - if (dashboardTabs.length === 0) return; - - for (const tab of dashboardTabs) { - try { - await chrome.tabs.sendMessage(tab.id, { - action: 'tabs-changed', - source: eventMeta.source || 'tabs.changed', - triggerTabId: eventMeta.triggerTabId ?? null, - }); - } catch (err) { - // Tab might be closed or not ready, ignore - console.warn(`[tab-harbor bg] Failed to notify tab ${tab.id}:`, err.message); - } - } + try { + await chrome.runtime.sendMessage(message); } catch (err) { - console.warn('[tab-harbor bg] Error in notifyTabHarborPages:', err); + const messageText = err && err.message ? err.message : String(err); + const noReceiverPatterns = [ + /Receiving end does not exist/i, + /接收端不存在/, + ]; + if (noReceiverPatterns.some((p) => p.test(messageText))) return; + + if (notifyTabHarborPages._lastWarn !== messageText) { + notifyTabHarborPages._lastWarn = messageText; + console.warn("[tab-harbor bg] Error notifying Tab Harbor pages:", err); + } } } @@ -130,20 +118,20 @@ chrome.runtime.onStartup.addListener(() => { // Update badge and notify Tab Harbor pages whenever a tab is opened chrome.tabs.onCreated.addListener((tab) => { updateBadge(); - notifyTabHarborPages({ source: 'tabs.onCreated', triggerTabId: tab?.id }); + notifyTabHarborPages({ source: "tabs.onCreated", triggerTabId: tab?.id }); closeDuplicateNewTabs(); }); // Update badge and notify Tab Harbor pages whenever a tab is closed chrome.tabs.onRemoved.addListener((tabId) => { updateBadge(); - notifyTabHarborPages({ source: 'tabs.onRemoved', triggerTabId: tabId }); + notifyTabHarborPages({ source: "tabs.onRemoved", triggerTabId: tabId }); }); // Update badge and notify Tab Harbor pages when a tab's URL changes (e.g. navigating to/from chrome://) chrome.tabs.onUpdated.addListener((tabId) => { updateBadge(); - notifyTabHarborPages({ source: 'tabs.onUpdated', triggerTabId: tabId }); + notifyTabHarborPages({ source: "tabs.onUpdated", triggerTabId: tabId }); }); // ─── Initial run ───────────────────────────────────────────────────────────── diff --git a/extension/manifest.json b/extension/manifest.json index bf6a807..6efc2d2 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,9 +1,16 @@ { "manifest_version": 3, "name": "Tab Harbor", - "version": "1.1.1", + "version": "1.1.2", "description": "A new tab dashboard for organizing open tabs, quick links, todos, and saved tab sessions in one calm workspace.", - "permissions": ["tabs", "storage", "search", "clipboardRead", "tabGroups", "favicon"], + "permissions": [ + "tabs", + "storage", + "search", + "clipboardRead", + "tabGroups", + "favicon" + ], "chrome_url_overrides": { "newtab": "index.html" }, "background": { "service_worker": "background.js" }, "action": { diff --git a/extension/ui-regression.test.js b/extension/ui-regression.test.js index 7a70d54..0308a54 100644 --- a/extension/ui-regression.test.js +++ b/extension/ui-regression.test.js @@ -345,7 +345,7 @@ test('optional local config is loaded safely before app mount', () => { }); test('background keeps the toolbar badge empty', () => { - assert.match(backgroundJs, /await chrome\.action\.setBadgeText\(\{\s*text:\s*''\s*\}\)/); + assert.match(backgroundJs, /await chrome\.action\.setBadgeText\(\{\s*text:\s*["']{2}\s*\}\)/); assert.doesNotMatch(backgroundJs, /String\(count\)/); }); @@ -1056,13 +1056,16 @@ test('dynamic animation styles are generated by JavaScript instead of hardcoded test('dashboard auto-refreshes when tabs change via background message', () => { const bgJs = fs.readFileSync(path.join(__dirname, 'background.js'), 'utf8'); - // Background should notify Tab Harbor pages + // Background should broadcast tab changes via chrome.runtime.sendMessage + // (the dashboard is an extension page, NOT a content script — sendMessage + // to a content script would always fail with "Receiving end does not exist"). assert.match(bgJs, /notifyTabHarborPages/); - assert.match(bgJs, /chrome\.tabs\.sendMessage/); - assert.match(bgJs, /action:\s*'tabs-changed'/); + assert.match(bgJs, /chrome\.runtime\.sendMessage/); + assert.doesNotMatch(bgJs, /chrome\.tabs\.sendMessage/); + assert.match(bgJs, /action:\s*["']tabs-changed["']/); assert.match(bgJs, /triggerTabId/); - assert.match(bgJs, /source:\s*'tabs\.onCreated'/); - assert.match(bgJs, /source:\s*'tabs\.onUpdated'/); + assert.match(bgJs, /source:\s*["']tabs\.onCreated["']/); + assert.match(bgJs, /source:\s*["']tabs\.onUpdated["']/); assert.match(bgJs, /chrome\.tabs\.onCreated\.addListener/); assert.match(bgJs, /chrome\.tabs\.onRemoved\.addListener/); From 8173cd3b8f86aa6f0a6f73994f5f46efaf3a87c0 Mon Sep 17 00:00:00 2001 From: LiuDeTao <57797351+KanoCifer@users.noreply.github.com> Date: Thu, 9 Jul 2026 01:23:28 +0800 Subject: [PATCH 5/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- extension/ui-regression.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/ui-regression.test.js b/extension/ui-regression.test.js index 0308a54..624ec36 100644 --- a/extension/ui-regression.test.js +++ b/extension/ui-regression.test.js @@ -345,7 +345,7 @@ test('optional local config is loaded safely before app mount', () => { }); test('background keeps the toolbar badge empty', () => { - assert.match(backgroundJs, /await chrome\.action\.setBadgeText\(\{\s*text:\s*["']{2}\s*\}\)/); + assert.match(backgroundJs, /await chrome\.action\.setBadgeText\(\{\s*text:\s*(?:''|"\")\s*\}\)/); assert.doesNotMatch(backgroundJs, /String\(count\)/); }); From efa6c005280e50520b8d61a7d6536664d62be37b Mon Sep 17 00:00:00 2001 From: Kuroome Date: Thu, 9 Jul 2026 01:43:14 +0800 Subject: [PATCH 6/7] =?UTF-8?q?fix(extension):=20=E7=A7=BB=E9=99=A4=20buil?= =?UTF-8?q?dDomainGroups=20=E4=B8=AD=E5=A4=9A=E4=BD=99=E7=9A=84=20undefine?= =?UTF-8?q?d=20=E8=B5=8B=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit groupMap.__landing_pages__ = undefined 在 groupMap 中残留一个 undefined 值,导致 Object.values 后 sort 比较器访问 a.domain 时抛出 TypeError。当用户打开了落地页域名根路径的标签页时触发, 表现为标签页变更后 dashboard 渲染失败。 --- extension/dashboard-runtime.js | 1 - 1 file changed, 1 deletion(-) diff --git a/extension/dashboard-runtime.js b/extension/dashboard-runtime.js index 07abf5a..3fc866b 100644 --- a/extension/dashboard-runtime.js +++ b/extension/dashboard-runtime.js @@ -3234,7 +3234,6 @@ async function buildDomainGroups(realTabs = getRealTabs()) { } if (landingTabs.length > 0) { - groupMap.__landing_pages__ = undefined; groupMap['__landing-pages__'] = { domain: '__landing-pages__', tabs: landingTabs }; } From f413433f9a5414d2e778235c1b51bb3d6bf9a794 Mon Sep 17 00:00:00 2001 From: Kuroome Date: Thu, 9 Jul 2026 01:46:17 +0800 Subject: [PATCH 7/7] =?UTF-8?q?fix(extension):=20=E9=9D=99=E9=BB=98?= =?UTF-8?q?=E5=90=9E=E6=8E=89=E6=97=A0=E6=8E=A5=E6=94=B6=E6=96=B9=E7=9A=84?= =?UTF-8?q?=20runtime=20=E5=B9=BF=E6=92=AD=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chrome.runtime.sendMessage 在 dashboard 与 popup 都关闭时必定抛 "Receiving end does not exist",这是正常现象。原 catch 仍 console.warn 产生噪音日志。改为仅当错误非该连接错误时才告警。 --- extension/background.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/extension/background.js b/extension/background.js index 02a22d4..69e0300 100644 --- a/extension/background.js +++ b/extension/background.js @@ -91,16 +91,11 @@ async function notifyTabHarborPages(eventMeta = {}) { try { await chrome.runtime.sendMessage(message); } catch (err) { - const messageText = err && err.message ? err.message : String(err); - const noReceiverPatterns = [ - /Receiving end does not exist/i, - /接收端不存在/, - ]; - if (noReceiverPatterns.some((p) => p.test(messageText))) return; - - if (notifyTabHarborPages._lastWarn !== messageText) { - notifyTabHarborPages._lastWarn = messageText; - console.warn("[tab-harbor bg] Error notifying Tab Harbor pages:", err); + if (err?.message && !err.message.includes("Receiving end does not exist")) { + console.warn( + "[tab-harbor bg] Error notifying Tab Harbor pages:", + err.message, + ); } } }