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
104 changes: 104 additions & 0 deletions src/app/service/service_worker/popup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,110 @@ describe("PopupService addScriptRunNumber 页面脚本执行计数", () => {
expect(mA?.runNum).toBe(2);
expect(mB?.runNum).toBe(4);
});

// tabScript:-1 是后台脚本的命名空间。旧实现在 frameId 为 0 时会先清空列表,所以不止「多出一个普通脚本」,
// 还会把真正的后台脚本整批挤掉(#1774)。
it.each([-1, 0])("tabId 为 %i(非真实标签页)时不得改动 tabScript:-1 缓存", async (tabId) => {
const backgroundMenu = createMenu("bg-script", { runNum: 1, runStatus: SCRIPT_RUN_STATUS_RUNNING });
await cacheInstance.set(`${CACHE_KEY_TAB_SCRIPT}${-1}`, [backgroundMenu]);
const { service } = createService();

for (const frameId of [0, undefined, 3]) {
await service.addScriptRunNumber({
tabId,
frameId,
url: "https://example.com/",
scriptmenus: [createMenu("normal-script", { runNum: 0 })],
});
}

await expect(service.getScriptMenu(-1)).resolves.toEqual([backgroundMenu]);
await expect(service.getScriptMenu(tabId)).resolves.toEqual(tabId === -1 ? [backgroundMenu] : []);
});

// 消息/事件边界的运行时数据不一定符合类型,undefined / NaN 也不能落成 tabScript:undefined / tabScript:NaN。
it.each([undefined, Number.NaN])("tabId 为 %s(越过类型的运行时数据)时不写入任何 tabScript 缓存", async (tabId) => {
const { service } = createService();

await service.addScriptRunNumber({
tabId: tabId as never,
frameId: 0,
url: "https://example.com/",
scriptmenus: [createMenu("normal-script", { runNum: 0 })],
});

const keys = await cacheInstance.list();
expect(keys.filter((key) => key.startsWith(CACHE_KEY_TAB_SCRIPT))).toEqual([]);
});
});

// ─────────────────────────────────────────────────────────────────────────────

describe("PopupService tabScript:-1 命名空间契约:页面事件不得触碰后台脚本列表", () => {
const backgroundUuid = "bg-script";
const normalUuid = "normal-script";
const backgroundScript = createScript(backgroundUuid, { type: SCRIPT_TYPE_BACKGROUND });
const normalScript = createScript(normalUuid, { type: SCRIPT_TYPE_NORMAL });
const byUuid = (uuid: string) => [backgroundScript, normalScript].find((script) => script.uuid === uuid);

beforeEach(async () => {
await cacheInstance.clear();
});

it("无标签页的页面加载、iframe、菜单注册/注销、启停与运行状态事件之后,tabScript:-1 仍只含后台脚本", async () => {
const { service, subscriptions } = createService({
scriptDAO: {
get: vi.fn(async (uuid: string) => byUuid(uuid)),
gets: vi.fn(async (uuids: string[]) => uuids.map(byUuid)),
},
});
service.dealBackgroundScriptInstall();
const [installHandler] = subscriptions.get("installScript") || [];
await installHandler!({
script: backgroundScript,
update: false,
} as TInstallScript);
// session 缓存的读取不拷贝,直接持有引用的话原地修改会连 expected 一起改掉,断言恒真。
const expected = structuredClone(await service.getScriptMenu(-1));
expect(expected.map((menu) => menu.uuid)).toEqual([backgroundUuid]);

const expectNamespaceUntouched = async () => {
await flushAsync();
await expect(service.getScriptMenu(-1)).resolves.toEqual(expected);
};

// 无标签页的主 frame / iframe / tabId 0 页面加载
for (const tabId of [-1, 0]) {
for (const frameId of [0, 2]) {
await service.addScriptRunNumber({
tabId,
frameId,
url: "https://example.com/",
scriptmenus: [createMenu(normalUuid, { runNum: 0 })],
});
await expectNamespaceUntouched();
}
}

// 普通脚本在无标签页页面里的 GM_registerMenuCommand / GM_unregisterMenuCommand(tabId 会落成 -1)
const menuMessage = { uuid: normalUuid, key: "menu", name: "Menu", options: {}, tabId: -1 };
await (service as any).updateRegisterMenuCommand(menuMessage, 1 /* REGISTER */);
await expectNamespaceUntouched();
await (service as any).updateRegisterMenuCommand(menuMessage, 2 /* UNREGISTER */);
await expectNamespaceUntouched();

// 普通脚本的启用 / 禁用 / 运行状态事件
const [enableHandler] = subscriptions.get("enableScripts") || [];
await enableHandler!([{ uuid: normalUuid, enable: true }] satisfies TEnableScript[]);
await expectNamespaceUntouched();
await enableHandler!([{ uuid: normalUuid, enable: false }] satisfies TEnableScript[]);
await expectNamespaceUntouched();
const [runStatusHandler] = subscriptions.get("scriptRunStatus") || [];
runStatusHandler!({ uuid: normalUuid, runStatus: SCRIPT_RUN_STATUS_RUNNING } satisfies TScriptRunStatus);
await expectNamespaceUntouched();
runStatusHandler!({ uuid: normalUuid, runStatus: SCRIPT_RUN_STATUS_COMPLETE } satisfies TScriptRunStatus);
await expectNamespaceUntouched();
});
});

