Skip to content
Closed
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
38 changes: 23 additions & 15 deletions src/app/service/service_worker/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,22 +83,9 @@ export class ResourceService {
const ret: { [key: string]: Resource } = {};
await Promise.allSettled(
script.metadata[type].map(async (uri) => {
/** 资源键名 */
let resourceKey = uri;
/** 文件路径 */
let path: string | null = uri;
if (type === "resource") {
// @resource xxx https://...
const split = uri.split(/\s+/);
if (split.length === 2) {
resourceKey = split[0];
path = split[1].trim();
} else {
path = null;
}
}
const { resourceKey, path } = parseResourceMetadataItem(uri, type);
if (path) {
if (uri.startsWith("file:///")) {
if (path.startsWith("file:///")) {
// 如果是file://协议,则每次请求更新一下文件
const res = await this.updateResource(script.uuid, path, type);
ret[resourceKey] = res;
Expand Down Expand Up @@ -365,3 +352,24 @@ export class ResourceService {
});
}
}

export function parseResourceMetadataItem(
uri: string,
type: ResourceType
): { resourceKey: string; path: string | null } {
/** 资源键名 */
let resourceKey = uri;
/** 文件路径 */
let path: string | null = uri;
if (type === "resource") {
// @resource xxx https://...
const split = uri.split(/\s+/);
if (split.length === 2) {
resourceKey = split[0];
path = split[1].trim();
} else {
path = null;
}
}
return { resourceKey, path };
}
329 changes: 327 additions & 2 deletions src/app/service/service_worker/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import { vi, describe, it, expect, beforeEach, type MockedFunction } from "vites
import { randomUUID } from "crypto";
import type { Script, ScriptRunResource } from "@App/app/repo/scripts";
import { SCRIPT_STATUS_DISABLE, SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL } from "@App/app/repo/scripts";
import { getCombinedMeta } from "./utils";
import { buildScriptRunResourceBasic, getCombinedMeta, scriptURLPatternResults } from "./utils";
import type { SystemConfig } from "@App/pkg/config/config";
import type { Group } from "@Packages/message/server";
import type { ServiceWorkerMessageSend, WindowMessageBody } from "@Packages/message/window_message";
import type { IMessageQueue } from "@Packages/message/message_queue";
import type { ValueService } from "./value";
import type { ScriptService } from "./script";
import type { ResourceService } from "./resource";
import { ResourceService } from "./resource";
import type { ScriptDAO } from "@App/app/repo/scripts";
import { LocalStorageDAO } from "@App/app/repo/localStorage";
import type { MessageConnect, TMessage } from "@Packages/message/types";
import { obtainBlackList } from "@App/pkg/utils/utils";
import type { CompiledResource, Resource, ResourceType } from "@App/app/repo/resource";

initTestEnv();

Expand Down Expand Up @@ -389,3 +390,327 @@ describe.concurrent("RuntimeService - getPageScriptMatchingResultByUrl 脚本匹
});
});
});

describe("RuntimeService - getScriptsForTab 页面加载静态资料缓存", () => {
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: 1,
updatetime: 1,
checktime: 1,
metadata: {
match: ["https://www.example.com/*"],
},
...overrides,
});

const createResource = (
url: string,
type: ResourceType,
content: string,
sha512 = `${url}:${content}`
): Resource => ({
url,
content,
base64: "base64",
hash: {
md5: "",
sha1: "",
sha256: "",
sha384: "",
sha512,
},
link: {},
type,
contentType: "text/plain",
createtime: 1,
updatetime: 1,
});

