单测
This commit is contained in:
parent
93d41169e1
commit
eeaf9e071e
@ -7,6 +7,8 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx -y only-allow pnpm",
|
||||
"test": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"build": "cross-env NODE_ENV=production rspack build",
|
||||
"dev": "cross-env NODE_ENV=development rspack",
|
||||
"format": "prettier --write .",
|
||||
@ -35,9 +37,11 @@
|
||||
"eslint-plugin-react": "^7.37.1",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"globals": "^15.11.0",
|
||||
"jsdom": "^25.0.1",
|
||||
"prettier": "^3.3.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.8.1"
|
||||
"typescript-eslint": "^8.8.1",
|
||||
"vitest": "^2.1.4"
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ export class ExtServer implements IServer {
|
||||
});
|
||||
}
|
||||
|
||||
onConnect(callback: (eventName: string, con: IConnect) => void) {
|
||||
onConnect(callback: (con: IConnect) => void) {
|
||||
this.EE.on("connect", callback);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import EventEmitter from "eventemitter3";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export interface IServer {
|
||||
onConnect: (callback: (eventName: string, con: IConnect) => void) => void;
|
||||
onConnect: (callback: (con: IConnect) => void) => void;
|
||||
}
|
||||
|
||||
export interface IConnect {
|
||||
@ -18,8 +18,8 @@ export class Server {
|
||||
|
||||
constructor(private connect: IServer) {
|
||||
this.EE = new EventEmitter();
|
||||
this.connect.onConnect((eventName, con) => {
|
||||
this.EE.emit(eventName, con);
|
||||
this.connect.onConnect((con) => {
|
||||
this.EE.emit("connection", con);
|
||||
});
|
||||
}
|
||||
|
||||
@ -31,20 +31,21 @@ export class Server {
|
||||
export class Connect {
|
||||
private EE: EventEmitter;
|
||||
|
||||
private con: IConnect;
|
||||
|
||||
constructor(
|
||||
private id: string | IConnect,
|
||||
private con: IConnect
|
||||
con?: IConnect
|
||||
) {
|
||||
this.EE = new EventEmitter();
|
||||
if (arguments.length === 1) {
|
||||
this.con = id as IConnect;
|
||||
this.con.onMessage((message) => {
|
||||
const data = message as { eventName: string; data: unknown[]; messageId: string };
|
||||
data.data.push(this.callbackFunc(data.messageId));
|
||||
this.EE.emit(data.eventName, ...data.data);
|
||||
this.messageHandler(message);
|
||||
});
|
||||
} else {
|
||||
// 子连接
|
||||
this.con = con!;
|
||||
this.con.onMessage((message) => {
|
||||
const data = message as { eventName: string; data: unknown; id: string };
|
||||
if (data.eventName === "subcon") {
|
||||
@ -60,13 +61,13 @@ export class Connect {
|
||||
}
|
||||
|
||||
private callbackFunc(msgId: string): (...data: unknown[]) => void {
|
||||
return function (...data: unknown[]) {
|
||||
return (...data: unknown[]) => {
|
||||
this.con.postMessage({ eventName: "callback", data, messageId: msgId });
|
||||
};
|
||||
}
|
||||
|
||||
private messageHandler(data: unknown) {
|
||||
const subData = data.data as { eventName: string; data: unknown[]; messageId: string };
|
||||
const subData = data as { eventName: string; data: unknown[]; messageId: string };
|
||||
subData.data.push(this.callbackFunc(subData.messageId));
|
||||
this.EE.emit(subData.eventName, ...subData.data);
|
||||
}
|
||||
|
42
packages/message/message.test.ts
Normal file
42
packages/message/message.test.ts
Normal file
@ -0,0 +1,42 @@
|
||||
// @vitest-environment jsdom
|
||||
import { expect, test, vi } from "vitest";
|
||||
import { Server, Connect } from ".";
|
||||
import { connect, WindowServer } from "./window";
|
||||
|
||||
test("server", async () => {
|
||||
const myFunc = vi.fn();
|
||||
const server = new Server(new WindowServer(global.window));
|
||||
server.on("connection", (con) => {
|
||||
myFunc();
|
||||
con.onMessage((message) => {
|
||||
myFunc(message);
|
||||
});
|
||||
});
|
||||
const client = connect(window, window);
|
||||
client.postMessage("hello");
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
expect(myFunc).toHaveBeenCalledTimes(2);
|
||||
expect(myFunc).toHaveBeenCalledWith("hello");
|
||||
});
|
||||
|
||||
test("connect", async () => {
|
||||
const myFunc = vi.fn();
|
||||
const server = new Server(new WindowServer(global.window));
|
||||
server.on("connection", (con) => {
|
||||
myFunc();
|
||||
const wrapCon = new Connect(con);
|
||||
wrapCon.on("hello", (message) => {
|
||||
myFunc(message);
|
||||
wrapCon.emit("world", "world");
|
||||
});
|
||||
});
|
||||
const client = new Connect(connect(window, window));
|
||||
client.on("world", (message) => {
|
||||
myFunc(message);
|
||||
});
|
||||
client.emit("hello", "hello");
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
expect(myFunc).toHaveBeenCalledTimes(3);
|
||||
expect(myFunc).toHaveBeenCalledWith("hello");
|
||||
expect(myFunc).toHaveBeenCalledWith("world");
|
||||
});
|
60
packages/message/window.ts
Normal file
60
packages/message/window.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import EventEmitter from "eventemitter3";
|
||||
import { IConnect, IServer } from ".";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export class WindowServer implements IServer {
|
||||
private EE: EventEmitter;
|
||||
|
||||
constructor(win: Window) {
|
||||
this.EE = new EventEmitter();
|
||||
win.addEventListener("message", (event) => {
|
||||
if (event.data.type === "connect") {
|
||||
this.EE.emit("connection", new WindowConnect(event.data.connectId, event.target as Window, win));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onConnect(callback: (con: IConnect) => void) {
|
||||
this.EE.on("connection", callback);
|
||||
}
|
||||
}
|
||||
|
||||
export function connect(source: Window, target: Window) {
|
||||
const con = new WindowConnect(uuidv4(), source, target);
|
||||
con.postMessage({ type: "connect" });
|
||||
return con;
|
||||
}
|
||||
|
||||
export class WindowConnect implements IConnect {
|
||||
private EE: EventEmitter;
|
||||
|
||||
constructor(
|
||||
private id: string,
|
||||
private source: Window,
|
||||
private target: Window
|
||||
) {
|
||||
this.EE = new EventEmitter();
|
||||
this.source.addEventListener("message", (event) => {
|
||||
if (event.data.id === id) {
|
||||
this.EE.emit("message", event.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
postMessage(message: unknown) {
|
||||
this.target.postMessage(message, "*");
|
||||
}
|
||||
|
||||
onMessage(callback: (message: unknown) => void) {
|
||||
this.EE.on("message", callback);
|
||||
}
|
||||
|
||||
onDisconnect(callback: () => void) {
|
||||
this.EE.on("disconnect", callback);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.EE.emit("disconnect");
|
||||
this.EE.removeAllListeners();
|
||||
}
|
||||
}
|
296
pnpm-lock.yaml
generated
296
pnpm-lock.yaml
generated
@ -66,6 +66,9 @@ importers:
|
||||
globals:
|
||||
specifier: ^15.11.0
|
||||
version: 15.11.0
|
||||
jsdom:
|
||||
specifier: ^25.0.1
|
||||
version: 25.0.1
|
||||
prettier:
|
||||
specifier: ^3.3.3
|
||||
version: 3.3.3
|
||||
@ -78,6 +81,9 @@ importers:
|
||||
typescript-eslint:
|
||||
specifier: ^8.8.1
|
||||
version: 8.11.0(eslint@9.13.0)(typescript@5.6.3)
|
||||
vitest:
|
||||
specifier: ^2.1.4
|
||||
version: 2.1.4(@types/node@22.8.1)(jsdom@25.0.1)
|
||||
|
||||
packages/message:
|
||||
dependencies:
|
||||
@ -868,6 +874,10 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
agent-base@7.1.1:
|
||||
resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ajv-formats@2.1.1:
|
||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||
peerDependencies:
|
||||
@ -953,6 +963,9 @@ packages:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -1041,6 +1054,10 @@ packages:
|
||||
colorette@2.0.19:
|
||||
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
commander@7.2.0:
|
||||
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
|
||||
engines: {node: '>= 10'}
|
||||
@ -1093,9 +1110,17 @@ packages:
|
||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
cssstyle@4.1.0:
|
||||
resolution: {integrity: sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
data-urls@5.0.0:
|
||||
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
data-view-buffer@1.0.1:
|
||||
resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -1128,6 +1153,9 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
decimal.js@10.4.3:
|
||||
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
|
||||
|
||||
deep-eql@5.0.2:
|
||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||
engines: {node: '>=6'}
|
||||
@ -1159,6 +1187,10 @@ packages:
|
||||
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
depd@1.1.2:
|
||||
resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -1209,6 +1241,10 @@ packages:
|
||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
es-abstract@1.23.3:
|
||||
resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -1409,6 +1445,10 @@ packages:
|
||||
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data@4.0.1:
|
||||
resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
forwarded@0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -1517,6 +1557,10 @@ packages:
|
||||
hpack.js@2.1.6:
|
||||
resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==}
|
||||
|
||||
html-encoding-sniffer@4.0.0:
|
||||
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
html-entities@2.5.2:
|
||||
resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==}
|
||||
|
||||
@ -1537,6 +1581,10 @@ packages:
|
||||
http-parser-js@0.5.8:
|
||||
resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
http-proxy-middleware@2.0.7:
|
||||
resolution: {integrity: sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@ -1550,6 +1598,10 @@ packages:
|
||||
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
https-proxy-agent@7.0.5:
|
||||
resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
human-signals@2.1.0:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||
engines: {node: '>=10.17.0'}
|
||||
@ -1565,6 +1617,10 @@ packages:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
ignore@5.3.2:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
@ -1687,6 +1743,9 @@ packages:
|
||||
resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
is-potential-custom-element-name@1.0.1:
|
||||
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||
|
||||
is-regex@1.1.4:
|
||||
resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -1753,6 +1812,15 @@ packages:
|
||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||
hasBin: true
|
||||
|
||||
jsdom@25.0.1:
|
||||
resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
canvas: ^2.11.2
|
||||
peerDependenciesMeta:
|
||||
canvas:
|
||||
optional: true
|
||||
|
||||
json-buffer@3.0.1:
|
||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||
|
||||
@ -1904,6 +1972,9 @@ packages:
|
||||
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
nwsapi@2.2.13:
|
||||
resolution: {integrity: sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -1982,6 +2053,9 @@ packages:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
parse5@7.2.1:
|
||||
resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -2159,6 +2233,9 @@ packages:
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
rrweb-cssom@0.7.1:
|
||||
resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==}
|
||||
|
||||
rsbuild-plugin-dts@0.0.16:
|
||||
resolution: {integrity: sha512-Yfc7h1cTTp55baLgS6DVa3mlLaFlNfoUVmcX7hCH1BT1fNbMabnngvXDtfqJpP62QyXcfrXNmcW0fznTYKgKmA==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@ -2196,6 +2273,10 @@ packages:
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
saxes@6.0.0:
|
||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||
engines: {node: '>=v12.22.7'}
|
||||
|
||||
scheduler@0.23.2:
|
||||
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
|
||||
|
||||
@ -2358,6 +2439,9 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
symbol-tree@3.2.4:
|
||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||
|
||||
text-table@0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
|
||||
@ -2392,6 +2476,13 @@ packages:
|
||||
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tldts-core@6.1.60:
|
||||
resolution: {integrity: sha512-XHjoxak8SFQnHnmYHb3PcnW5TZ+9ErLZemZei3azuIRhQLw4IExsVbL3VZJdHcLeNaXq6NqawgpDPpjBOg4B5g==}
|
||||
|
||||
tldts@6.1.60:
|
||||
resolution: {integrity: sha512-TYVHm7G9NCnhgqOsFalbX6MG1Po5F4efF+tLfoeiOGQq48Oqgwcgz8upY2R1BHWa4aDrj28RYx0dkYJ63qCFMg==}
|
||||
hasBin: true
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
@ -2404,6 +2495,14 @@ packages:
|
||||
resolution: {integrity: sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tough-cookie@5.0.0:
|
||||
resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
tr46@5.0.0:
|
||||
resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tree-dump@1.0.2:
|
||||
resolution: {integrity: sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==}
|
||||
engines: {node: '>=10.0'}
|
||||
@ -2571,9 +2670,17 @@ packages:
|
||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
w3c-xmlserializer@5.0.0:
|
||||
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
wbuf@1.7.3:
|
||||
resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==}
|
||||
|
||||
webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
webpack-bundle-analyzer@4.6.1:
|
||||
resolution: {integrity: sha512-oKz9Oz9j3rUciLNfpGFjOb49/jEpXNmWdVH8Ls//zNcnLlQdTGXQQMsBbb/gR7Zl8WNLxVCq+0Hqbx3zv6twBw==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
@ -2609,6 +2716,18 @@ packages:
|
||||
resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
|
||||
whatwg-encoding@3.1.1:
|
||||
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
whatwg-mimetype@4.0.0:
|
||||
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
whatwg-url@14.0.0:
|
||||
resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
which-boxed-primitive@1.0.2:
|
||||
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
|
||||
|
||||
@ -2670,6 +2789,13 @@ packages:
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
xml-name-validator@5.0.0:
|
||||
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
xmlchars@2.2.0:
|
||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
@ -3319,6 +3445,12 @@ snapshots:
|
||||
|
||||
acorn@8.13.0: {}
|
||||
|
||||
agent-base@7.1.1:
|
||||
dependencies:
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ajv-formats@2.1.1(ajv@8.17.1):
|
||||
optionalDependencies:
|
||||
ajv: 8.17.1
|
||||
@ -3423,6 +3555,8 @@ snapshots:
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.0.0
|
||||
@ -3531,6 +3665,10 @@ snapshots:
|
||||
|
||||
colorette@2.0.19: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
commander@7.2.0: {}
|
||||
|
||||
compressible@2.0.18:
|
||||
@ -3579,8 +3717,17 @@ snapshots:
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
cssstyle@4.1.0:
|
||||
dependencies:
|
||||
rrweb-cssom: 0.7.1
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
data-urls@5.0.0:
|
||||
dependencies:
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.0.0
|
||||
|
||||
data-view-buffer@1.0.1:
|
||||
dependencies:
|
||||
call-bind: 1.0.7
|
||||
@ -3609,6 +3756,8 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decimal.js@10.4.3: {}
|
||||
|
||||
deep-eql@5.0.2: {}
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
@ -3638,6 +3787,8 @@ snapshots:
|
||||
has-property-descriptors: 1.0.2
|
||||
object-keys: 1.1.1
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
depd@1.1.2: {}
|
||||
|
||||
depd@2.0.0: {}
|
||||
@ -3670,6 +3821,8 @@ snapshots:
|
||||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
es-abstract@1.23.3:
|
||||
dependencies:
|
||||
array-buffer-byte-length: 1.0.1
|
||||
@ -4020,6 +4173,12 @@ snapshots:
|
||||
cross-spawn: 7.0.3
|
||||
signal-exit: 4.1.0
|
||||
|
||||
form-data@4.0.1:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
|
||||
forwarded@0.2.0: {}
|
||||
|
||||
fresh@0.5.2: {}
|
||||
@ -4123,6 +4282,10 @@ snapshots:
|
||||
readable-stream: 2.3.8
|
||||
wbuf: 1.7.3
|
||||
|
||||
html-encoding-sniffer@4.0.0:
|
||||
dependencies:
|
||||
whatwg-encoding: 3.1.1
|
||||
|
||||
html-entities@2.5.2: {}
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
@ -4148,6 +4311,13 @@ snapshots:
|
||||
|
||||
http-parser-js@0.5.8: {}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.1
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
http-proxy-middleware@2.0.7(@types/express@4.17.21):
|
||||
dependencies:
|
||||
'@types/http-proxy': 1.17.15
|
||||
@ -4168,6 +4338,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
https-proxy-agent@7.0.5:
|
||||
dependencies:
|
||||
agent-base: 7.1.1
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
human-signals@2.1.0: {}
|
||||
|
||||
hyperdyperid@1.2.0: {}
|
||||
@ -4180,6 +4357,10 @@ snapshots:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
import-fresh@3.3.0:
|
||||
@ -4277,6 +4458,8 @@ snapshots:
|
||||
|
||||
is-plain-obj@3.0.0: {}
|
||||
|
||||
is-potential-custom-element-name@1.0.1: {}
|
||||
|
||||
is-regex@1.1.4:
|
||||
dependencies:
|
||||
call-bind: 1.0.7
|
||||
@ -4343,6 +4526,34 @@ snapshots:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
jsdom@25.0.1:
|
||||
dependencies:
|
||||
cssstyle: 4.1.0
|
||||
data-urls: 5.0.0
|
||||
decimal.js: 10.4.3
|
||||
form-data: 4.0.1
|
||||
html-encoding-sniffer: 4.0.0
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.5
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
nwsapi: 2.2.13
|
||||
parse5: 7.2.1
|
||||
rrweb-cssom: 0.7.1
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 5.0.0
|
||||
w3c-xmlserializer: 5.0.0
|
||||
webidl-conversions: 7.0.0
|
||||
whatwg-encoding: 3.1.1
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.0.0
|
||||
ws: 8.18.0
|
||||
xml-name-validator: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
json-buffer@3.0.1: {}
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
@ -4465,6 +4676,8 @@ snapshots:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
|
||||
nwsapi@2.2.13: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-inspect@1.13.2: {}
|
||||
@ -4552,6 +4765,10 @@ snapshots:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
||||
parse5@7.2.1:
|
||||
dependencies:
|
||||
entities: 4.5.0
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
@ -4735,6 +4952,8 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc': 4.24.4
|
||||
fsevents: 2.3.3
|
||||
|
||||
rrweb-cssom@0.7.1: {}
|
||||
|
||||
rsbuild-plugin-dts@0.0.16(@rsbuild/core@1.0.19)(typescript@5.6.3):
|
||||
dependencies:
|
||||
'@rsbuild/core': 1.0.19
|
||||
@ -4769,6 +4988,10 @@ snapshots:
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
saxes@6.0.0:
|
||||
dependencies:
|
||||
xmlchars: 2.2.0
|
||||
|
||||
scheduler@0.23.2:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
@ -4991,6 +5214,8 @@ snapshots:
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
symbol-tree@3.2.4: {}
|
||||
|
||||
text-table@0.2.0: {}
|
||||
|
||||
thingies@1.21.0(tslib@2.8.0):
|
||||
@ -5014,6 +5239,12 @@ snapshots:
|
||||
|
||||
tinyspy@3.0.2: {}
|
||||
|
||||
tldts-core@6.1.60: {}
|
||||
|
||||
tldts@6.1.60:
|
||||
dependencies:
|
||||
tldts-core: 6.1.60
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
@ -5022,6 +5253,14 @@ snapshots:
|
||||
|
||||
totalist@1.1.0: {}
|
||||
|
||||
tough-cookie@5.0.0:
|
||||
dependencies:
|
||||
tldts: 6.1.60
|
||||
|
||||
tr46@5.0.0:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
tree-dump@1.0.2(tslib@2.8.0):
|
||||
dependencies:
|
||||
tslib: 2.8.0
|
||||
@ -5192,12 +5431,54 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vitest@2.1.4(@types/node@22.8.1)(jsdom@25.0.1):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.4
|
||||
'@vitest/mocker': 2.1.4(vite@5.4.10(@types/node@22.8.1))
|
||||
'@vitest/pretty-format': 2.1.4
|
||||
'@vitest/runner': 2.1.4
|
||||
'@vitest/snapshot': 2.1.4
|
||||
'@vitest/spy': 2.1.4
|
||||
'@vitest/utils': 2.1.4
|
||||
chai: 5.1.2
|
||||
debug: 4.3.7
|
||||
expect-type: 1.1.0
|
||||
magic-string: 0.30.12
|
||||
pathe: 1.1.2
|
||||
std-env: 3.7.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 0.3.1
|
||||
tinypool: 1.0.1
|
||||
tinyrainbow: 1.2.0
|
||||
vite: 5.4.10(@types/node@22.8.1)
|
||||
vite-node: 2.1.4(@types/node@22.8.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.8.1
|
||||
jsdom: 25.0.1
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
void-elements@3.1.0: {}
|
||||
|
||||
w3c-xmlserializer@5.0.0:
|
||||
dependencies:
|
||||
xml-name-validator: 5.0.0
|
||||
|
||||
wbuf@1.7.3:
|
||||
dependencies:
|
||||
minimalistic-assert: 1.0.1
|
||||
|
||||
webidl-conversions@7.0.0: {}
|
||||
|
||||
webpack-bundle-analyzer@4.6.1:
|
||||
dependencies:
|
||||
acorn: 8.13.0
|
||||
@ -5268,6 +5549,17 @@ snapshots:
|
||||
|
||||
websocket-extensions@0.1.4: {}
|
||||
|
||||
whatwg-encoding@3.1.1:
|
||||
dependencies:
|
||||
iconv-lite: 0.6.3
|
||||
|
||||
whatwg-mimetype@4.0.0: {}
|
||||
|
||||
whatwg-url@14.0.0:
|
||||
dependencies:
|
||||
tr46: 5.0.0
|
||||
webidl-conversions: 7.0.0
|
||||
|
||||
which-boxed-primitive@1.0.2:
|
||||
dependencies:
|
||||
is-bigint: 1.0.4
|
||||
@ -5333,6 +5625,10 @@ snapshots:
|
||||
|
||||
ws@8.18.0: {}
|
||||
|
||||
xml-name-validator@5.0.0: {}
|
||||
|
||||
xmlchars@2.2.0: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
7
vitest.config.ts
Normal file
7
vitest.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
// ...
|
||||
},
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user