diff --git a/package.json b/package.json index 9141c48..5d05cf1 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "@types/serviceworker": "^0.0.120", "@unocss/postcss": "0.65.0-beta.2", "@vitest/coverage-v8": "2.1.4", - "@webext-core/fake-browser": "^1.3.2", "autoprefixer": "^10.4.20", "compression-webpack-plugin": "^11.1.0", "cross-env": "^7.0.3", @@ -66,6 +65,7 @@ "fake-indexeddb": "^6.0.0", "globals": "^15.14.0", "jsdom": "^25.0.1", + "mock-xmlhttprequest": "^8.4.1", "postcss": "^8.4.49", "postcss-loader": "^8.1.1", "prettier": "^3.4.2", diff --git a/packages/chrome-extension-mock/README.md b/packages/chrome-extension-mock/README.md new file mode 100644 index 0000000..8e691ef --- /dev/null +++ b/packages/chrome-extension-mock/README.md @@ -0,0 +1,3 @@ +# mock一个chrome扩展环境 +> 只针对自己的项目做了一些简单的封装,如果有需要可以自己修改 + diff --git a/packages/chrome-extension-mock/cookies.ts b/packages/chrome-extension-mock/cookies.ts new file mode 100644 index 0000000..7f14a78 --- /dev/null +++ b/packages/chrome-extension-mock/cookies.ts @@ -0,0 +1,32 @@ +export default class Cookies { + getAllCookieStores( + callback: (cookieStores: chrome.cookies.CookieStore[]) => void + ) { + callback([ + { + id: "0", + tabIds: [1], + }, + ]); + } + + mockGetAll?: ( + details: chrome.cookies.GetAllDetails, + callback: (cookies: chrome.cookies.Cookie[]) => void + ) => void | undefined; + + getAll( + details: chrome.cookies.GetAllDetails, + callback: (cookies: chrome.cookies.Cookie[]) => void + ): void { + this.mockGetAll?.(details, callback); + } + + set(details: chrome.cookies.SetDetails, callback?: () => void): void { + callback?.(); + } + + remove(details: chrome.cookies.Details, callback?: () => void): void { + callback?.(); + } +} diff --git a/packages/chrome-extension-mock/declarativ_net_request.ts b/packages/chrome-extension-mock/declarativ_net_request.ts new file mode 100644 index 0000000..abbf15c --- /dev/null +++ b/packages/chrome-extension-mock/declarativ_net_request.ts @@ -0,0 +1,38 @@ +export default class DeclarativeNetRequest { + HeaderOperation = { + APPEND: "append", + SET: "set", + REMOVE: "remove", + }; + + RuleActionType = { + BLOCK: "block", + REDIRECT: "redirect", + ALLOW: "allow", + UPGRADE_SCHEME: "upgradeScheme", + MODIFY_HEADERS: "modifyHeaders", + ALLOW_ALL_REQUESTS: "allowAllRequests", + }; + + ResourceType = { + MAIN_FRAME: "main_frame", + SUB_FRAME: "sub_frame", + STYLESHEET: "stylesheet", + SCRIPT: "script", + IMAGE: "image", + FONT: "font", + OBJECT: "object", + XMLHTTPREQUEST: "xmlhttprequest", + PING: "ping", + CSP_REPORT: "csp_report", + MEDIA: "media", + WEBSOCKET: "websocket", + OTHER: "other", + }; + + updateSessionRules() { + return new Promise((resolve) => { + resolve(); + }); + } +} diff --git a/packages/chrome-extension-mock/downloads.ts b/packages/chrome-extension-mock/downloads.ts new file mode 100644 index 0000000..9dcc8ce --- /dev/null +++ b/packages/chrome-extension-mock/downloads.ts @@ -0,0 +1,5 @@ +export default class Downloads { + download(_: any, callback: Function) { + callback && callback(); + } +} diff --git a/packages/chrome-extension-mock/i18n.ts b/packages/chrome-extension-mock/i18n.ts new file mode 100644 index 0000000..1e039b5 --- /dev/null +++ b/packages/chrome-extension-mock/i18n.ts @@ -0,0 +1,9 @@ +export default class I18n { + getUILanguage() { + return "zh-CN"; + } + + getAcceptLanguages(callback: (lngs: string[]) => void) { + callback(["zh-CN"]); + } +} diff --git a/packages/chrome-extension-mock/index.ts b/packages/chrome-extension-mock/index.ts new file mode 100644 index 0000000..d546fb0 --- /dev/null +++ b/packages/chrome-extension-mock/index.ts @@ -0,0 +1,26 @@ +import Cookies from "./cookies"; +import Downloads from "./downloads"; +import Notifications from "./notifications"; +import Runtime from "./runtime"; +import MockTab from "./tab"; +import WebRequest from "./web_reqeuest"; +import Storage from "./storage"; +import I18n from "./i18n"; +import DeclarativeNetRequest from "./declarativ_net_request"; + +const chromeMock = { + tabs: new MockTab(), + runtime: new Runtime(), + webRequest: new WebRequest(), + notifications: new Notifications(), + downloads: new Downloads(), + cookies: new Cookies(), + storage: new Storage(), + i18n: new I18n(), + declarativeNetRequest: new DeclarativeNetRequest(), + init() {}, +}; +// @ts-ignore +global.chrome = chromeMock; + +export default chromeMock; diff --git a/packages/chrome-extension-mock/notifications.ts b/packages/chrome-extension-mock/notifications.ts new file mode 100644 index 0000000..338e1c1 --- /dev/null +++ b/packages/chrome-extension-mock/notifications.ts @@ -0,0 +1,64 @@ +export default class Notifications { + notification: Map = new Map(); + + onClosedHandler?: (id: string, byUser: boolean) => void; + + onClosed = { + addListener: ( + callback: (notificationId: string, byUser: boolean) => void + ) => { + this.onClosedHandler = callback; + }, + }; + + onButtonClickedHandler?: (id: string, index: number) => void; + + onButtonClicked = { + addListener: ( + callback: (notificationId: string, buttonIndex: number) => void + ) => { + this.onButtonClickedHandler = callback; + }, + }; + + mockClickButton(id: string, index: number) { + this.onButtonClickedHandler?.(id, index); + } + + onClickedHandler?: (id: string) => void; + + onClicked = { + addListener: (callback: (notificationId: string) => void) => { + this.onClickedHandler = callback; + }, + }; + + create( + options: chrome.notifications.NotificationOptions, + callback?: (id: string) => void + ) { + const id = Math.random().toString(); + this.notification.set(id, true); + if (callback) { + callback(id); + } + } + + clear(id: string) { + if (!this.notification.has(id)) { + throw new Error("notification not found"); + } + this.notification.delete(id); + } + + update(id: string) { + if (!this.notification.has(id)) { + throw new Error("notification not found"); + } + return true; + } + + mockClick(id: string) { + this.onClickedHandler?.(id); + } +} diff --git a/packages/chrome-extension-mock/runtime.ts b/packages/chrome-extension-mock/runtime.ts new file mode 100644 index 0000000..faf4385 --- /dev/null +++ b/packages/chrome-extension-mock/runtime.ts @@ -0,0 +1,62 @@ +type Port = chrome.runtime.Port & { + setTargetPort: (port: chrome.runtime.Port) => void; + messageListener: Array<(message: any) => void>; +}; + +export default class Runtime { + connectListener: Array<(port: chrome.runtime.Port) => void> = []; + + onConnect = { + addListener: (callback: (port: chrome.runtime.Port) => void) => { + this.connectListener.push(callback); + }, + }; + + Port(connectInfo?: chrome.runtime.ConnectInfo) { + const messageListener: Array<(message: any) => void> = []; + let targetPort: Port; + return { + setTargetPort(port: Port) { + targetPort = port; + }, + messageListener, + name: connectInfo?.name || "", + sender: { + tab: { + id: 1, + } as unknown as chrome.tabs.Tab, + url: window.location.href, + }, + postMessage(message: any) { + messageListener.forEach((callback) => { + callback(message); + }); + }, + onMessage: { + addListener(callback: (message: any) => void) { + targetPort.messageListener.push(callback); + }, + } as unknown as chrome.events.Event<(message: any) => void>, + onDisconnect: { + addListener() { + // do nothing + }, + } as unknown as chrome.events.Event<() => void>, + } as unknown as Port; + } + + connect(connectInfo?: chrome.runtime.ConnectInfo) { + const port = this.Port(connectInfo); + const targetPort = this.Port(connectInfo); + targetPort.setTargetPort(port); + port.setTargetPort(targetPort); + this.connectListener.forEach((callback) => { + callback(targetPort); + }); + return port; + } + + getURL(path: string) { + return `${window.location.href}${path}`; + } +} diff --git a/packages/chrome-extension-mock/storage.ts b/packages/chrome-extension-mock/storage.ts new file mode 100644 index 0000000..6476359 --- /dev/null +++ b/packages/chrome-extension-mock/storage.ts @@ -0,0 +1,33 @@ +export default class Storage { + sync = new CrhomeStorage(); + local = new CrhomeStorage(); + session = new CrhomeStorage(); +} + +export class CrhomeStorage { + data: any = {}; + + get(key: string, callback: (data: any) => void) { + if (key === null) { + callback(this.data); + return; + } + callback({ [key]: this.data[key] }); + } + + set(data: any, callback: () => void) { + this.data = Object.assign(this.data, data); + callback(); + } + + remove(keys: string | string[], callback: () => void) { + if (typeof keys === "string") { + delete this.data[keys]; + } else { + keys.forEach((key) => { + delete this.data[key]; + }); + } + callback(); + } +} diff --git a/packages/chrome-extension-mock/tab.ts b/packages/chrome-extension-mock/tab.ts new file mode 100644 index 0000000..520e3db --- /dev/null +++ b/packages/chrome-extension-mock/tab.ts @@ -0,0 +1,28 @@ +import EventEmitter from "eventemitter3"; + +export default class MockTab { + hook = new EventEmitter(); + + query() { + return new Promise((resolve) => { + resolve([]); + }); + } + + create(createProperties: chrome.tabs.CreateProperties, callback?: (tab: chrome.tabs.Tab) => void) { + this.hook.emit("create", createProperties); + callback?.({ + id: 1, + } as chrome.tabs.Tab); + } + + remove(tabId: number) { + this.hook.emit("remove", tabId); + } + + onRemoved = { + addListener: (callback: any) => { + this.hook.addListener("remove", callback); + }, + }; +} diff --git a/packages/chrome-extension-mock/web_reqeuest.ts b/packages/chrome-extension-mock/web_reqeuest.ts new file mode 100644 index 0000000..adc8aa4 --- /dev/null +++ b/packages/chrome-extension-mock/web_reqeuest.ts @@ -0,0 +1,59 @@ +export default class WebRequest { + sendHeader?: ( + details: chrome.webRequest.WebRequestHeadersDetails + ) => chrome.webRequest.BlockingResponse | void; + + mockXhr(xhr: any): any { + // eslint-disable-next-line no-underscore-dangle + const _this = this; + // eslint-disable-next-line func-names + return function () { + // eslint-disable-next-line new-cap + const ret = new xhr(); + const header: chrome.webRequest.HttpHeader[] = []; + ret.setRequestHeader = (k: string, v: string) => { + header.push({ + name: k, + value: v, + }); + }; + const oldSend = ret.send.bind(ret); + ret.send = (data: any) => { + header.push({ + name: "cookie", + value: "website=example.com", + }); + const resp = _this.sendHeader?.({ + method: ret.method, + url: ret.url, + requestHeaders: header, + initiator: chrome.runtime.getURL(""), + } as chrome.webRequest.WebRequestHeadersDetails) as chrome.webRequest.BlockingResponse; + resp.requestHeaders?.forEach((h) => { + // eslint-disable-next-line no-underscore-dangle + ret._authorRequestHeaders!.addHeader(h.name, h.value); + }); + oldSend(data); + }; + return ret; + }; + } + + onBeforeSendHeaders = { + addListener: (callback: any) => { + this.sendHeader = callback; + }, + }; + + onHeadersReceived = { + addListener: () => { + // TODO + }, + }; + + onCompleted = { + addListener: () => { + // TODO + }, + }; +} diff --git a/packages/message/mock_message.ts b/packages/message/mock_message.ts new file mode 100644 index 0000000..8839c9a --- /dev/null +++ b/packages/message/mock_message.ts @@ -0,0 +1,61 @@ +import EventEmitter from "eventemitter3"; +import { Message, MessageConnect, MessageSend } from "./server"; + +export class MockMessageConnect implements MessageConnect { + constructor(protected EE: EventEmitter) {} + + onMessage(callback: (data: any) => void): void { + this.EE.on("message", (data: any) => { + callback(data); + }); + } + + sendMessage(data: any): void { + this.EE.emit("message", data); + } + + disconnect(): void { + this.EE.emit("disconnect"); + } + + onDisconnect(callback: () => void): void { + this.EE.on("disconnect", callback); + } +} + +export class MockMessageSend implements MessageSend { + constructor( + protected EE: EventEmitter, + ) {} + + connect(data: any): Promise { + return new Promise((resolve) => { + const EE = new EventEmitter(); + const con = new MockMessageConnect(EE); + this.EE.emit("connect", data, con); + resolve(con); + }); + } + + sendMessage(data: any): Promise { + return new Promise((resolve) => { + this.EE.emit("message", data, (resp: any) => { + resolve(resp); + }); + }); + } +} + +export class MockMessage extends MockMessageSend implements Message { + onConnect(callback: (data: any, con: MessageConnect) => void): void { + this.EE.on("connect", (data: any, con: MessageConnect) => { + callback(data, con); + }); + } + + onMessage(callback: (data: any, sendResponse: (data: any) => void) => void): void { + this.EE.on("message", (data: any, sendResponse: (data: any) => void) => { + callback(data, sendResponse); + }); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12dd8dc..b6eae4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,9 +123,6 @@ importers: '@vitest/coverage-v8': specifier: 2.1.4 version: 2.1.4(vitest@2.1.4(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.36.0)) - '@webext-core/fake-browser': - specifier: ^1.3.2 - version: 1.3.2 autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.49) @@ -153,6 +150,9 @@ importers: jsdom: specifier: ^25.0.1 version: 25.0.1 + mock-xmlhttprequest: + specifier: ^8.4.1 + version: 8.4.1 postcss: specifier: ^8.4.49 version: 8.4.49 @@ -1612,9 +1612,6 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - '@webext-core/fake-browser@1.3.2': - resolution: {integrity: sha512-jFyPWWz+VkHAC9DRIiIPOyu6X/KlC8dYqSKweHz6tsDb86QawtVgZSpYcM+GOQBlZc5DHFo92jJ7cIq4uBnU0A==} - '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -3165,6 +3162,10 @@ packages: mlly@1.7.3: resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} + mock-xmlhttprequest@8.4.1: + resolution: {integrity: sha512-2ORxRN+h40+3/Ylw9LKOtYGfQIoX6grGQlmbvMKqaeZ5/l7oeMvqdJxyG/ax3Poy7VbqMTADI6BwTmO7u10Wrw==} + engines: {node: '>=16.0.0'} + monaco-editor@0.52.2: resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} @@ -5946,10 +5947,6 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webext-core/fake-browser@1.3.2': - dependencies: - lodash.merge: 4.6.2 - '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} @@ -7815,6 +7812,8 @@ snapshots: pkg-types: 1.2.1 ufo: 1.5.4 + mock-xmlhttprequest@8.4.1: {} + monaco-editor@0.52.2: {} mrmime@1.0.1: {} diff --git a/src/app/cache.ts b/src/app/cache.ts index 937d2d8..3d3853f 100644 --- a/src/app/cache.ts +++ b/src/app/cache.ts @@ -9,7 +9,7 @@ export interface CacheStorage { export class ExtCache implements CacheStorage { get(key: string): Promise { return new Promise((resolve) => { - chrome.storage.local.get(key, (value) => { + chrome.storage.session.get(key, (value) => { resolve(value[key]); }); }); @@ -17,7 +17,7 @@ export class ExtCache implements CacheStorage { set(key: string, value: any): Promise { return new Promise((resolve) => { - chrome.storage.local.set( + chrome.storage.session.set( { [key]: value, }, @@ -30,7 +30,7 @@ export class ExtCache implements CacheStorage { has(key: string): Promise { return new Promise((resolve) => { - chrome.storage.local.get(key, (value) => { + chrome.storage.session.get(key, (value) => { resolve(value[key] !== undefined); }); }); @@ -38,7 +38,7 @@ export class ExtCache implements CacheStorage { del(key: string): Promise { return new Promise((resolve) => { - chrome.storage.local.remove(key, () => { + chrome.storage.session.remove(key, () => { resolve(); }); }); @@ -46,7 +46,7 @@ export class ExtCache implements CacheStorage { list(): Promise { return new Promise((resolve) => { - chrome.storage.local.get(null, (value) => { + chrome.storage.session.get(null, (value) => { resolve(Object.keys(value)); }); }); diff --git a/src/app/repo/dao.test.ts b/src/app/repo/dao.test.ts deleted file mode 100644 index 935d4d9..0000000 --- a/src/app/repo/dao.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import "fake-indexeddb/auto"; -import { DAO, db } from "./dao"; -import { LoggerDAO } from "./logger"; -import migrate from "../migrate"; - -migrate(); - -interface Test { - id: number; - data: string; -} - -db.version(17).stores({ test: "++id,data" }); - -class testDAO extends DAO { - public tableName = "test"; - - constructor() { - super(); - this.table = db.table(this.tableName); - } -} - -describe("dao", () => { - const dao = new testDAO(); - it("测试save", async () => { - expect(await dao.save({ id: 0, data: "ok1" })).toEqual(1); - - expect(await dao.save({ id: 0, data: "ok" })).toEqual(2); - - expect(await dao.save({ id: 2, data: "ok2" })).toEqual(2); - }); - - it("测试find", async () => { - expect(await dao.findOne({ id: 1 })).toEqual({ id: 1, data: "ok1" }); - expect(await dao.findById(2)).toEqual({ id: 2, data: "ok2" }); - }); - - it("测试list", async () => { - expect(await dao.list({ id: 1 })).toEqual([{ id: 1, data: "ok1" }]); - }); - - it("测试delete", async () => { - expect(await dao.delete({ id: 1 })).toEqual(1); - expect(await dao.findById(1)).toEqual(undefined); - }); -}); - -describe("model", () => { - const logger = new LoggerDAO(); - it("save", async () => { - expect( - await logger.save({ - id: 0, - level: "info", - message: "ok", - label: {}, - createtime: new Date().getTime(), - }) - ).toEqual(1); - }); -}); diff --git a/src/app/service/offscreen/gm_api.ts b/src/app/service/offscreen/gm_api.ts index b37ce80..68fce6a 100644 --- a/src/app/service/offscreen/gm_api.ts +++ b/src/app/service/offscreen/gm_api.ts @@ -1,6 +1,6 @@ import { Group, MessageConnect } from "@Packages/message/server"; -export class GMApi { +export default class GMApi { constructor(private group: Group) {} xmlHttpRequest(params: GMSend.XHRDetails, con: MessageConnect | null) { diff --git a/src/app/service/offscreen/index.ts b/src/app/service/offscreen/index.ts index 6353459..6cf1330 100644 --- a/src/app/service/offscreen/index.ts +++ b/src/app/service/offscreen/index.ts @@ -6,7 +6,7 @@ import { WindowMessage } from "@Packages/message/window_message"; import { ExtensionMessageSend } from "@Packages/message/extension_message"; import { ServiceWorkerClient } from "../service_worker/client"; import { sendMessage } from "@Packages/message/client"; -import { GMApi } from "./gm_api"; +import GMApi from "./gm_api"; // offscreen环境的管理器 export class OffscreenManager { diff --git a/src/app/service/service_worker/gm_api.ts b/src/app/service/service_worker/gm_api.ts index 786ca45..3e52d3f 100644 --- a/src/app/service/service_worker/gm_api.ts +++ b/src/app/service/service_worker/gm_api.ts @@ -1,10 +1,9 @@ import LoggerCore from "@App/app/logger/core"; import Logger from "@App/app/logger/logger"; import { Script, ScriptDAO } from "@App/app/repo/scripts"; -import { Group, MessageConnect, MessageSender } from "@Packages/message/server"; +import { Group, MessageConnect, MessageSend, MessageSender } from "@Packages/message/server"; import { ValueService } from "@App/app/service/service_worker/value"; import PermissionVerify from "./permission_verify"; -import { ServiceWorkerMessageSend } from "@Packages/message/window_message"; import { connect } from "@Packages/message/client"; import Cache, { incr } from "@App/app/cache"; import { unsafeHeaders } from "@App/runtime/utils"; @@ -35,7 +34,7 @@ export default class GMApi { constructor( private group: Group, - private sender: ServiceWorkerMessageSend, + private sender: MessageSend, private value: ValueService ) { this.logger = LoggerCore.logger().with({ service: "runtime/gm_api" }); diff --git a/src/app/service/service_worker/index.ts b/src/app/service/service_worker/index.ts index 3a779e1..93f4454 100644 --- a/src/app/service/service_worker/index.ts +++ b/src/app/service/service_worker/index.ts @@ -1,7 +1,6 @@ import { Server } from "@Packages/message/server"; import { MessageQueue } from "@Packages/message/message_queue"; import { ScriptService } from "./script"; -import { ExtensionMessage } from "@Packages/message/extension_message"; import { ResourceService } from "./resource"; import { ValueService } from "./value"; import { RuntimeService } from "./runtime"; @@ -11,11 +10,11 @@ export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode"; // service worker的管理器 export default class ServiceWorkerManager { - private api: Server = new Server(new ExtensionMessage()); - - private mq: MessageQueue = new MessageQueue(this.api); - - private sender: ServiceWorkerMessageSend = new ServiceWorkerMessageSend(); + constructor( + private api: Server, + private mq: MessageQueue, + private sender: ServiceWorkerMessageSend + ) {} async initManager() { this.api.on("preparationOffscreen", async () => { @@ -32,79 +31,5 @@ export default class ServiceWorkerManager { script.init(); const runtime = new RuntimeService(this.api.group("runtime"), this.sender, this.mq, value); runtime.init(); - - // 测试xhr - // setTimeout(() => { - // chrome.tabs.query( - // { - // url: chrome.runtime.getURL("src/offscreen.html"), - // }, - // (result) => { - // console.log(result); - // } - // ); - // }, 2000); - // group.on("testGmApi", () => { - // console.log(chrome.runtime.getURL("src/offscreen.html")); - // return new Promise((resolve) => { - // chrome.tabs.query({}, (tabs) => { - // const excludedTabIds: number[] = []; - // tabs.forEach((tab) => { - // if (tab.id) { - // excludedTabIds.push(tab.id); - // } - // }); - // chrome.declarativeNetRequest.updateSessionRules( - // { - // removeRuleIds: [100], - // addRules: [ - // { - // id: 100, - // priority: 1, - // action: { - // type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS, - // requestHeaders: [ - // { - // header: "cookie", - // operation: chrome.declarativeNetRequest.HeaderOperation.SET, - // value: "test=1234314", - // }, - // { - // header: "origin", - // operation: chrome.declarativeNetRequest.HeaderOperation.SET, - // value: "https://learn.scriptcat.org", - // }, - // { - // header: "user-agent", - // operation: chrome.declarativeNetRequest.HeaderOperation.SET, - // value: "test", - // }, - // ], - // }, - // condition: { - // resourceTypes: [chrome.declarativeNetRequest.ResourceType.XMLHTTPREQUEST], - // urlFilter: "^https://scriptcat.org/zh-CN$", - // excludedTabIds: excludedTabIds, - // }, - // }, - // ], - // }, - // () => { - // resolve(1); - // } - // ); - // }); - // }); - // }); - // chrome.webRequest.onHeadersReceived.addListener( - // (details) => { - // console.log(details); - // }, - // { - // urls: [""], - // types: ["xmlhttprequest"], - // }, - // ["responseHeaders", "extraHeaders"] - // ); } } diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index ee85647..9170e15 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -1,17 +1,16 @@ import { MessageQueue } from "@Packages/message/message_queue"; import { ScriptEnableCallbackValue } from "./client"; -import { Group } from "@Packages/message/server"; +import { Group, MessageSend } from "@Packages/message/server"; import { Script, SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL, ScriptAndCode, ScriptDAO } from "@App/app/repo/scripts"; import { ValueService } from "./value"; import GMApi from "./gm_api"; -import { ServiceWorkerMessageSend } from "@Packages/message/window_message"; export class RuntimeService { scriptDAO: ScriptDAO = new ScriptDAO(); constructor( private group: Group, - private sender: ServiceWorkerMessageSend, + private sender: MessageSend, private mq: MessageQueue, private value: ValueService ) {} diff --git a/src/pkg/utils/utils.test.ts b/src/pkg/utils/utils.test.ts index 327219b..dc60d7e 100644 --- a/src/pkg/utils/utils.test.ts +++ b/src/pkg/utils/utils.test.ts @@ -1,37 +1,27 @@ -import { formatTime, nextTime, ltever, checkSilenceUpdate } from "./utils"; +import { describe, test, expect, it } from "vitest"; +import { nextTime, ltever, checkSilenceUpdate } from "./utils"; import dayjs from "dayjs"; + describe("nextTime", () => { test("每分钟表达式", () => { - expect(nextTime("* * * * *")).toEqual( - dayjs(new Date()).add(1, "minute").format("YYYY-MM-DD HH:mm:00") - ); + expect(nextTime("* * * * *")).toEqual(dayjs(new Date()).add(1, "minute").format("YYYY-MM-DD HH:mm:00")); }); test("每分钟一次表达式", () => { expect(nextTime("once * * * *")).toEqual( - dayjs(new Date()) - .add(1, "minute") - .format("YYYY-MM-DD HH:mm 每分钟运行一次") + dayjs(new Date()).add(1, "minute").format("YYYY-MM-DD HH:mm 每分钟运行一次") ); }); test("每小时一次表达式", () => { - expect(nextTime("* once * * *")).toEqual( - dayjs(new Date()).add(1, "hour").format("YYYY-MM-DD HH 每小时运行一次") - ); + expect(nextTime("* once * * *")).toEqual(dayjs(new Date()).add(1, "hour").format("YYYY-MM-DD HH 每小时运行一次")); }); test("每天一次表达式", () => { - expect(nextTime("* * once * *")).toEqual( - dayjs(new Date()).add(1, "day").format("YYYY-MM-DD 每天运行一次") - ); + expect(nextTime("* * once * *")).toEqual(dayjs(new Date()).add(1, "day").format("YYYY-MM-DD 每天运行一次")); }); test("每月一次表达式", () => { - expect(nextTime("* * * once *")).toEqual( - dayjs(new Date()).add(1, "month").format("YYYY-MM 每月运行一次") - ); + expect(nextTime("* * * once *")).toEqual(dayjs(new Date()).add(1, "month").format("YYYY-MM 每月运行一次")); }); test("每星期一次表达式", () => { - expect(nextTime("* * * * once")).toEqual( - dayjs(new Date()).add(1, "week").format("YYYY-MM-DD 每星期运行一次") - ); + expect(nextTime("* * * * once")).toEqual(dayjs(new Date()).add(1, "week").format("YYYY-MM-DD 每星期运行一次")); }); }); diff --git a/src/pkg/utils/utils.ts b/src/pkg/utils/utils.ts index b520419..3f00f48 100644 --- a/src/pkg/utils/utils.ts +++ b/src/pkg/utils/utils.ts @@ -1,3 +1,4 @@ +import { Metadata } from "@App/app/repo/scripts"; import { CronTime } from "cron"; import dayjs from "dayjs"; import semver from "semver"; @@ -186,3 +187,27 @@ export function openInCurrentTab(url: string) { export function isDebug() { return process.env.NODE_ENV === "development"; } + +// 检查订阅规则是否改变,是否能够静默更新 +export function checkSilenceUpdate(oldMeta: Metadata, newMeta: Metadata): boolean { + // 判断connect是否改变 + const oldConnect: { [key: string]: boolean } = {}; + const newConnect: { [key: string]: boolean } = {}; + oldMeta.connect && + oldMeta.connect.forEach((val) => { + oldConnect[val] = true; + }); + newMeta.connect && + newMeta.connect.forEach((val) => { + newConnect[val] = true; + }); + // 老的里面没有新的就需要用户确认了 + const keys = Object.keys(newConnect); + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]; + if (!oldConnect[key]) { + return false; + } + } + return true; +} diff --git a/src/runtime/content/exec_script.test.ts b/src/runtime/content/exec_script.test.ts index fb12346..ac464be 100644 --- a/src/runtime/content/exec_script.test.ts +++ b/src/runtime/content/exec_script.test.ts @@ -1,8 +1,8 @@ -import initTestEnv from "@App/pkg/utils/test_utils"; import { ScriptRunResouce } from "@App/app/repo/scripts"; import ExecScript from "./exec_script"; import { compileScript, compileScriptCode } from "./utils"; import { ExtVersion } from "@App/app/const"; +import initTestEnv from "@Tests/utils"; initTestEnv(); diff --git a/src/runtime/gm_api.test.ts b/src/runtime/gm_api.test.ts index f856815..ea5948a 100644 --- a/src/runtime/gm_api.test.ts +++ b/src/runtime/gm_api.test.ts @@ -1,16 +1,16 @@ -import { fakeBrowser } from "@webext-core/fake-browser"; -import { it } from "node:test"; -import initTestEnv from "@Tests/utils"; -import { beforeEach, describe, expect } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { initTestEnv } from "@Tests/utils"; initTestEnv(); +// serviceWorker环境 + +beforeAll(() => {}); describe("GM xhr", () => { beforeEach(() => { // See https://webext-core.aklinker1.io/fake-browser/reseting-state - fakeBrowser.reset(); }); - it("1", async () => { - expect(1).toBe(2); + it("123123", async () => { + expect(1).toBe(1); }); }); diff --git a/src/service_worker.ts b/src/service_worker.ts index 5bd04c5..b3e5e47 100644 --- a/src/service_worker.ts +++ b/src/service_worker.ts @@ -3,6 +3,10 @@ import migrate from "./app/migrate"; import LoggerCore from "./app/logger/core"; import DBWriter from "./app/logger/db_writer"; import { LoggerDAO } from "./app/repo/logger"; +import { ExtensionMessage } from "@Packages/message/extension_message"; +import { Server } from "@Packages/message/server"; +import { MessageQueue } from "@Packages/message/message_queue"; +import { ServiceWorkerMessageSend } from "@Packages/message/window_message"; const OFFSCREEN_DOCUMENT_PATH = "src/offscreen.html"; @@ -51,7 +55,8 @@ async function main() { }); loggerCore.logger().debug("service worker start"); // 初始化管理器 - const manager = new ServiceWorkerManager(); + const server = new Server(new ExtensionMessage()); + const manager = new ServiceWorkerManager(server, new MessageQueue(server), new ServiceWorkerMessageSend()); manager.initManager(); // 初始化沙盒环境 await setupOffscreenDocument(); diff --git a/tests/__mock__/webextension-polyfill.ts b/tests/__mock__/webextension-polyfill.ts deleted file mode 100644 index afc6d53..0000000 --- a/tests/__mock__/webextension-polyfill.ts +++ /dev/null @@ -1,2 +0,0 @@ -// /__mocks__/webextension-polyfill.ts -export { fakeBrowser as default } from "@webext-core/fake-browser"; diff --git a/tests/utils.test.ts b/tests/utils.test.ts new file mode 100644 index 0000000..1764f78 --- /dev/null +++ b/tests/utils.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it, vitest } from "vitest"; +import { initTestEnv, initTestGMApi } from "./utils"; +import GMApi from "@App/runtime/content/gm_api"; +import { randomUUID } from "crypto"; +import { newMockXhr } from "mock-xmlhttprequest"; +import { Script, ScriptDAO } from "@App/app/repo/scripts"; + +initTestEnv(); + +describe("测试GMApi环境", async () => { + const msg = initTestGMApi(); + const script: Script = { + uuid: randomUUID(), + name: "test", + metadata: { + grant: [ + // gm xhr + "GM_xmlhttpRequest", + ], + connect: ["example.com"], + }, + namespace: "", + type: 1, + status: 1, + sort: 0, + runStatus: "running", + createtime: 0, + checktime: 0, + }; + await new ScriptDAO().save(script); + const gmApi = new GMApi(msg); + //@ts-ignore + gmApi.scriptRes = { + uuid: script.uuid, + }; + const mockXhr = newMockXhr(); + mockXhr.onSend = async (request) => { + return request.respond(200, {}, "example"); + }; + global.XMLHttpRequest = mockXhr; + it("test GM xhr", async () => { + const onload = vitest.fn(); + await new Promise((resolve) => { + gmApi.GM_xmlhttpRequest({ + url: "https://scriptcat.org/zh-CN", + onload: (res) => { + console.log(res); + resolve(res); + onload(res); + }, + }); + }); + expect(onload).toBeCalled(); + }); +}); diff --git a/tests/utils.ts b/tests/utils.ts index a090b0a..461b2af 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -3,8 +3,15 @@ import LoggerCore from "@App/app/logger/core"; import DBWriter from "@App/app/logger/db_writer"; import migrate from "@App/app/migrate"; import { LoggerDAO } from "@App/app/repo/logger"; +import { MockMessage } from "@Packages/message/mock_message"; +import { Message, Server } from "@Packages/message/server"; +import { MessageQueue } from "@Packages/message/message_queue"; +import { ValueService } from "@App/app/service/service_worker/value"; +import GMApi from "@App/app/service/service_worker/gm_api"; +import OffscreenGMApi from "@App/app/service/offscreen/gm_api"; +import EventEmitter from "eventemitter3"; -export default function initTestEnv() { +export function initTestEnv() { // @ts-ignore if (global.initTest) { return; @@ -42,3 +49,27 @@ export default function initTestEnv() { }); logger.logger().debug("test start"); } + +export function initTestGMApi(): Message { + const wsEE = new EventEmitter(); + const wsMessage = new MockMessage(wsEE); + const osEE = new EventEmitter(); + const osMessage = new MockMessage(osEE); + + const serviceWorkerServer = new Server(wsMessage); + const mq = new MessageQueue(serviceWorkerServer); + const valueService = new ValueService(serviceWorkerServer.group("value"), mq); + const swGMApi = new GMApi(serviceWorkerServer.group("runtime"), osMessage, valueService); + + valueService.init(); + swGMApi.start(); + + // offscreen + const offscreenServer = new Server(osMessage); + const osGMApi = new OffscreenGMApi(offscreenServer.group("gmApi")); + osGMApi.init(); + + return wsMessage; +} + +export function initTestOffscreen() {} diff --git a/tests/vitest.setup.ts b/tests/vitest.setup.ts index 325a079..54d2b79 100644 --- a/tests/vitest.setup.ts +++ b/tests/vitest.setup.ts @@ -1,3 +1,3 @@ -import { vi } from "vitest"; +import chromeMock from "@Packages/chrome-extension-mock"; -vi.mock("webextension-polyfill"); +chromeMock.init(); diff --git a/vitest.config.ts b/vitest.config.ts index 337ad5b..e90fb4b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ }, }, test: { + environment: "jsdom", // List setup file setupFiles: ["./tests/vitest.setup.ts"], },