const createRuntimeHarness = () => {
const scripts = new Map<string, Script>();
const compiledResources = new Map<string, CompiledResource>();
const codes = new Map<string, string>();
const resources = new Map<string, Record<string, Resource>>();
const values = new Map<string, Record<string, unknown>>();
const localResources = new Map<string, Resource>();

const mockGroup = {
use: vi.fn().mockReturnThis(),
group: vi.fn().mockReturnThis(),
on: vi.fn(),
} as unknown as Group;
const mockSender = {
async init() {},
messageHandle(_data: WindowMessageBody) {},
async connect(_data: TMessage): Promise<MessageConnect> {
return {} as MessageConnect;
},
async sendMessage<T = any>(_data: TMessage): Promise<T> {
return {} as T;
},
} as ServiceWorkerMessageSend;
const mockMessageQueue = {
group: vi.fn().mockReturnValue(mockGroup),
subscribe: vi.fn(),
publish: vi.fn(),
} as unknown as IMessageQueue;
const mockSystemConfig = {
getBlacklist: vi.fn().mockReturnValue(""),
addListener: vi.fn(),
} as unknown as SystemConfig;
const mockValueService = {
getScriptValue: vi.fn(async (script: Script) => values.get(script.uuid) || {}),
} as unknown as ValueService;
const mockResourceService = {
getScriptResources: vi.fn(async (script: Script) => resources.get(script.uuid) || {}),
updateResource: vi.fn(async (_uuid: string, url: string) => localResources.get(url)!),
} as unknown as ResourceService;
const mockScriptDAO = {
gets: vi.fn(async (uuids: string[]) => uuids.map((uuid) => scripts.get(uuid))),
get: vi.fn(async (uuid: string) => scripts.get(uuid)),
all: vi.fn().mockResolvedValue([]),
scriptCodeDAO: {
get: vi.fn(async (uuid: string) => {
const code = codes.get(uuid);
return code ? { uuid, code } : undefined;
}),
},
} as unknown as ScriptDAO;

const runtime = new RuntimeService(
mockSystemConfig,
mockGroup,
mockSender,
mockMessageQueue,
mockValueService,
{} as ScriptService,
mockResourceService,
mockScriptDAO,
new LocalStorageDAO()
);
const compiledResourceDAO = {
gets: vi.fn(async (uuids: string[]) => uuids.map((uuid) => compiledResources.get(uuid))),
get: vi.fn(async (uuid: string) => compiledResources.get(uuid)),
save: vi.fn(),
all: vi.fn().mockResolvedValue([]),
};
runtime.compiledResourceDAO = compiledResourceDAO as any;
runtime.updateSites = vi.fn();

const addScript = (script: Script, code = "// code", resource: Record<string, Resource> = {}) => {
scripts.set(script.uuid, script);
codes.set(script.uuid, code);
resources.set(script.uuid, resource);
const scriptRes = buildScriptRunResourceBasic(script);
const patterns = scriptURLPatternResults(scriptRes);
if (!patterns) return;
compiledResources.set(script.uuid, {
name: script.name,
flag: scriptRes.flag,
uuid: script.uuid,
require: [],
matches: [],
includeGlobs: [],
excludeMatches: [],
excludeGlobs: [],
allFrames: true,
world: "MAIN",
runAt: "",
scriptUrlPatterns: patterns.scriptUrlPatterns,
originalUrlPatterns:
patterns.originalUrlPatterns === patterns.scriptUrlPatterns ? null : patterns.originalUrlPatterns,
});
runtime.applyScriptMatchInfo(scriptRes);
};

return {
runtime,
addScript,
scripts,
values,
resources,
localResources,
mockScriptDAO,
compiledResourceDAO,
mockValueService,
mockResourceService,
};
};

beforeEach(() => {
vi.restoreAllMocks();
vi.spyOn(chrome.userScripts as any, "getScripts").mockResolvedValue([]);
vi.spyOn(chrome.userScripts as any, "update").mockResolvedValue(undefined);
});

