Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "scriptcat",
"version": "1.5.0-beta.1",
"version": "1.5.0-beta.2",
"description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!",
"author": "CodFrm",
"license": "GPLv3",
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function createFirefoxManifest(manifest, agentEnabled, firefoxId) {
browser_specific_settings: {
gecko: {
id: firefoxId,
strict_min_version: "154.0a1",
strict_min_version: "153",
data_collection_permissions: {
required: ["none"],
optional: ["authenticationInfo", "personallyIdentifyingInfo"],
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ describe("构建配置 - 浏览器专用 manifest", () => {
expect(result.incognito).toBe("spanning");
expect(result).not.toHaveProperty("message_serialization");
expect(result.browser_specific_settings.gecko.id).toBe("{firefox-id}");
expect(result.browser_specific_settings.gecko.strict_min_version).toBe("154.0a1");
expect(result.browser_specific_settings.gecko.strict_min_version).toBe("153");
expect(result.commands).toEqual({ _execute_action: {} });
expect(source.optional_permissions).toEqual(["background", "userScripts"]);
expect(source.background).toHaveProperty("service_worker");
Expand Down
25 changes: 20 additions & 5 deletions scripts/check-issue-templates.test.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, afterEach } from "vitest";
import { describe, it, expect, beforeAll, afterEach } from "vitest";
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
import path from "node:path";
import os from "node:os";
Expand Down Expand Up @@ -81,10 +81,6 @@ describe("issue 模板机械检查", () => {
expect(problemsOf(makeFixtureRoot())).toEqual([]);
});

it("仓库现有的 issue 模板应全部通过检查", () => {
expect(problemsOf(REPO_ROOT)).toEqual([]);
});

describe("YAML 与 issue-form schema", () => {
it("未加引号导致的 YAML 解析失败应报错,而不是被跳过", () => {
// description 里裸露的 `: ` 会让 YAML 认为这里又开了一个映射键。
Expand Down Expand Up @@ -224,3 +220,22 @@ describe("issue 模板机械检查", () => {
});
});
});

// 真实仓库模板检查是 I/O 密集的集成检查,不是微单元测试:它要读取全部 12 个模板并做 YAML 解析,
// 再对全量 src 做 issues/new 预填契约的 TS AST 扫描,还要加载 yaml / typescript 两个重型依赖
// (冷启动实测 ~200ms,CI 并行负载下还会成倍放大)。它不该套用 fast 项目的 340ms 单元预算——
// 那正是它偶发超时(CI 偶发 Test timed out in 340ms)的根因,与上面的 fixture 测试成本不同。
//
// 因此把「计算」放进 beforeAll:vitest 的 hook 走默认 10s 预算,与 testTimeout 无关,只算一次
// 并缓存;「断言」阶段的 it 只对已算好的结果做快照校验,微秒级完成,也不会把仓库扫描重复执行。
// 以后若要加更多真实仓库断言,直接读下面的缓存即可。
describe("仓库现有模板(集成检查)", () => {
let repoCheck;
beforeAll(() => {
repoCheck = runCheck(REPO_ROOT);
});

it("全部通过机械检查", () => {
expect(repoCheck.problems).toEqual([]);
});
});
3 changes: 3 additions & 0 deletions src/app/repo/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export type SCMetadata = Partial<Record<string, string[]>>;

// OnlyRunOnUrl writes an empty include override; retain its provenance so resetMatch can distinguish it from a user override.
export const SELF_METADATA_ONLY_RUN_ON_URL = "__scriptcat_only_run_on_url";

// 解析标签 允许使用逗号和空格分隔
export function parseTags(metadata: SCMetadata): string[] {
const tags = new Set<string>();
Expand Down
4 changes: 4 additions & 0 deletions src/app/service/service_worker/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ export class ScriptClient extends Client {
return this.do("allowUrl", { uuid, matchPattern, excludePattern });
}

excludeFromMatch(uuid: string, matchPattern: string) {
return this.do("excludeFromMatch", { uuid, matchPattern });
}

// 重置匹配项
resetMatch(uuid: string, match: string[] | undefined) {
return this.do("resetMatch", { uuid, match });
Expand Down
59 changes: 48 additions & 11 deletions src/app/service/service_worker/popup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const createMenu = (uuid: string, overrides: Partial<ScriptMenu> = {}): ScriptMe
runNumByIframe: 0,
menus: [],
isEffective: true,
hasMatchOverride: false,
...overrides,
});

Expand Down Expand Up @@ -104,23 +105,17 @@ describe("PopupService 删除脚本后 Popup 菜单残留清理", () => {
await cacheInstance.clear();
});

it("getPopupData 读取 Popup 数据时,应过滤掉 runScripts 缓存里已删除脚本的残留记录", async () => {
it("getPopupData 读取 Popup 数据时,不应显示 runScripts 缓存中的未匹配脚本", async () => {
const deletedUuid = "deleted-script";
const liveUuid = "live-script";
await cacheInstance.set(`${CACHE_KEY_TAB_SCRIPT}${1}`, [createMenu(deletedUuid), createMenu(liveUuid)]);

const { service, scriptDAO } = createService({
scriptDAO: {
gets: vi.fn(async (uuids: string[]) =>
uuids.map((uuid) => (uuid === liveUuid ? createScript(uuid) : undefined))
),
},
});
const { service, scriptDAO } = createService();

const result = await service.getPopupData({ tabId: 1, url: "https://example.com/" });

expect(result.scriptList.map((script) => script.uuid)).toEqual([liveUuid]);
expect(scriptDAO.gets).toHaveBeenCalledWith([deletedUuid, liveUuid]);
expect(result.scriptList).toEqual([]);
expect(scriptDAO.gets).toHaveBeenCalledWith([]);
});

it("updateRegisterMenuCommand 应忽略已删除脚本发来的迟到 GM_registerMenuCommand", async () => {
Expand Down Expand Up @@ -288,7 +283,7 @@ describe("PopupService getPopupData Popup 数据获取与合并", () => {
isUrlBlacklist: vi.fn().mockReturnValue(false),
},
scriptDAO: {
gets: vi.fn().mockResolvedValue([createScript(uuid)]),
gets: vi.fn().mockResolvedValue([createScript(uuid, { selfMetadata: { match: ["*://example.com/*"] } })]),
},
});

Expand All @@ -298,9 +293,29 @@ describe("PopupService getPopupData Popup 数据获取与合并", () => {
expect(result.scriptList[0].uuid).toBe(uuid);
expect(result.scriptList[0].isEffective).toBe(true);
expect(result.scriptList[0].enable).toBe(true);
expect(result.scriptList[0].hasMatchOverride).toBe(true);
expect(result.isBlacklist).toBe(false);
});

it("无 match 覆盖的脚本(无运行缓存)hasMatchOverride 应为 false", async () => {
const uuid = "no-match-uuid";
const matchMap = new Map([[uuid, { uuid, effective: true }]]);

const { service } = createService({
runtime: {
getPopupPageScriptMatchingResultByUrl: vi.fn().mockResolvedValue(matchMap),
isUrlBlacklist: vi.fn().mockReturnValue(false),
},
scriptDAO: {
gets: vi.fn().mockResolvedValue([createScript(uuid)]),
},
});

const result = await service.getPopupData({ tabId: 1, url: "https://example.com/" });

expect(result.scriptList[0].hasMatchOverride).toBe(false);
});

it("脚本同时在匹配结果与运行缓存中,应复用缓存记录(保留 runNum)并更新 enable/isEffective/hasUserConfig", async () => {
const uuid = "run-uuid";
const matchMap = new Map([[uuid, { uuid, effective: false }]]);
Expand All @@ -326,6 +341,28 @@ describe("PopupService getPopupData Popup 数据获取与合并", () => {
expect(result.scriptList[0].hasUserConfig).toBe(true); // script.config 存在
});

it("合并 run 记录时应按当前脚本同步 hasMatchOverride(match 覆盖存在性)", async () => {
const uuid = "run-uuid-2";
const matchMap = new Map([[uuid, { uuid, effective: false }]]);
// 运行缓存中存有旧的 hasMatchOverride 记录(模拟缓存来自更早的一次合并)
await cacheInstance.set(`${CACHE_KEY_TAB_SCRIPT}${1}`, [createMenu(uuid, { runNum: 3, hasMatchOverride: false })]);

const { service } = createService({
runtime: {
getPopupPageScriptMatchingResultByUrl: vi.fn().mockResolvedValue(matchMap),
isUrlBlacklist: vi.fn().mockReturnValue(false),
},
scriptDAO: {
gets: vi.fn().mockResolvedValue([createScript(uuid, { selfMetadata: { match: ["*://example.com/*"] } })]),
},
});

const result = await service.getPopupData({ tabId: 1, url: "https://example.com/" });

expect(result.scriptList[0].runNum).toBe(3); // 保留缓存中的执行次数
expect(result.scriptList[0].hasMatchOverride).toBe(true); // 按当前脚本的 match 覆盖同步
});

it("URL 无任何匹配脚本时,scriptList 为空;后台脚本应出现在 backScriptList", async () => {
const bgUuid = "bg-uuid";
// 预置后台菜单缓存(tabId = -1)
Expand Down
14 changes: 1 addition & 13 deletions src/app/service/service_worker/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ export class PopupService {
// 如果脚本已经存在,则不添加,更新信息
run.enable = script.status === SCRIPT_STATUS_ENABLE;
run.isEffective = o.effective!;
run.hasMatchOverride = script.selfMetadata?.match !== undefined;
run.hasUserConfig = !!script.config;
} else {
// 由于目前没有在 Popup 显示 @match @include @exclude, 所以以下代码暂不需要
Expand All @@ -407,19 +408,6 @@ export class PopupService {
scriptMenuMap.set(uuid, run);
}

// 将未匹配当前 url 但仍在运行的脚本,附加到清单末端,避免使用者找不到其菜单。
// 这些记录来自 tabScript:<tabId> session cache;脚本删除事件与 Popup 读取可能交错,
// 因此这里必须用 DAO 结果做读侧防护,避免已删除脚本残留在 Popup 清单。
const unmatchedRunScripts = runScripts.filter((script) => !scriptMenuMap.has(script.uuid));
if (unmatchedRunScripts.length) {
const existingRunScripts = await this.scriptDAO.gets(unmatchedRunScripts.map((script) => script.uuid));
for (let idx = 0, l = unmatchedRunScripts.length; idx < l; idx++) {
const script = unmatchedRunScripts[idx];
if (existingRunScripts[idx]) {
scriptMenuMap.set(script.uuid, script);
}
}
}
const scriptMenu = [...scriptMenuMap.values()];
// 检查是否在黑名单中
const isBlacklist = this.runtime.isUrlBlacklist(url);
Expand Down
21 changes: 20 additions & 1 deletion src/app/service/service_worker/popup_scriptmenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { changeLanguage } from "@App/locales/locales";
import { initTestLanguage } from "@Tests/initTestLanguage";
import type { Script } from "@App/app/repo/scripts";
import type { ScriptMenu } from "./types";
import { applyScriptDisplayInfo } from "./popup_scriptmenu";
import { applyScriptDisplayInfo, scriptToMenu } from "./popup_scriptmenu";

const makeMenu = (over: Partial<ScriptMenu> = {}): ScriptMenu => ({
uuid: "u1",
Expand All @@ -16,6 +16,7 @@ const makeMenu = (over: Partial<ScriptMenu> = {}): ScriptMenu => ({
runNumByIframe: 0,
menus: [],
isEffective: null,
hasMatchOverride: false,
...over,
});

Expand All @@ -24,6 +25,24 @@ const makeMenu = (over: Partial<ScriptMenu> = {}): ScriptMenu => ({
const makeScript = (metadata: Record<string, string[]>, name = "Raw Name"): Script =>
({ name, metadata }) as unknown as Script;

describe("scriptToMenu Script 转 ScriptMenu", () => {
it("无 selfMetadata.match 覆盖时 hasMatchOverride 应为 false", () => {
expect(scriptToMenu(makeScript({})).hasMatchOverride).toBe(false);
});

it("存在 selfMetadata.match 覆盖时 hasMatchOverride 应为 true", () => {
const script = makeScript({});
script.selfMetadata = { match: ["*://example.com/*"] };
expect(scriptToMenu(script).hasMatchOverride).toBe(true);
});

it("selfMetadata.match 为空数组时(显式空覆盖)hasMatchOverride 应为 true", () => {
const script = makeScript({});
script.selfMetadata = { match: [] };
expect(scriptToMenu(script).hasMatchOverride).toBe(true);
});
});

describe("applyScriptDisplayInfo Popup 展示信息补充", () => {
beforeAll(() => initTestLanguage("zh-CN"));

Expand Down
1 change: 1 addition & 0 deletions src/app/service/service_worker/popup_scriptmenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const scriptToMenu = (script: Script): ScriptMenu => {
runNumByIframe: 0,
menus: [],
isEffective: null,
hasMatchOverride: script.selfMetadata?.match !== undefined,
};
};

Expand Down
39 changes: 39 additions & 0 deletions src/app/service/service_worker/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,45 @@ describe.concurrent("RuntimeService - getPageScriptMatchingResultByUrl 脚本匹
});
});

it.concurrent("match 覆盖清空后不再保留此前的匹配规则", async () => {
const { runtime } = createRuntimeTestContext();
const script = createMockScript({
metadata: { match: ["https://www.example.com/*"] },
selfMetadata: { match: ["https://www.example.com/*"] },
});

await runtime.applyScriptMatchInfo(createScriptRunResource(script));
expect(runtime.getPageScriptMatchingResultByUrl("https://www.example.com/").has(script.uuid)).toBe(true);

const emptyMatchOverride = createScriptRunResource({
...script,
selfMetadata: { match: [] },
});
expect(await runtime.applyScriptMatchInfo(emptyMatchOverride)).toBeUndefined();

expect(runtime.getPageScriptMatchingResultByUrl("https://www.example.com/", true).has(script.uuid)).toBe(false);
});

it.concurrent("空匹配覆盖时应删除持久化 CompiledResource 并注销旧注册", async () => {
const { runtime, mockScriptService } = createRuntimeTestContext();
const script = createMockScript({
metadata: { match: ["https://www.example.com/*"] },
selfMetadata: { match: [] },
status: SCRIPT_STATUS_ENABLE,
});
const scriptRunResource = createScriptRunResource(script);
mockScriptService.buildScriptRunResource.mockResolvedValue(scriptRunResource);

const deleteSpy = vi.spyOn(runtime.compiledResourceDAO, "delete").mockResolvedValue(undefined);
const unregisterSpy = vi.spyOn(runtime, "unregistryPageScripts").mockResolvedValue(undefined);

await runtime.updateResourceOnScriptChange(script);

// 空覆盖 = 全站不匹配:旧 CompiledResource 与浏览器注册必须被清掉,否则 SW 重启后旧范围复活
expect(deleteSpy).toHaveBeenCalledWith(script.uuid);
expect(unregisterSpy).toHaveBeenCalledWith([script.uuid]);
});

describe.concurrent("includeDisabled 选项", () => {
it.concurrent("当 includeDisabled=false 时不返回禁用脚本;当 includeDisabled=true 时返回禁用脚本", async () => {
// Arrange
Expand Down
13 changes: 12 additions & 1 deletion src/app/service/service_worker/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,14 @@ export class RuntimeService {
}
// 安装,启用,或earlyStartScript的value更新
const ret = await this.buildAndSaveCompiledResourceFromScript(script, true);
if (!ret) return;
if (!ret) {
// 空匹配覆盖(match 与 include 均为空)时脚本不再匹配任何站点。
// 内存 matcher 已由 applyScriptMatchInfo 清空,这里再清掉持久化的 CompiledResource
// 并注销浏览器旧注册,否则 SW 重启后 waitInit 会信任旧资源、让旧范围复活。
await this.compiledResourceDAO.delete(script.uuid);
await this.unregistryPageScripts([script.uuid]);
return;
}
const { apiScript } = ret;
await this.loadPageScript(script, apiScript!);
}
Expand Down Expand Up @@ -1692,6 +1699,10 @@ export class RuntimeService {
async applyScriptMatchInfo(scriptRes: ScriptRunResource) {
const o = scriptURLPatternResults(scriptRes);
if (!o) {
const { uuid } = scriptRes;
this.scriptMatchEnable.clearRules(uuid);
this.scriptMatchEnable.clearRules(this.getOriginalMatchUuid(uuid));
this.cachedPatterns.delete(uuid);
return undefined;
}
// 构建脚本匹配信息
Expand Down
Loading
Loading