Skip to content

Commit 870d1dd

Browse files
committed
更多测试
1 parent 40cee32 commit 870d1dd

1 file changed

Lines changed: 232 additions & 2 deletions

File tree

src/app/service/content/create_context.test.ts

Lines changed: 232 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
1-
import { describe, it, expect } from "vitest";
2-
import { shouldFnBind } from "./create_context";
1+
import { afterEach, describe, it, expect, vi } from "vitest";
2+
import type { TScriptInfo } from "@App/app/repo/scripts";
3+
import { createContext, createProxyContext, shouldFnBind } from "./create_context";
4+
5+
const createScriptInfo = (metadata: Record<string, string[]> = {}): TScriptInfo =>
6+
({
7+
id: 1,
8+
uuid: "script-uuid",
9+
name: "create-context-test",
10+
metadata: {
11+
grant: ["none"],
12+
version: ["1.0.0"],
13+
...metadata,
14+
},
15+
code: "",
16+
sourceCode: "",
17+
value: {
18+
foo: "bar",
19+
nested: { a: 1 },
20+
},
21+
resource: {},
22+
}) as unknown as TScriptInfo;
23+
24+
const createTestContext = (grants: string[], metadata: Record<string, string[]> = {}) =>
25+
createContext(
26+
createScriptInfo(metadata),
27+
{ script: { name: "create-context-test" }, scriptMetaStr: "" },
28+
"vitest",
29+
undefined as any,
30+
undefined as any,
31+
new Set(grants)
32+
);
333