it("连续页面加载应复用静态资料但每次重新读取 value", async () => {
const h = createRuntimeHarness();
const script = createMockScript();
h.values.set(script.uuid, { key: "A" });
h.addScript(script);

const first = await h.runtime.getScriptsForTab({ url: "https://www.example.com/a", tabId: 1, frameId: 0 });
h.values.set(script.uuid, { key: "B" });
const second = await h.runtime.getScriptsForTab({ url: "https://www.example.com/b", tabId: 1, frameId: 0 });

expect(first?.injectScriptList[0].value).toEqual({ key: "A" });
expect(second?.injectScriptList[0].value).toEqual({ key: "B" });
expect(h.mockScriptDAO.gets).toHaveBeenCalledTimes(1);
expect(h.compiledResourceDAO.gets).toHaveBeenCalledTimes(1);
expect(h.mockResourceService.getScriptResources).toHaveBeenCalledTimes(1);
expect(h.mockScriptDAO.scriptCodeDAO.get).toHaveBeenCalledTimes(1);
expect(h.mockValueService.getScriptValue).toHaveBeenCalledTimes(2);
expect(h.runtime.__internalGetPageLoadStaticCacheSizeForTest()).toBe(1);
});

it("部分命中缓存时仍保持 inject、content 与 scriptmenus 顺序", async () => {
const h = createRuntimeHarness();
const injectA = createMockScript({ name: "A", sort: 1 });
const contentB = createMockScript({
name: "B",
sort: 2,
metadata: { match: ["https://www.example.com/*"], "inject-into": ["content"] },
});
const injectC = createMockScript({ name: "C", sort: 3 });
h.addScript(injectA);
h.addScript(contentB);
h.addScript(injectC);

await h.runtime.getScriptsForTab({ url: "https://www.example.com/a", tabId: 1, frameId: 0 });
h.runtime.__internalClearPageLoadStaticCacheForTest([contentB.uuid]);
const second = await h.runtime.getScriptsForTab({ url: "https://www.example.com/b", tabId: 1, frameId: 0 });

expect(second?.injectScriptList.map((script) => script.name)).toEqual(["A", "C"]);
expect(second?.contentScriptList.map((script) => script.name)).toEqual(["B"]);
expect(second?.scriptmenus.map((script) => script.name)).toEqual(["A", "B", "C"]);
expect(h.mockScriptDAO.gets).toHaveBeenLastCalledWith([contentB.uuid]);
});

it("中间脚本被 run-in 或 noframes 过滤后剩余脚本顺序仍正确", async () => {
const h = createRuntimeHarness();
const first = createMockScript({ name: "A", sort: 1 });
const runInFiltered = createMockScript({
name: "B",
sort: 2,
metadata: { match: ["https://www.example.com/*"], "run-in": ["incognito-tabs"] },
});
const noframesFiltered = createMockScript({
name: "C",
sort: 3,
metadata: { match: ["https://www.example.com/*"], noframes: [""] },
});
const last = createMockScript({ name: "D", sort: 4 });
h.addScript(first);
h.addScript(runInFiltered);
h.addScript(noframesFiltered);
h.addScript(last);

const result = await h.runtime.getScriptsForTab({ url: "https://www.example.com/a", tabId: 1, frameId: 1 });

expect(result?.injectScriptList.map((script) => script.name)).toEqual(["A", "D"]);
expect(result?.scriptmenus.map((script) => script.name)).toEqual(["A", "D"]);
});

it("页面过滤语义应保持不变", async () => {
const h = createRuntimeHarness();
const enabled = createMockScript();
h.addScript(enabled);

h.runtime.isLoadScripts = false;
await expect(
h.runtime.getScriptsForTab({ url: "https://www.example.com/a", tabId: 1, frameId: 0 })
).resolves.toBeNull();
h.runtime.isLoadScripts = true;

h.runtime.blacklist = obtainBlackList("https://www.example.com/*");
h.runtime.loadBlacklist();
await expect(
h.runtime.getScriptsForTab({ url: "https://www.example.com/a", tabId: 1, frameId: 0 })
).resolves.toBeNull();
h.runtime.blacklist = [];
h.runtime.loadBlacklist();

await expect(
h.runtime.getScriptsForTab({ url: "https://www.no-match.com/a", tabId: 1, frameId: 0 })
).resolves.toBeNull();

const disabledHarness = createRuntimeHarness();
disabledHarness.addScript(createMockScript({ status: SCRIPT_STATUS_DISABLE }));
await expect(
disabledHarness.runtime.getScriptsForTab({ url: "https://www.example.com/a", tabId: 1, frameId: 0 })
).resolves.toBeNull();

const excludedHarness = createRuntimeHarness();
excludedHarness.addScript(
createMockScript({
selfMetadata: { exclude: ["https://www.example.com/*"] },
})
);
await expect(
excludedHarness.runtime.getScriptsForTab({ url: "https://www.example.com/a", tabId: 1, frameId: 0 })
).resolves.toBeNull();
});