// ─────────────────────────────────────────────────────────────────────────────
Expand Down
4 changes: 4 additions & 0 deletions src/app/service/service_worker/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,10 @@ export class PopupService {

async addScriptRunNumber(o: TPopupPageLoadInfo) {
const { tabId, frameId, scriptmenus } = o;
// tabScript:-1 是后台脚本的命名空间,只允许后台脚本的 install/enable/runStatus 事件写入。
// 页面执行计数若落到 tabId <= 0,frameId 为 0 时还会先清空整个列表(#1774);生产者(RuntimeService.pageLoad)已过滤,这里是第二道防线。
// 写成 !(tabId > 0) 而非 tabId <= 0:undefined / NaN 这类越过类型的运行时数据同样要拒绝。
if (!(tabId > 0)) return;
// 设置数据
await cacheInstance.tx(`${CACHE_KEY_TAB_SCRIPT}${tabId}`, (data: ScriptMenu[] | undefined, tx) => {
const isPrevDataEmpty = !data?.length;
Expand Down
55 changes: 55 additions & 0 deletions src/app/service/service_worker/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1121,6 +1121,61 @@ describe("pageLoad 按消息发送方标签页区分隐身上下文", () => {
});
});

// 弹窗把 tabId -1 当作「后台脚本」命名空间;不属于任何标签页的页面若也记到 -1,
// 普通脚本就会出现在「开启和运行的后台脚本」里(#1774)。
describe("pageLoad 页面运行计数只记到真实标签页", () => {
const pageUrl = "https://www.example.com/page";
const scriptsForTab = {
injectScriptList: [],
contentScriptList: [],
envInfo: {},
scriptmenus: [],
} as unknown as Awaited<ReturnType<RuntimeService["getScriptsForTab"]>>;
const popupPageLoadEmits = (emit: ReturnType<typeof vi.fn>) =>
emit.mock.calls.filter(([topic]) => topic === "popupPageLoadUpdate");

it.each([
["没有 tab", {}],
["tab.id 为 TAB_ID_NONE(-1)", { tab: { id: -1 } }],
])("%s 时照常下发脚本,但不记入任何标签页的运行计数", async (_label, senderTab) => {
const { runtime, mockGroup } = _createRuntimeContext();
vi.spyOn(runtime, "getScriptsForTab").mockResolvedValue(scriptsForTab);

const res = await runtime.pageLoad(
undefined,
new SenderRuntime({
id: "scriptcat-test",
url: pageUrl,
frameId: 0,
...senderTab,
} as chrome.runtime.MessageSender)
);

expect(res.ok).toBe(true);
expect(popupPageLoadEmits(mockGroup.emit)).toEqual([]);
});

it("真实标签页照常记入该标签页", async () => {
const tabId = 11;
const { runtime, mockGroup } = _createRuntimeContext();
vi.spyOn(runtime, "getScriptsForTab").mockResolvedValue(scriptsForTab);

await runtime.pageLoad(
undefined,
new SenderRuntime({
id: "scriptcat-test",
url: pageUrl,
frameId: 0,
tab: { id: tabId, incognito: false },
} as chrome.runtime.MessageSender)
);

expect(popupPageLoadEmits(mockGroup.emit)).toEqual([
["popupPageLoadUpdate", { tabId, frameId: 0, url: pageUrl, scriptmenus: [] }],
]);
});
});

describe("sandbox verified 初始化重放", () => {
it("忽略 fallback 通知,并且真实握手与重复握手只初始化一次脚本和语言监听", async () => {
const { runtime, mockSystemConfig, mockScriptDAO } = _createRuntimeContext();
Expand Down
18 changes: 11 additions & 7 deletions src/app/service/service_worker/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1279,17 +1279,21 @@ export class RuntimeService {
// 异常加载
return { ok: false };
}
const tabId = chromeSender.tab?.id || -1;
const tabId = chromeSender.tab?.id;
const frameId = chromeSender.frameId;
const incognito = chromeSender.tab?.incognito ?? false;
const res = await this.getScriptsForTab({ url, tabId, frameId, incognito });

this.mq.emit<TPopupPageLoadInfo>("popupPageLoadUpdate", {
tabId: tabId,
frameId: frameId,
url: url,
scriptmenus: res?.scriptmenus || [], // 对于 popup, resources那些不需要
});
// 页面可能不属于任何标签页(sender.tab 缺失或为 TAB_ID_NONE),脚本照常下发,但不能记运行计数:
// 弹窗把 tabId -1 当作后台脚本的命名空间,记进去普通脚本就会混进后台脚本列表(#1774)。
if (tabId !== undefined && tabId > 0) {
this.mq.emit<TPopupPageLoadInfo>("popupPageLoadUpdate", {
tabId: tabId,
frameId: frameId,
url: url,
scriptmenus: res?.scriptmenus || [], // 对于 popup, resources那些不需要
});
}

if (res) {
// 返回脚本资料,在页面加载
Expand Down
Loading