|
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 | + ); |
3 | 33 |
|
4 | 34 | describe.concurrent("shouldFnBind", () => { |
5 | 35 | it.concurrent("不处理非原生函数", () => { |
@@ -46,3 +76,203 @@ describe.concurrent("shouldFnBind", () => { |
46 | 76 | expect(shouldFnBind(o.setTimeoutForTest2)).toBe(true); |
47 | 77 | }); |
48 | 78 | }); |
| 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