it("本地 file resource 命中缓存时应热刷新,hash 不变不更新注册,hash 变化更新返回资源和注册代码", async () => {
const h = createRuntimeHarness();
const script = createMockScript({
metadata: {
match: ["https://www.example.com/*"],
require: ["file:///tmp/require.js"],
"require-css": ["file:///tmp/style.css"],
resource: ["asset file:///tmp/local.txt"],
},
});
const initialResources = {
"file:///tmp/require.js": createResource("file:///tmp/require.js", "require", "require-v1", "require-v1"),
"file:///tmp/style.css": createResource("file:///tmp/style.css", "require-css", "style-v1", "style-v1"),
asset: createResource("file:///tmp/local.txt", "resource", "asset-v1", "asset-v1"),
};
h.localResources.set("file:///tmp/require.js", initialResources["file:///tmp/require.js"]);
h.localResources.set("file:///tmp/style.css", initialResources["file:///tmp/style.css"]);
h.localResources.set("file:///tmp/local.txt", initialResources.asset);
h.addScript(script, "console.log('script');", initialResources);

await h.runtime.getScriptsForTab({ url: "https://www.example.com/a", tabId: 1, frameId: 0 });
await h.runtime.getScriptsForTab({ url: "https://www.example.com/b", tabId: 1, frameId: 0 });

expect(h.mockResourceService.updateResource).toHaveBeenCalledTimes(3);
expect(chrome.userScripts.update).not.toHaveBeenCalled();

const changedRequire = createResource("file:///tmp/require.js", "require", "require-v2", "require-v2");
const changedAsset = createResource("file:///tmp/local.txt", "resource", "asset-v2", "asset-v2");
h.localResources.set("file:///tmp/require.js", changedRequire);
h.localResources.set("file:///tmp/local.txt", changedAsset);
vi.mocked(chrome.userScripts.getScripts as any).mockResolvedValue([{ id: script.uuid, js: [{ code: "old" }] }]);

const third = await h.runtime.getScriptsForTab({ url: "https://www.example.com/c", tabId: 1, frameId: 0 });

expect(third?.injectScriptList[0].resource["file:///tmp/require.js"].content).toBe("require-v2");
expect(third?.injectScriptList[0].resource.asset.content).toBe("asset-v2");
expect(chrome.userScripts.update).toHaveBeenCalledTimes(1);
expect(vi.mocked(chrome.userScripts.update).mock.calls[0][0][0].js?.[0].code).toContain("require-v2");
});

it("命名 @resource 的 file:/// 路径应按解析后的路径触发刷新", async () => {
const service = new ResourceService({} as Group, {} as IMessageQueue);
const fileResource = createResource("file:///tmp/local.txt", "resource", "local");
const updateResource = vi.spyOn(service, "updateResource").mockResolvedValue(fileResource);
const getResource = vi.spyOn(service, "getResource").mockResolvedValue(undefined);

const result = await service.getResourceByType(
createMockScript({ metadata: { resource: ["asset file:///tmp/local.txt"] } }),
"resource",
false
);

expect(updateResource).toHaveBeenCalledWith(expect.any(String), "file:///tmp/local.txt", "resource");
expect(getResource).not.toHaveBeenCalled();
expect(result.asset.content).toBe("local");
});
});
Loading
Loading