diff --git a/src/app/service/service_worker/popup.test.ts b/src/app/service/service_worker/popup.test.ts index d42d19a21..fdf4f5750 100644 --- a/src/app/service/service_worker/popup.test.ts +++ b/src/app/service/service_worker/popup.test.ts @@ -456,7 +456,7 @@ describe("PopupService getPopupData 页面可达性(脚本猫无法触及的 expect(result.backScriptList.map((s) => s.uuid)).toEqual([bgUuid]); }); - it("黑名单页应返回 blacklist,且不列出脚本(黑名单页同样不会注入)", async () => { + it("黑名单页应返回 blacklist,并列出匹配脚本(移出黑名单后它们就会跑)", async () => { const uuid = "allsite"; const { service } = createService({ runtime: { @@ -470,7 +470,7 @@ describe("PopupService getPopupData 页面可达性(脚本猫无法触及的 const result = await service.getPopupData({ tabId: 1, url: WEB_URL }); expect(result.pageStatus).toBe("blacklist"); - expect(result.scriptList).toEqual([]); + expect(result.scriptList.map((s) => s.uuid)).toEqual([uuid]); }); it("可注入页收到 content script 报到后返回 ok,正常列出脚本", async () => { @@ -487,7 +487,7 @@ describe("PopupService getPopupData 页面可达性(脚本猫无法触及的 expect(result.scriptList.map((s) => s.uuid)).toEqual([uuid]); }); - it("可注入页但从未收到报到(页面比扩展旧 / 被策略拦下)应返回 not-injected", async () => { + it("可注入页但从未收到报到(页面比扩展旧 / 被策略拦下)应返回 not-injected,并列出匹配脚本", async () => { const uuid = "allsite"; const { service } = createService({ runtime: { getPopupPageScriptMatchingResultByUrl: matchOne(uuid) }, @@ -497,7 +497,7 @@ describe("PopupService getPopupData 页面可达性(脚本猫无法触及的 const result = await service.getPopupData({ tabId: 1, url: WEB_URL }); expect(result.pageStatus).toBe("not-injected"); - expect(result.scriptList).toEqual([]); + expect(result.scriptList.map((s) => s.uuid)).toEqual([uuid]); }); it("同 origin 内的后续导航(SPA 换页)仍算已注入", async () => { @@ -544,13 +544,18 @@ describe("PopupService getPopupData 页面可达性(脚本猫无法触及的 expect(result.scriptList.map((s) => s.uuid)).toEqual([uuid]); }); - it("file:// 页未授权文件访问时应返回 file-access-denied", async () => { + it("file:// 页未授权文件访问时应返回 file-access-denied,并列出匹配脚本", async () => { vi.spyOn(extensionMock, "isAllowedFileSchemeAccess").mockResolvedValue(false); - const { service } = createService(); + const uuid = "allsite"; + const { service } = createService({ + runtime: { getPopupPageScriptMatchingResultByUrl: matchOne(uuid) }, + scriptDAO: { gets: vi.fn().mockResolvedValue([createScript(uuid)]) }, + }); const result = await service.getPopupData({ tabId: 1, url: "file:///tmp/a.html" }); expect(result.pageStatus).toBe("file-access-denied"); + expect(result.scriptList.map((s) => s.uuid)).toEqual([uuid]); }); it("未注入且全局脚本开关已关闭时应指出开关,而不是让用户白刷新", async () => { @@ -569,7 +574,7 @@ describe("PopupService getPopupData 页面可达性(脚本猫无法触及的 expect(result.pageStatus).toBe("userscripts-unavailable"); }); - it("关掉开关不会杀死已注入页面上正在跑的脚本,该页仍应为 ok", async () => { + it("全局开关已关闭时,本 tab 报到过也应报 scripts-disabled:提示不能取决于标签页新旧", async () => { const uuid = "allsite"; const { service } = createService({ runtime: { isLoadScripts: false, getPopupPageScriptMatchingResultByUrl: matchOne(uuid) }, @@ -579,7 +584,33 @@ describe("PopupService getPopupData 页面可达性(脚本猫无法触及的 const result = await service.getPopupData({ tabId: 1, url: WEB_URL }); - expect(result.pageStatus).toBe("ok"); + expect(result.pageStatus).toBe("scripts-disabled"); + expect(result.scriptList.map((s) => s.uuid)).toEqual([uuid]); + }); + + it("全局开关已关闭的新标签页仍应列出按网址匹配的脚本", async () => { + const uuid = "allsite"; + const { service } = createService({ + runtime: { isLoadScripts: false, getPopupPageScriptMatchingResultByUrl: matchOne(uuid) }, + scriptDAO: { gets: vi.fn().mockResolvedValue([createScript(uuid)]) }, + }); + + const result = await service.getPopupData({ tabId: 1, url: WEB_URL }); + + expect(result.pageStatus).toBe("scripts-disabled"); + expect(result.scriptList.map((s) => s.uuid)).toEqual([uuid]); + }); + + it("UserScripts API 不可用时同样列出匹配脚本:解除后这些脚本就会生效", async () => { + const uuid = "allsite"; + const { service } = createService({ + runtime: { isUserScriptsAvailable: false, getPopupPageScriptMatchingResultByUrl: matchOne(uuid) }, + scriptDAO: { gets: vi.fn().mockResolvedValue([createScript(uuid)]) }, + }); + + const result = await service.getPopupData({ tabId: 1, url: WEB_URL }); + + expect(result.pageStatus).toBe("userscripts-unavailable"); expect(result.scriptList.map((s) => s.uuid)).toEqual([uuid]); }); diff --git a/src/app/service/service_worker/popup.ts b/src/app/service/service_worker/popup.ts index 1c6b3fe77..f921137e3 100644 --- a/src/app/service/service_worker/popup.ts +++ b/src/app/service/service_worker/popup.ts @@ -376,9 +376,11 @@ export class PopupService { async getPopupData(req: GetPopupDataReq): Promise { const { url, tabId } = req; const pageStatus = await this.getPageStatus(tabId, url); - if (pageStatus !== "ok") { - // 页面上不会有任何脚本运行,列出「匹配到的」脚本只会让人以为它们在跑(#1687); - // 后台脚本与当前页无关,照常返回。 + if (pageStatus === "restricted") { + // 浏览器保留页与自家扩展商店上脚本猫永远触及不到,列出「匹配到的」脚本只会让人以为 + // 它们在跑(#1687)。其余状态的抑制原因都可以解除——开开关、开开发者模式、移出黑名单、 + // 给文件访问权限、刷新页面——照常列出匹配脚本,用户才知道解除后哪些会生效;顶部提示 + // 已经说明了它们现在为什么没跑。后台脚本与当前页无关,照常返回。 return { pageStatus, scriptList: [], @@ -443,6 +445,9 @@ export class PopupService { * 判断当前页脚本猫是否触及得到。 * * 顺序有意为之:浏览器保留页与黑名单是「无论如何都不会注入」的确定结论,先判; + * 接着是扩展整体没跑起来的两种情况(全局开关关闭、UserScripts API 不可用),它们与具体 + * 标签页无关,必须先于注入证据 —— 注入证据只是「本 tab 曾经报到过」,页面刷新与关开关都 + * 不会让它失效,用它去否定全局状态会使同一开关状态下老标签页没提示、新标签页有提示。 * 其余情况以「本 tab 有没有 content script 报到」为准 —— 它是运行时证据, * 比协议白名单准(企业策略、扩展商店等都拦不住白名单)。file:// 的权限查询只用来 * 给未注入的情况一个更准确的原因,不能反过来否定已经注入成功的事实(Firefox 上该 @@ -452,13 +457,12 @@ export class PopupService { const kind = getPageAccessKind(url); if (kind === "restricted") return "restricted"; if (this.runtime.isUrlBlacklist(url)) return "blacklist"; - if (await this.isTabInjected(tabId, url)) return "ok"; - // 以下都是「确认没注入」,只为给出更准确的原因。 // 脚本功能整体没开时 content script 根本没注册(registerUserscripts 直接 return), // 此时说「刷新页面后生效」是错的——刷新永远不会生效,得先开开关/开发者模式。 - // 同样放在注入证据之后:关掉开关不会杀死已注入页面上正在跑的脚本。 if (!this.runtime.isUserScriptsAvailable) return "userscripts-unavailable"; if (!this.runtime.isLoadScripts) return "scripts-disabled"; + if (await this.isTabInjected(tabId, url)) return "ok"; + // 以下都是「确认没注入」,只为给出更准确的原因。 // 两项判据都与浏览器有关(Edge 商店在 Chrome 里是普通网页;Firefox 的文件访问开关语义也不同), // 放在注入证据之后才不会误伤实际能运行的页面。 if (isExtensionStoreUrl(url)) return "restricted";