Skip to content
Merged
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
3 changes: 2 additions & 1 deletion src/app/service/service_worker/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ export class ScriptClient extends Client {
return this.do("setCheckUpdateUrl", { uuid, checkUpdate, checkUpdateUrl });
}

updateMetadata(uuid: string, key: string, value: string[]) {
// value 为 undefined 表示撤销用户覆盖,生效值回落脚本自带 metadata
updateMetadata(uuid: string, key: string, value: string[] | undefined) {
return this.do("updateMetadata", { uuid, key, value });
}
async getBatchUpdateRecordLite(i: number) {
Expand Down
163 changes: 163 additions & 0 deletions src/app/service/service_worker/script.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { initTestEnv } from "@Tests/utils";
import { ScriptService } from "./script";
import { vi, describe, it, expect, beforeEach } from "vitest";
import { randomUUID } from "crypto";
import type { Script, ScriptDAO } from "@App/app/repo/scripts";
import { SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL } from "@App/app/repo/scripts";
import type { Group } from "@Packages/message/server";
import { Server } from "@Packages/message/server";
import type { IMessageQueue } from "@Packages/message/message_queue";
import { MessageQueue } from "@Packages/message/message_queue";
import { MockMessage } from "@Packages/message/mock_message";
import EventEmitter from "eventemitter3";
import type { SystemConfig } from "@App/pkg/config/config";
import type { ValueService } from "./value";
import type { ResourceService } from "./resource";

initTestEnv();

/**
* selfMetadata 是「用户对脚本自带 @metadata 的覆盖」。
* undefined 表示撤销覆盖(生效值回落脚本自带 metadata),空数组表示用户显式清空,两者语义不同。
*/
describe("ScriptService selfMetadata 用户覆盖", () => {
let scriptService: ScriptService;
let mockScriptDAO: ScriptDAO;
let mockGroup: Group;
let mockMessageQueue: IMessageQueue;

const createMockScript = (overrides: Partial<Script> = {}): Script => ({
uuid: randomUUID(),
name: "test-script",
namespace: "test-namespace",
type: SCRIPT_TYPE_NORMAL,
status: SCRIPT_STATUS_ENABLE,
sort: 0,
runStatus: "running" as const,
createtime: Date.now(),
checktime: Date.now(),
metadata: {
name: ["test-script"],
match: ["*://script.com/*"],
exclude: ["*://ads.script.com/*"],
tag: ["script-tag"],
},
...overrides,
});

// 取回 scriptDAO.update 实际写入的 selfMetadata
const savedSelfMetadata = () => vi.mocked(mockScriptDAO.update).mock.calls[0][1].selfMetadata;

beforeEach(() => {
const eventEmitter = new EventEmitter<string, any>();
const server = new Server("test", new MockMessage(eventEmitter));
mockGroup = server.group("script");
mockMessageQueue = new MessageQueue();
mockMessageQueue.publish = vi.fn();

mockScriptDAO = {
get: vi.fn(),
update: vi.fn().mockResolvedValue(true),
} as any;

scriptService = new ScriptService(
{} as SystemConfig,
mockGroup,
mockMessageQueue,
{} as ValueService,
{} as ResourceService,
mockScriptDAO
);
});

describe("excludeUrl - popup 排除/取消排除", () => {
it("取消最后一条排除后应保存空覆盖,而不是回落脚本自带的 exclude", async () => {
// 脚本自带 @exclude,用户此前未覆盖:集合以脚本自带规则为起点
const script = createMockScript();
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.excludeUrl({ uuid: script.uuid, excludePattern: "*://ads.script.com/*", remove: true });

// 若删除覆盖,生效 exclude 会回落 metadata.exclude 使该规则复活,用户永远取消不掉
expect(savedSelfMetadata()).toEqual({ exclude: [] });
});

it("取消排除后仍有其他规则时应保存剩余规则", async () => {
const script = createMockScript({
selfMetadata: { exclude: ["*://ads.script.com/*", "*://user.com/*"] },
});
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.excludeUrl({ uuid: script.uuid, excludePattern: "*://user.com/*", remove: true });

expect(savedSelfMetadata()).toEqual({ exclude: ["*://ads.script.com/*"] });
});

it("排除新网站时应追加到覆盖中", async () => {
const script = createMockScript();
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.excludeUrl({ uuid: script.uuid, excludePattern: "*://user.com/*", remove: false });

expect(savedSelfMetadata()).toEqual({ exclude: ["*://ads.script.com/*", "*://user.com/*"] });
});
});

describe("resetMatch / resetExclude - 编辑器匹配列表", () => {
it("传入 undefined(重置)应删除用户覆盖", async () => {
const script = createMockScript({ selfMetadata: { match: ["*://user.com/*"] } });
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.resetMatch({ uuid: script.uuid, match: undefined });

expect(savedSelfMetadata()).toBeUndefined();
});

it("传入空数组(删除最后一项)应保存空覆盖", async () => {
const script = createMockScript({ selfMetadata: { match: ["*://user.com/*"] } });
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.resetMatch({ uuid: script.uuid, match: [] });

expect(savedSelfMetadata()).toEqual({ match: [] });
});

it("resetExclude 传入 undefined(重置)应删除用户覆盖", async () => {
const script = createMockScript({ selfMetadata: { exclude: ["*://user.com/*"] } });
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.resetExclude({ uuid: script.uuid, exclude: undefined });

expect(savedSelfMetadata()).toBeUndefined();
});

it("resetExclude 传入空数组(删除最后一项)应保存空覆盖", async () => {
const script = createMockScript({ selfMetadata: { exclude: ["*://user.com/*"] } });
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.resetExclude({ uuid: script.uuid, exclude: [] });

expect(savedSelfMetadata()).toEqual({ exclude: [] });
});
});

describe("updateMetadata - 标签与运行环境", () => {
it("删除最后一个标签时应保存空覆盖,而不是回落脚本自带的 tag", async () => {
const script = createMockScript({ selfMetadata: { tag: ["user-tag"] } });
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.updateMetadata({ uuid: script.uuid, key: "tag", value: [] });

expect(savedSelfMetadata()).toEqual({ tag: [] });
});

it("传入 undefined 应删除用户覆盖(run-in 选择「默认」即跟随脚本)", async () => {
const script = createMockScript({ selfMetadata: { "run-in": ["content-script"] } });
vi.mocked(mockScriptDAO.get).mockResolvedValue(script);

await scriptService.updateMetadata({ uuid: script.uuid, key: "run-in", value: undefined });

expect(savedSelfMetadata()).toBeUndefined();
});
});
});
14 changes: 7 additions & 7 deletions src/app/service/service_worker/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -761,8 +761,8 @@ export class ScriptService {
if (!script) {
throw new Error("script not found");
}
// 建立Set去掉重复(如有)
const excludeSet = new Set(exclude || []);
// 建立Set去掉重复(如有);exclude 为 undefined 表示重置,即撤销用户覆盖
const excludeSet = exclude === undefined ? undefined : new Set(exclude);
// 更新 script.selfMetadata.exclude
script = selfMetadataUpdate(script, "exclude", excludeSet);
return this.scriptDAO
Expand All @@ -783,8 +783,8 @@ export class ScriptService {
if (!script) {
throw new Error("script not found");
}
// 建立Set去掉重复(如有)
const matchSet = new Set(match || []);
// 建立Set去掉重复(如有);match 为 undefined 表示重置,即撤销用户覆盖
const matchSet = match === undefined ? undefined : new Set(match);
// 更新 script.selfMetadata.match
script = selfMetadataUpdate(script, "match", matchSet);
return this.scriptDAO
Expand Down Expand Up @@ -1371,13 +1371,13 @@ export class ScriptService {
return this.scriptDAO.update(uuid, update);
}

// 更新脚本元数据
async updateMetadata({ uuid, key, value }: { uuid: string; key: string; value: string[] }) {
// 更新脚本元数据;value 为 undefined 表示撤销用户覆盖,生效值回落脚本自带 metadata
async updateMetadata({ uuid, key, value }: { uuid: string; key: string; value: string[] | undefined }) {
let script = await this.scriptDAO.get(uuid);
if (!script) {
throw new Error("script not found");
}
const valueSet = new Set(value);
const valueSet = value === undefined ? undefined : new Set(value);
script = selfMetadataUpdate(script, key, valueSet);
return this.scriptDAO
.update(uuid, script)
Expand Down
26 changes: 24 additions & 2 deletions src/app/service/service_worker/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,38 @@ describe.concurrent("selfMetadataUpdate", () => {
expect(result).not.toBe(script);
});

it.concurrent("应该删除空字段并处理空对象", () => {
it.concurrent("传入 undefined 应删除用户覆盖并处理空对象", () => {
const script = createMockScript({
exclude: ["https://admin.com/*"],
});

const result = selfMetadataUpdate(script, "exclude", new Set());
const result = selfMetadataUpdate(script, "exclude", undefined);

expect(result.selfMetadata).toBeUndefined();
});

it.concurrent("传入空集合应保存为空覆盖而非删除用户覆盖", () => {
const script = createMockScript({
exclude: ["https://admin.com/*"],
});

const result = selfMetadataUpdate(script, "exclude", new Set());

// 空覆盖代表「用户显式清空」,与 undefined(撤销覆盖、回落脚本自带 metadata) 是两回事
expect(result.selfMetadata).toEqual({ exclude: [] });
});

it.concurrent("传入 undefined 应只删除指定 key,保留其他覆盖", () => {
const script = createMockScript({
exclude: ["https://admin.com/*"],
match: ["https://user.com/*"],
});

const result = selfMetadataUpdate(script, "exclude", undefined);

expect(result.selfMetadata).toEqual({ match: ["https://user.com/*"] });
});

it.concurrent("应该处理没有 selfMetadata 的脚本", () => {
const script = createMockScript();

Expand Down
11 changes: 6 additions & 5 deletions src/app/service/service_worker/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,18 +148,19 @@ export function getCombinedMeta(metaBase: SCMetadata, metaCustom: SCMetadata): S
return metaRet;
}

export function selfMetadataUpdate(script: Script, key: string, valueSet: Set<string>) {
// valueSet 为 undefined 表示撤销用户覆盖(生效值回落脚本自带 metadata);
// 空集合表示用户显式清空该项,两者语义不同,不可互相替代
export function selfMetadataUpdate(script: Script, key: string, valueSet: Set<string> | undefined) {
// 更新 selfMetadata 时建立浅拷贝
const selfMetadata = { ...(script.selfMetadata || {}) };
script = { ...script, selfMetadata };
const value = [...valueSet].filter((item) => typeof item === "string");
if (value.length > 0) {
selfMetadata[key] = value;
} else {
if (valueSet === undefined) {
delete selfMetadata[key];
if (Object.keys(selfMetadata).length === 0) {
script.selfMetadata = undefined; // delete script.selfMetadata;
}
} else {
selfMetadata[key] = [...valueSet].filter((item) => typeof item === "string");
}
return script;
}
Expand Down
4 changes: 2 additions & 2 deletions src/locales/de-DE/editor.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
"confirm_reset": "Zurücksetzen bestätigen?",
"confirm_delete_match": "Diese Übereinstimmung löschen bestätigen?",
"confirm_delete_exclude": "Diese Ausnahme löschen bestätigen?",
"after_deleting_match_item": "Nach dem Löschen werden die vom Skript definierten Übereinstimmungsregeln wieder zur Übereinstimmungsliste hinzugefügt",
"after_deleting_exclude_item": "Nach dem Löschen werden die vom Skript definierten Übereinstimmungsregeln wieder zur Ausnahmeliste hinzugefügt",
"after_deleting_match_item": "Nach dem Bearbeiten werden benutzerdefinierte Übereinstimmungsregeln verwendet; klicken Sie auf „Zurücksetzen“, um die vom Skript definierten Übereinstimmungsregeln wiederherzustellen.",
"after_deleting_exclude_item": "Nach dem Bearbeiten werden benutzerdefinierte Ausnahmeregeln verwendet; klicken Sie auf „Zurücksetzen“, um die vom Skript definierten Ausnahmeregeln wiederherzustellen.",
"undo": "Rückgängig",
"redo": "Wiederherstellen",
"cut": "Ausschneiden",
Expand Down
4 changes: 2 additions & 2 deletions src/locales/en-US/editor.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
"confirm_reset": "Confirm reset?",
"confirm_delete_match": "Confirm deletion of this match?",
"confirm_delete_exclude": "Confirm deletion of this exclusion?",
"after_deleting_match_item": "Match rules defined by the script are added back to the match list after deletion",
"after_deleting_exclude_item": "Match rules defined by the script are added back to the exclude list after deletion",
"after_deleting_match_item": "After editing, your custom match rules are used; click Reset to restore the script's built-in match rules.",
"after_deleting_exclude_item": "After editing, your custom exclude rules are used; click Reset to restore the script's built-in exclude rules.",
"undo": "Undo",
"redo": "Redo",
"cut": "Cut",
Expand Down
4 changes: 2 additions & 2 deletions src/locales/ja-JP/editor.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
"confirm_reset": "リセットしてもよろしいですか?",
"confirm_delete_match": "このマッチを削除してもよろしいですか?",
"confirm_delete_exclude": "この除外を削除してもよろしいですか?",
"after_deleting_match_item": "削除後、スクリプトで定義された対象ルールが対象リストに再度追加されます",
"after_deleting_exclude_item": "削除後、スクリプトで定義された対象ルールが除外リストに再度追加されます",
"after_deleting_match_item": "編集するとカスタムのマッチルールが使用されます。「リセット」をクリックするとスクリプト本来のマッチルールに戻ります。",
"after_deleting_exclude_item": "編集するとカスタムの除外ルールが使用されます。「リセット」をクリックするとスクリプト本来の除外ルールに戻ります。",
"undo": "元に戻す",
"redo": "やり直し",
"cut": "切り取り",
Expand Down
4 changes: 2 additions & 2 deletions src/locales/ru-RU/editor.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
"confirm_reset": "Подтвердить сброс?",
"confirm_delete_match": "Подтвердить удаление этого совпадения?",
"confirm_delete_exclude": "Подтвердить удаление этого исключения?",
"after_deleting_match_item": "После удаления правила совпадения, объявленные скриптом, будут добавлены обратно в список совпадений",
"after_deleting_exclude_item": "После удаления правила совпадения, объявленные скриптом, будут добавлены обратно в список исключений",
"after_deleting_match_item": "После редактирования используются пользовательские правила совпадения; нажмите «Сброс», чтобы восстановить встроенные правила совпадения скрипта.",
"after_deleting_exclude_item": "После редактирования используются пользовательские правила исключения; нажмите «Сброс», чтобы восстановить встроенные правила исключения скрипта.",
"undo": "Отменить",
"redo": "Повторить",
"cut": "Вырезать",
Expand Down
4 changes: 2 additions & 2 deletions src/locales/tr-TR/editor.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
"confirm_reset": "Sıfırlansın mı?",
"confirm_delete_match": "Bu eşleşme silinsin mi?",
"confirm_delete_exclude": "Bu dışlama silinsin mi?",
"after_deleting_match_item": "Silinen betik tarafından tanımlanan eşleşme kuralları tekrar eşleşme listesine eklenir",
"after_deleting_exclude_item": "Silinen betik tarafından tanımlanan dışlama kuralları tekrar dışlama listesine eklenir",
"after_deleting_match_item": "Düzenledikten sonra özel eşleşme kuralları kullanılır; betiğin yerleşik eşleşme kurallarını geri yüklemek için «Sıfırla»ya tıklayın.",
"after_deleting_exclude_item": "Düzenledikten sonra özel dışlama kuralları kullanılır; betiğin yerleşik dışlama kurallarını geri yüklemek için «Sıfırla»ya tıklayın.",
"undo": "Geri Al",
"redo": "Yinele",
"cut": "Kes",
Expand Down
4 changes: 2 additions & 2 deletions src/locales/vi-VN/editor.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
"confirm_reset": "Xác nhận đặt lại?",
"confirm_delete_match": "Xác nhận xóa khớp này?",
"confirm_delete_exclude": "Xác nhận xóa loại trừ này?",
"after_deleting_match_item": "Sau khi xóa, các quy tắc khớp do script khai báo sẽ được thêm lại vào danh sách khớp",
"after_deleting_exclude_item": "Sau khi xóa, các quy tắc khớp do script khai báo sẽ được thêm lại vào danh sách loại trừ",
"after_deleting_match_item": "Sau khi chỉnh sửa, các quy tắc khớp tùy chỉnh sẽ được sử dụng; nhấp «Đặt lại» để khôi phục các quy tắc khớp có sẵn của script.",
"after_deleting_exclude_item": "Sau khi chỉnh sửa, các quy tắc loại trừ tùy chỉnh sẽ được sử dụng; nhấp «Đặt lại» để khôi phục các quy tắc loại trừ có sẵn của script.",
"undo": "Hoàn tác",
"redo": "Làm lại",
"cut": "Cắt",
Expand Down
4 changes: 2 additions & 2 deletions src/locales/zh-CN/editor.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
"confirm_reset": "确定重置?",
"confirm_delete_match": "确认删除该匹配?",
"confirm_delete_exclude": "确认删除该排除?",
"after_deleting_match_item": "脚本设定的匹配项删除后会自动添加到匹配项中",
"after_deleting_exclude_item": "脚本设定的匹配项删除后会自动添加到排除项中",
"after_deleting_match_item": "编辑后将使用自定义的匹配规则;点击「重置」可恢复脚本自带的匹配规则。",
"after_deleting_exclude_item": "编辑后将使用自定义的排除规则;点击「重置」可恢复脚本自带的排除规则。",
"undo": "撤销",
"redo": "重做",
"cut": "剪切",
Expand Down
4 changes: 2 additions & 2 deletions src/locales/zh-TW/editor.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
"confirm_reset": "確定要重設嗎?",
"confirm_delete_match": "確定要刪除此符合項目嗎?",
"confirm_delete_exclude": "確定要刪除此排除項目嗎?",
"after_deleting_match_item": "腳本設定的符合項目刪除後會自動加入到符合項目中",
"after_deleting_exclude_item": "腳本設定的符合項目刪除後會自動加入到排除項目中",
"after_deleting_match_item": "編輯後將使用自訂的符合規則;點擊「重設」可恢復腳本自帶的符合規則。",
"after_deleting_exclude_item": "編輯後將使用自訂的排除規則;點擊「重設」可恢復腳本自帶的排除規則。",
"undo": "復原",
"redo": "重做",
"cut": "剪下",
Expand Down
Loading
Loading