434
describe.concurrent("shouldFnBind", () => {
535
it.concurrent("不处理非原生函数", () => {
@@ -46,3 +76,203 @@ describe.concurrent("shouldFnBind", () => {
4676
expect(shouldFnBind(o.setTimeoutForTest2)).toBe(true);
4777
});
4878
});
79+
80+
describe("createContext", () => {
81+
it("按 @grant 注入 GM_ 与 GM.* 双命名空间,并忽略未知 grant", async () => {
82+
const context = createTestContext(["GM_getValue", "GM_setValue", "GM.cookie", "not_exist"]);
83+
84+
expect(context.GM.info).toBe(context.GM_info);
85+
expect(context.unsafeWindow).toBe(global);
86+
expect(context.GM_getValue("foo")).toBe("bar");
87+
expect(await context.GM.getValue("foo")).toBe("bar");
88+
expect(context.GM_setValue.name).toBe("bound GM_setValue");
89+
expect(context.GM.setValue.name).toBe("bound GM.setValue");
90+
expect(context.GM.cookie.name).toBe("bound GM.cookie");
91+
expect(context.GM.cookie.set.name).toBe("bound GM.cookie.set");
92+
expect(context.GM.cookie.list.name).toBe("bound GM.cookie.list");
93+
expect(context.not_exist).toBeUndefined();
94+
});
95+
96+
it("重复 grant 与依赖 grant 会保留 GM_ / GM.* 互通", async () => {
97+
const context = createTestContext(["GM_getValues", "GM.getValues", "GM_getValues"]);
98+
99+
expect(context.GM_getValues(["foo"])).toEqual({ foo: "bar" });
100+
await expect(context.GM.getValues(["foo"])).resolves.toEqual({
101+
foo: "bar",
102+
});
103+
});
104+
105+
it("兼容 GM.Cookie 风格的多级命名空间", () => {
106+
const context = createTestContext(["GM_cookie"]);
107+
108+
expect(context.GM_cookie.name).toBe("bound GM_cookie");
109+
expect(context.GM_cookie.set.name).toBe("bound GM_cookie.set");
110+
expect(context.GM_cookie.list.name).toBe("bound GM_cookie.list");
111+
expect(context.GM_cookie.delete.name).toBe("bound GM_cookie.delete");
112+
expect(context.GM.cookie.name).toBe("bound GM.cookie");
113+
expect(context.GM.cookie.set.name).toBe("bound GM.cookie.set");
114+
expect(context.GM.cookie.list.name).toBe("bound GM.cookie.list");
115+
expect(context.GM.cookie.delete.name).toBe("bound GM.cookie.delete");
116+
});
117+
118+
it("window grant 先挂到 context.window,再由代理沙盒暴露为 window 方法", () => {
119+
const context = createTestContext(["window.close", "window.focus"]);
120+
const sandbox = createProxyContext(context);
121+
122+
expect(context.close).toBeUndefined();
123+
expect(context.window.close.name).toBe("bound window.close");
124+
expect(context.window.focus.name).toBe("bound window.focus");
125+
expect(sandbox.close).toBe(context.window.close);
126+
expect(sandbox.focus).toBe(context.window.focus);
127+
});
128+
129+
it("early-start 脚本的 CAT_scriptLoaded 会返回等待 Promise", () => {
130+
const context = createTestContext(["CAT_scriptLoaded"], {
131+
"early-start": [""],
132+
"run-at": ["document-start"],
133+
});
134+
135+
expect(context.CAT_scriptLoaded()).toEqual(expect.any(Promise));
136+
});
137+
138+
it("非 early-start 脚本的 CAT_scriptLoaded 不会产生等待 Promise", () => {
139+
const context = createTestContext(["CAT_scriptLoaded"], {
140+
"run-at": ["document-end"],
141+
});
142+
143+
expect(context.CAT_scriptLoaded()).toBeUndefined();
144+
});
145+
});
146+
147+
describe("createProxyContext", () => {
148+
afterEach(() => {
149+
vi.restoreAllMocks();
150+
});
151+
152+
it("隔离沙盒全局对象、保护内部字段,并提供一次性的 $ 入口", () => {
153+
const context = createTestContext(["GM_getValue"]);
154+
const sandbox = createProxyContext(context);
155+
156+
expect(sandbox.window).toBe(sandbox);
157+
expect(sandbox.self).toBe(sandbox);
158+
expect(sandbox.globalThis).toBe(sandbox);
159+
expect(sandbox.parent).toBe(sandbox);
160+
expect(Object.getPrototypeOf(sandbox)).toBeNull();
161+
expect(Object.prototype.toString.call(sandbox)).toBe(Object.prototype.toString.call(global));
162+
expect(sandbox.constructor).toBe(global.constructor);
163+
// jsdom 的 top/frames 会返回 Window proxy;真实浏览器自引用由 example/tests/sandbox_test.js 覆盖。
164+
expect(sandbox.GM_getValue("foo")).toBe("bar");
165+
expect(sandbox.unsafeWindow).toBe(global);
166+
expect(sandbox.define).toBeUndefined();
167+
expect(sandbox.module).toBeUndefined();
168+
expect(sandbox.exports).toBeUndefined();
169+
expect(sandbox.console).not.toBe(console);
170+
expect(sandbox.console.log).toBe(console.log);
171+
172+
const firstDollarRead = sandbox.$;
173+
expect(firstDollarRead).toBe(sandbox);
174+
expect("$" in sandbox).toBe(false);
175+
expect(sandbox.$).toBeUndefined();
176+
});
177+
178+
it("Object.prototype 污染不会穿透到沙盒 window", () => {
179+
const key = `polluted_${Date.now()}`;
180+
try {
181+
//@ts-ignore
182+
Object.prototype[key] = "polluted";
183+
const sandbox = createProxyContext(createTestContext([]));
184+
185+
expect({}[key]).toBe("polluted");
186+
expect(sandbox[key]).toBeUndefined();
187+
expect(key in sandbox).toBe(false);
188+
} finally {
189+
//@ts-ignore
190+
delete Object.prototype[key];
191+
}
192+
});
193+
194+
it("原生函数会绑定到真实 global,避免作为裸函数调用时报 Illegal invocation", () => {
195+
const sandbox = createProxyContext(createTestContext([]));
196+
const setTimeoutForTest1 = sandbox.setTimeoutForTest1;
197+
198+
expect(() => setTimeoutForTest1(() => undefined, 0)).not.toThrow();
199+
});
200+
201+
it("onxxx 事件属性使用沙盒 this,并在清空后移除页面监听", () => {
202+
const addEventListener = vi.spyOn(global, "addEventListener");
203+
const removeEventListener = vi.spyOn(global, "removeEventListener");
204+
const sandbox = createProxyContext(createTestContext([]));
205+
const onload = vi.fn(function (this: any) {
206+
expect(this).toBe(sandbox);
207+
});
208+
209+
sandbox.onload = onload;
210+
expect(addEventListener).toHaveBeenCalledWith("load", expect.any(Object));
211+
212+
const eventObject = addEventListener.mock.calls.find(([name]) => name === "load")?.[1] as EventListenerObject;
213+
eventObject.handleEvent(new Event("load"));
214+
expect(onload).toHaveBeenCalledTimes(1);
215+
216+
sandbox.onload = null;
217+
expect(removeEventListener).toHaveBeenCalledWith("load", eventObject);
218+
});
219+
220+
it("onxxx primitive 会转为 null,普通对象只保存不注册监听", () => {
221+
const addEventListener = vi.spyOn(global, "addEventListener");
222+
const sandbox = createProxyContext(createTestContext([]));
223+
const listenerObject = { handleEvent: vi.fn() };
224+
225+
//@ts-ignore
226+
sandbox.onload = 123;
227+
expect(sandbox.onload).toBeNull();
228+
229+
//@ts-ignore
230+
sandbox.onload = "text";
231+
expect(sandbox.onload).toBeNull();
232+
233+
//@ts-ignore
234+
sandbox.onload = listenerObject;
235+
expect(sandbox.onload).toBe(listenerObject);
236+
expect(addEventListener).not.toHaveBeenCalledWith("load", expect.any(Object));
237+
});
238+
239+
it("onxxx 函数替换不会重复注册监听,并且只调用最新函数", () => {
240+
const addEventListener = vi.spyOn(global, "addEventListener");
241+
const removeEventListener = vi.spyOn(global, "removeEventListener");
242+
const sandbox = createProxyContext(createTestContext([]));
243+
const oldHandler = vi.fn();
244+
const newHandler = vi.fn();
245+
246+
sandbox.onload = oldHandler;
247+
sandbox.onload = newHandler;
248+
249+
const loadListeners = addEventListener.mock.calls.filter(([name]) => name === "load");
250+
expect(loadListeners).toHaveLength(1);
251+
252+
const eventObject = loadListeners[0][1] as EventListenerObject;
253+
eventObject.handleEvent(new Event("load"));
254+
expect(oldHandler).not.toHaveBeenCalled();
255+
expect(newHandler).toHaveBeenCalledTimes(1);
256+
257+
sandbox.onload = null;
258+
expect(removeEventListener).toHaveBeenCalledWith("load", eventObject);
259+
});
260+
261+
it("onxxx 从函数改为普通对象时会移除页面监听,只保存对象值", () => {
262+
const addEventListener = vi.spyOn(global, "addEventListener");
263+
const removeEventListener = vi.spyOn(global, "removeEventListener");
264+
const sandbox = createProxyContext(createTestContext([]));
265+
const handler = vi.fn();
266+
const listenerObject = { handleEvent: vi.fn() };
267+
268+
sandbox.onload = handler;
269+
const eventObject = addEventListener.mock.calls.find(([name]) => name === "load")?.[1] as EventListenerObject;
270+
271+
//@ts-ignore
272+
sandbox.onload = listenerObject;
273+
274+
expect(handler).not.toHaveBeenCalled();
275+
expect(removeEventListener).toHaveBeenCalledWith("load", eventObject);
276+
expect(sandbox.onload).toBe(listenerObject);
277+
});
278+
});

0 commit comments

Comments
 (0)