diff --git a/extension/background/service-worker.js b/extension/background/service-worker.js index 060e74af1..9843e6f1a 100644 --- a/extension/background/service-worker.js +++ b/extension/background/service-worker.js @@ -58,7 +58,7 @@ async function buildScanConfig() { // engages with the extension (DevTools panel/sidebar opened, popup scan, etc). async function ensureContentScriptInjected(tabId) { const state = getState(tabId); - if (state.csInjected) return true; + if (state.csInjected) return { ok: true }; try { await chrome.scripting.executeScript({ target: { tabId }, @@ -66,16 +66,29 @@ async function ensureContentScriptInjected(tabId) { injectImmediately: true, }); state.csInjected = true; - return true; + return { ok: true }; } catch (err) { - // Common cause: chrome:// pages, the web store, or other restricted URLs - return false; + // Common cause: chrome:// pages, the web store, the Chrome Web Store, or + // file:// pages when "Allow access to file URLs" is off. Keep the real + // error so the UI can explain what happened. + return { ok: false, error: err?.message || String(err) }; } } async function sendScanToTab(tabId) { - const ok = await ensureContentScriptInjected(tabId); - if (!ok) return; + const { ok, error } = await ensureContentScriptInjected(tabId); + if (!ok) { + // Injection was blocked. Tell an open popup why so it can stop showing + // "Scanning..." and surface a hint. The popup may be closed, so ignore + // delivery failures. + let url = ''; + try { url = (await chrome.tabs.get(tabId))?.url || ''; } catch { /* tab gone */ } + const message = url.startsWith('file:') + ? 'Can\u2019t scan local files. Enable \u201CAllow access to file URLs\u201D for Impeccable in chrome://extensions.' + : `Couldn\u2019t scan this page${error ? `: ${error}` : '.'}`; + chrome.runtime.sendMessage({ action: 'scan-failed', tabId, message }).catch(() => {}); + return; + } const config = await buildScanConfig(); chrome.tabs.sendMessage(tabId, { action: 'scan', config }).catch(() => {}); } diff --git a/extension/popup/popup.css b/extension/popup/popup.css index 1cc43714d..85e13aac9 100644 --- a/extension/popup/popup.css +++ b/extension/popup/popup.css @@ -89,6 +89,17 @@ h1 { margin-bottom: 16px; } +.scan-error { + margin: -8px 0 16px; + font-size: 11px; + line-height: 1.4; + color: oklch(70% 0.13 30); +} + +.scan-error[hidden] { + display: none; +} + .btn { display: block; width: 100%; diff --git a/extension/popup/popup.html b/extension/popup/popup.html index 82c4b6388..e382231bb 100644 --- a/extension/popup/popup.html +++ b/extension/popup/popup.html @@ -25,6 +25,8 @@ +
+ diff --git a/extension/popup/popup.js b/extension/popup/popup.js index c8779df13..54a5a3822 100644 --- a/extension/popup/popup.js +++ b/extension/popup/popup.js @@ -8,8 +8,13 @@ const countNumber = document.getElementById('count-number'); const countLabel = document.getElementById('count-label'); const btnScan = document.getElementById('btn-scan'); const btnToggle = document.getElementById('btn-toggle'); +const scanError = document.getElementById('scan-error'); let overlaysVisible = true; +// The popup only ever reflects the active tab. Broadcasts from the service +// worker carry a tabId, so we cache the active one and ignore updates meant +// for other tabs (e.g. a DevTools-driven rescan failing on a background tab). +let activeTabId = null; async function getActiveTabId() { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); @@ -29,11 +34,14 @@ function updateFromState(state) { async function loadState() { const tabId = await getActiveTabId(); if (!tabId) return; + activeTabId = tabId; chrome.runtime.sendMessage({ action: 'get-state', tabId }, updateFromState); } // Listen for real-time updates from service worker chrome.runtime.onMessage.addListener((msg) => { + // Ignore broadcasts for a tab other than the one this popup is showing. + if (msg.tabId != null && activeTabId != null && msg.tabId !== activeTabId) return; if (msg.action === 'findings-updated') { const count = msg.findings?.reduce((sum, f) => sum + f.findings.length, 0) || 0; countNumber.textContent = String(count); @@ -41,6 +49,13 @@ chrome.runtime.onMessage.addListener((msg) => { countLabel.textContent = count === 1 ? 'anti-pattern' : 'anti-patterns'; btnScan.textContent = 'Scan page'; btnScan.disabled = false; + scanError.hidden = true; + } + if (msg.action === 'scan-failed') { + btnScan.textContent = 'Scan page'; + btnScan.disabled = false; + scanError.textContent = msg.message || 'Couldn\u2019t scan this page.'; + scanError.hidden = false; } if (msg.action === 'overlays-toggled-broadcast') { overlaysVisible = msg.visible; @@ -51,6 +66,8 @@ chrome.runtime.onMessage.addListener((msg) => { btnScan.addEventListener('click', async () => { const tabId = await getActiveTabId(); if (!tabId) return; + activeTabId = tabId; + scanError.hidden = true; btnScan.textContent = 'Scanning...'; btnScan.disabled = true; chrome.runtime.sendMessage({ action: 'scan', tabId });