test
Some checks failed
test / Run tests (push) Failing after 6s
build / Build (push) Failing after 9s

This commit is contained in:
王一之 2025-03-19 18:05:54 +08:00
parent c2219db73e
commit fd2aba4286
31 changed files with 584 additions and 199 deletions

View File

@ -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",

View File

@ -0,0 +1,3 @@
# mock一个chrome扩展环境
> 只针对自己的项目做了一些简单的封装,如果有需要可以自己修改

View File

@ -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?.();
}
}

View File

@ -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<void>((resolve) => {
resolve();
});
}
}

View File

@ -0,0 +1,5 @@
export default class Downloads {
download(_: any, callback: Function) {
callback && callback();
}
}

View File

@ -0,0 +1,9 @@
export default class I18n {
getUILanguage() {
return "zh-CN";
}
getAcceptLanguages(callback: (lngs: string[]) => void) {
callback(["zh-CN"]);
}
}

View File

@ -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;

View File

@ -0,0 +1,64 @@
export default class Notifications {
notification: Map<string, boolean> = 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);
}
}

View File

@ -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}`;
}
}

View File

@ -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();
}
}

View File

@ -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);
},
};
}

View File

@ -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
},
};
}

View File

@ -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<MessageConnect> {
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<any> {
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);
});
}
}

19
pnpm-lock.yaml generated
View File

@ -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: {}

View File

@ -9,7 +9,7 @@ export interface CacheStorage {
export class ExtCache implements CacheStorage {
get(key: string): Promise<any> {
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<void> {
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<boolean> {
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<void> {
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<string[]> {
return new Promise((resolve) => {
chrome.storage.local.get(null, (value) => {
chrome.storage.session.get(null, (value) => {
resolve(Object.keys(value));
});
});

View File

@ -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<Test> {
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);
});
});

View File

@ -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) {

View File

@ -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 {

View File

@ -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" });

View File

@ -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: ["<all_urls>"],
// types: ["xmlhttprequest"],
// },
// ["responseHeaders", "extraHeaders"]
// );
}
}

View File

@ -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
) {}

View File

@ -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 每星期运行一次"));
});
});

View File

@ -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;
}

View File

@ -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();

View File

@ -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);
});
});

View File

@ -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();

View File

@ -1,2 +0,0 @@
// <root>/__mocks__/webextension-polyfill.ts
export { fakeBrowser as default } from "@webext-core/fake-browser";

55
tests/utils.test.ts Normal file
View File

@ -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();
});
});

View File

@ -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() {}

View File

@ -1,3 +1,3 @@
import { vi } from "vitest";
import chromeMock from "@Packages/chrome-extension-mock";
vi.mock("webextension-polyfill");
chromeMock.init();

View File

@ -10,6 +10,7 @@ export default defineConfig({
},
},
test: {
environment: "jsdom",
// List setup file
setupFiles: ["./tests/vitest.setup.ts"],
},