1
0

refactor: 清理测试代码 eslint-disable 指令,消除文件级和重复局部禁用

This commit is contained in:
2026-05-21 00:35:08 +08:00
parent ccd16a583e
commit b432581444
6 changed files with 185 additions and 175 deletions

View File

@@ -1157,6 +1157,15 @@ bun run check # 一键运行 schema:check + typecheck + lint + test
| `eslint-plugin-import` | 导入路径验证、循环依赖检测、重复导入合并 |
| `eslint-plugin-prettier` recommended + `eslint-config-prettier` | 将 Prettier 格式集成为 ESLint 规则,禁用冲突规则 |
### 测试代码 ESLint 规范
测试代码与业务代码使用相同的 ESLint 规则集,应优先通过类型化 helper、类型化 mock、显式 no-op 和受控断言模式满足已启用的类型感知规则,最小化 `eslint-disable` 的使用。具体约定:
- 使用类型化 mock 变量(`vi.fn()`)替代动态 `require` 获取 mocked module
- 异步错误断言使用 helper 或显式 try/catch避免依赖 Bun `expect(...).rejects``await-thenable` 规则的类型不匹配
- polyfill 中的 intentional no-op 使用显式可解释写法(如 `() => undefined` 或共享 `noop` 函数)
-`process.exit` 等系统 API 使用 `spyOn`(从 `bun:test` 导入)受控 mock 而非手动 monkey patch
### Prettier 配置
配置文件:`.prettierrc.json`,通过 `eslint-plugin-prettier` 集成为 ESLint 规则(`lint` 命令同时检查格式),也可通过 `format` 命令独立运行。

View File

@@ -124,3 +124,26 @@
#### Scenario: 完整验证失败
- **WHEN** `verify` 中任一阶段失败
- **THEN** `verify` MUST 以非零状态退出且不能继续声明验证成功
### Requirement: 测试代码 ESLint 禁用最小化
项目测试代码 SHALL 优先通过类型化 helper、类型化 mock、显式 no-op 和受控断言模式满足已启用的 ESLint 类型感知规则。受本变更审计的项目自有测试文件 MUST NOT 保留用于压制可通过代码结构解决的 `eslint-disable` 指令。
#### Scenario: 消除组件测试文件级禁用
- **WHEN** ESLint 检查 `tests/web/components/App.test.tsx`
- **THEN** 该文件 MUST 不使用文件级 `eslint-disable` 关闭 `@typescript-eslint/no-require-imports``@typescript-eslint/no-unsafe-*` 规则,并且测试中的 hook mock SHALL 使用类型化引用或等价方式访问 mock API
#### Scenario: 消除配置加载测试重复 await 禁用
- **WHEN** `tests/server/checker/config-loader.test.ts` 断言 `loadConfig()` 异步失败
- **THEN** 测试 SHALL 使用 helper 或显式 try/catch 断言错误实例与消息MUST 不通过逐行 `eslint-disable-next-line @typescript-eslint/await-thenable` 压制 Bun `expect(...).rejects` 类型不匹配
#### Scenario: 测试环境 no-op polyfill 保持可解释
- **WHEN** `tests/setup.ts` 为 jsdom 测试环境定义浏览器 API polyfill
- **THEN** intentional no-op SHALL 使用显式可解释写法表达MUST 不通过文件级 `eslint-disable @typescript-eslint/no-empty-function` 关闭空函数检查
#### Scenario: release 测试拦截 process.exit 保持窄作用域
- **WHEN** `tests/scripts/release.test.ts` 验证无效 release target 会触发 `process.exit(1)`
- **THEN** 测试 SHALL 使用受控 mock 或等价窄作用域替换并在断言后恢复MUST 不通过 `eslint-disable-next-line @typescript-eslint/unbound-method` 保存未绑定方法
#### Scenario: 质量门禁验证禁用清理
- **WHEN** 开发者运行 `bun run lint`
- **THEN** ESLint MUST 检查项目自有测试代码并在无上述 `eslint-disable` 指令的情况下通过

View File

@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
import { mkdir, rm } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
@@ -45,17 +45,12 @@ describe("parseTargets", () => {
test("无效 target 导致进程退出", () => {
const exitCalls: number[] = [];
// eslint-disable-next-line @typescript-eslint/unbound-method
const originalExit = process.exit;
process.exit = ((code: number) => {
const exitSpy = spyOn(process, "exit").mockImplementation(((code: number) => {
exitCalls.push(code);
}) as never;
}) as typeof process.exit);
try {
parseTargets(["--target", "invalid-target"]);
} finally {
process.exit = originalExit;
}
parseTargets(["--target", "invalid-target"]);
exitSpy.mockRestore();
expect(exitCalls).toEqual([1]);
});

View File

@@ -104,6 +104,17 @@ describe("loadConfig", () => {
expect((error as Error).message).toContain(message);
}
async function expectConfigLoadError(configPath: string, message: string): Promise<void> {
let error: unknown;
try {
await loadConfig(configPath);
} catch (caught) {
error = caught;
}
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toContain(message);
}
test("解析最简 HTTP 配置", async () => {
const configPath = join(tempDir, "minimal-http.yaml");
await writeFile(
@@ -327,8 +338,7 @@ targets:
url: "http://example.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("name 不能为空白");
await expectConfigLoadError(configPath, "name 不能为空白");
});
test("name 仅包含空白字符抛出错误", async () => {
@@ -343,8 +353,7 @@ targets:
url: "http://example.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("name 不能为空白");
await expectConfigLoadError(configPath, "name 不能为空白");
});
test("description 显式 null 保留为 null", async () => {
@@ -449,8 +458,7 @@ targets:
maxRedirects: "\${max_redirects}"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("maxRedirects");
await expectConfigLoadError(configPath, "maxRedirects");
});
test("变量替换后通过 schema 校验", async () => {
@@ -491,8 +499,7 @@ targets:
url: "\${MISSING_BASE_URL}/health"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("未定义的变量");
await expectConfigLoadError(configPath, "未定义的变量");
});
test("绝对 dataDir 保持不变", async () => {
@@ -546,8 +553,7 @@ targets:
});
test("配置文件不存在抛出错误", async () => {
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig("/nonexistent/file.yaml")).rejects.toThrow("配置文件不存在");
await expectConfigLoadError("/nonexistent/file.yaml", "配置文件不存在");
});
test("target 缺少 id 抛出错误", async () => {
@@ -560,8 +566,7 @@ targets:
url: "http://example.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("缺少 id 字段");
await expectConfigLoadError(configPath, "缺少 id 字段");
});
test("target 缺少 type 抛出错误", async () => {
@@ -575,8 +580,7 @@ targets:
url: "http://example.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("缺少 type 字段");
await expectConfigLoadError(configPath, "缺少 type 字段");
});
test("HTTP target 缺少 url 抛出错误", async () => {
@@ -590,8 +594,7 @@ targets:
http: {}
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("缺少 http.url 字段");
await expectConfigLoadError(configPath, "缺少 http.url 字段");
});
test("HTTP target 缺少 http 分组抛出清晰错误", async () => {
@@ -604,8 +607,7 @@ targets:
type: http
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("缺少 http.url 字段");
await expectConfigLoadError(configPath, "缺少 http.url 字段");
});
test("HTTP target ignoreSSL 非布尔值抛出错误", async () => {
@@ -621,8 +623,7 @@ targets:
ignoreSSL: "true"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("http.ignoreSSL 类型不合法");
await expectConfigLoadError(configPath, "http.ignoreSSL 类型不合法");
});
test("HTTP target maxRedirects 非负整数校验", async () => {
@@ -638,8 +639,7 @@ targets:
maxRedirects: 1.5
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("http.maxRedirects 类型不合法");
await expectConfigLoadError(configPath, "http.maxRedirects 类型不合法");
});
test("HTTP target status 模式非法抛出错误", async () => {
@@ -656,8 +656,7 @@ targets:
status: ["abc"]
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("status 模式");
await expectConfigLoadError(configPath, "status 模式");
});
test("cmd target 缺少 exec 抛出错误", async () => {
@@ -671,8 +670,7 @@ targets:
cmd: {}
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("缺少 cmd.exec 字段");
await expectConfigLoadError(configPath, "缺少 cmd.exec 字段");
});
test("非法 target type 抛出错误", async () => {
@@ -685,8 +683,7 @@ targets:
type: dns
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("不支持的 type");
await expectConfigLoadError(configPath, "不支持的 type");
});
test("target id 重复抛出错误", async () => {
@@ -706,8 +703,7 @@ targets:
url: "http://b.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("target id 重复");
await expectConfigLoadError(configPath, "target id 重复");
});
test("target id 为空字符串抛出错误", async () => {
@@ -721,8 +717,7 @@ targets:
url: "http://example.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("缺少 id 字段");
await expectConfigLoadError(configPath, "缺少 id 字段");
});
test("target id 命名不合法抛出错误", async () => {
@@ -736,8 +731,7 @@ targets:
url: "http://example.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("id 不符合命名规则");
await expectConfigLoadError(configPath, "id 不符合命名规则");
});
test("target id 包含下划线和连字符通过", async () => {
@@ -758,8 +752,7 @@ targets:
test("targets 为空数组抛出错误", async () => {
const configPath = join(tempDir, "empty-targets.yaml");
await writeFile(configPath, `targets: []`);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("至少一个 target");
await expectConfigLoadError(configPath, "至少一个 target");
});
test("无效端口号抛出错误", async () => {
@@ -776,8 +769,7 @@ targets:
url: "http://a.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("server.port 数值范围不合法");
await expectConfigLoadError(configPath, "server.port 数值范围不合法");
});
test("非法 maxConcurrentChecks 抛出错误", async () => {
@@ -794,8 +786,7 @@ targets:
url: "http://a.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("runtime.maxConcurrentChecks 数值范围不合法");
await expectConfigLoadError(configPath, "runtime.maxConcurrentChecks 数值范围不合法");
});
test("非法 size 格式抛出错误", async () => {
@@ -813,8 +804,7 @@ targets:
url: "http://a.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("无效的 size 格式");
await expectConfigLoadError(configPath, "无效的 size 格式");
});
test("非法 interval 格式抛出错误", async () => {
@@ -830,8 +820,7 @@ targets:
url: "http://a.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("无效的时长格式");
await expectConfigLoadError(configPath, "无效的时长格式");
});
test("解析 expect 配置", async () => {
@@ -1011,8 +1000,7 @@ targets:
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("group 必须为字符串");
await expectConfigLoadError(configPath, "group 必须为字符串");
});
test("HTTP headers 非字符串值抛出错误", async () => {
@@ -1029,8 +1017,7 @@ targets:
X-Custom: 123
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("http.headers");
await expectConfigLoadError(configPath, "http.headers");
});
test("HTTP body 非字符串抛出错误", async () => {
@@ -1046,8 +1033,7 @@ targets:
body: 123
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("http.body 类型不合法");
await expectConfigLoadError(configPath, "http.body 类型不合法");
});
test("maxBodyBytes 负数抛出错误", async () => {
@@ -1063,8 +1049,7 @@ targets:
maxBodyBytes: -1
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("非负安全整数");
await expectConfigLoadError(configPath, "非负安全整数");
});
test("maxBodyBytes 非整数抛出错误", async () => {
@@ -1080,8 +1065,7 @@ targets:
maxBodyBytes: 1.5
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("非负安全整数");
await expectConfigLoadError(configPath, "非负安全整数");
});
test("expect.status 数字不在 100-599 范围抛出错误", async () => {
@@ -1098,8 +1082,7 @@ targets:
status: [999]
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("100-599");
await expectConfigLoadError(configPath, "100-599");
});
test("expect.status 范围 6xx 抛出错误", async () => {
@@ -1116,8 +1099,7 @@ targets:
status: ["6xx"]
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("5xx");
await expectConfigLoadError(configPath, "5xx");
});
test("expect.durationMs 对象简写抛出错误", async () => {
@@ -1135,8 +1117,7 @@ targets:
foo: "bar"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("expect.durationMs.foo 是未知 matcher");
await expectConfigLoadError(configPath, "expect.durationMs.foo 是未知 matcher");
});
test("expect.body 非数组抛出错误", async () => {
@@ -1153,8 +1134,7 @@ targets:
body: "not-array"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("expect.body 必须为数组");
await expectConfigLoadError(configPath, "expect.body 必须为数组");
});
test("body rule 缺少支持字段抛出错误", async () => {
@@ -1172,8 +1152,7 @@ targets:
- foo: "bar"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("foo 是未知字段");
await expectConfigLoadError(configPath, "foo 是未知字段");
});
test("body rule 使用 match 字段(非支持)抛出错误", async () => {
@@ -1191,8 +1170,7 @@ targets:
- match: "ok"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("match 是未知字段");
await expectConfigLoadError(configPath, "match 是未知字段");
});
test("body rule 直接 matcher 混入 extractor 抛出错误", async () => {
@@ -1212,8 +1190,7 @@ targets:
path: "$.status"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("直接 matcher 不能与 extractor 混用");
await expectConfigLoadError(configPath, "直接 matcher 不能与 extractor 混用");
});
test("body regex 非法正则抛出错误", async () => {
@@ -1231,8 +1208,7 @@ targets:
- regex: "[invalid"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("regex 正则不合法");
await expectConfigLoadError(configPath, "regex 正则不合法");
});
test("body json path 不以 $. 开头抛出错误", async () => {
@@ -1252,8 +1228,7 @@ targets:
equals: "ok"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("json.path");
await expectConfigLoadError(configPath, "json.path");
});
test("body css selector 为空抛出错误", async () => {
@@ -1272,8 +1247,7 @@ targets:
selector: ""
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("css.selector 必须为非空字符串");
await expectConfigLoadError(configPath, "css.selector 必须为非空字符串");
});
test("旧 match matcher 抛出错误", async () => {
@@ -1292,8 +1266,7 @@ targets:
match: "[invalid"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("match 是未知 matcher");
await expectConfigLoadError(configPath, "match 是未知 matcher");
});
test("operator gte 非数字抛出错误", async () => {
@@ -1313,8 +1286,7 @@ targets:
gte: "abc"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("gte 必须为有限数字");
await expectConfigLoadError(configPath, "gte 必须为有限数字");
});
test("operator exists 非布尔值抛出错误", async () => {
@@ -1334,8 +1306,7 @@ targets:
exists: "yes"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("exists 必须为布尔值");
await expectConfigLoadError(configPath, "exists 必须为布尔值");
});
test("未知字段导致启动失败", async () => {
@@ -1357,8 +1328,7 @@ targets:
note: "ignored"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("unknownHttpField 是未知字段");
await expectConfigLoadError(configPath, "unknownHttpField 是未知字段");
});
test("xpath path 非空字符串校验", async () => {
@@ -1377,8 +1347,7 @@ targets:
path: ""
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("xpath.path 必须为非空字符串");
await expectConfigLoadError(configPath, "xpath.path 必须为非空字符串");
});
test("expect headers 非对象抛出错误", async () => {
@@ -1395,8 +1364,7 @@ targets:
headers: "invalid"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("expect.headers 类型不合法");
await expectConfigLoadError(configPath, "expect.headers 类型不合法");
});
test("HTTP method 小写输入失败", async () => {
@@ -1760,8 +1728,7 @@ targets:
url: "http://example.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("description");
await expectConfigLoadError(configPath, "description");
});
test("description 超过 500 字符抛出错误", async () => {
@@ -1776,8 +1743,7 @@ targets:
url: "http://example.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("description");
await expectConfigLoadError(configPath, "description");
});
test("变量替换后 description 超长抛出错误", async () => {
@@ -1794,8 +1760,7 @@ targets:
url: "http://example.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("description");
await expectConfigLoadError(configPath, "description");
});
test("id 超过 30 字符抛出错误", async () => {
@@ -1809,8 +1774,7 @@ targets:
url: "http://example.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("id");
await expectConfigLoadError(configPath, "id");
});
test("name 超过 30 字符抛出错误", async () => {
@@ -1825,8 +1789,7 @@ targets:
url: "http://example.com"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("name");
await expectConfigLoadError(configPath, "name");
});
test("解析最简 tcp 配置", async () => {

View File

@@ -4,7 +4,6 @@
* 组件测试使用各自的 test-utils.tsx
*/
/* eslint-disable @typescript-eslint/no-empty-function */
// Set up jsdom for ALL tests (both backend and frontend)
import { JSDOM } from "jsdom";
@@ -34,8 +33,10 @@ const nodeProto = dom.window.Node.prototype;
const elementProto = dom.window.Element.prototype;
const htmlElementProto = dom.window.HTMLElement.prototype;
const attachEventFn = () => {};
const detachEventFn = () => {};
const noop = () => undefined;
const attachEventFn = noop;
const detachEventFn = noop;
Object.defineProperty(nodeProto, "attachEvent", { configurable: true, value: attachEventFn, writable: true });
Object.defineProperty(nodeProto, "detachEvent", { configurable: true, value: detachEventFn, writable: true });
@@ -46,27 +47,53 @@ Object.defineProperty(htmlElementProto, "detachEvent", { configurable: true, val
// Other polyfills
globalThis.ResizeObserver = class {
disconnect() {}
observe() {}
unobserve() {}
disconnect() {
return undefined;
}
observe() {
return undefined;
}
unobserve() {
return undefined;
}
};
globalThis.MutationObserver = class {
disconnect() {}
observe() {}
disconnect() {
return undefined;
}
observe() {
return undefined;
}
takeRecords() {
return [];
}
unobserve() {}
unobserve() {
return undefined;
}
};
globalThis.IntersectionObserver = class {
disconnect() {}
observe() {}
disconnect() {
return undefined;
}
observe() {
return undefined;
}
takeRecords() {
return [];
}
unobserve() {}
unobserve() {
return undefined;
}
} as unknown as typeof IntersectionObserver;
globalThis.requestAnimationFrame = (cb: FrameRequestCallback) => setTimeout(cb, 16);
@@ -74,24 +101,24 @@ globalThis.cancelAnimationFrame = (id: number) => clearTimeout(id);
Object.defineProperty(dom.window, "matchMedia", {
value: (query: string) => ({
addEventListener: () => {},
addListener: () => {},
addEventListener: noop,
addListener: noop,
dispatchEvent: () => true,
matches: false,
media: query,
onchange: null,
removeEventListener: () => {},
removeListener: () => {},
removeEventListener: noop,
removeListener: noop,
}),
writable: true,
});
dom.window.Element.prototype.scrollTo = () => {};
dom.window.Element.prototype.scrollIntoView = () => {};
dom.window.Element.prototype.scrollTo = noop;
dom.window.Element.prototype.scrollIntoView = noop;
Object.defineProperty(dom.window, "customElements", {
value: {
define: () => {},
define: noop,
get: () => undefined,
},
writable: true,

View File

@@ -1,7 +1,3 @@
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import "../../../tests/web/test-utils";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, test, vi } from "bun:test";
@@ -9,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "bun:test";
import { App } from "../../../src/web/app";
import { THEME_MEDIA_QUERY, THEME_PREFERENCE_STORAGE_KEY } from "../../../src/web/hooks/use-theme-preference";
function createDashboardResult(overrides = {}) {
function createDashboardResult(overrides: Record<string, unknown> = {}) {
return {
data: {
summary: {
@@ -35,6 +31,40 @@ function createDashboardResult(overrides = {}) {
};
}
const useDashboardMock = vi.fn(() => createDashboardResult());
const useMetaMock = vi.fn(() => ({
data: { checkerTypes: ["http", "cmd"] as string[], version: "0.1.0" },
}));
const useTargetDetailMock = vi.fn(() => ({
activeTab: "overview",
closeDrawer: vi.fn(),
handlePageChange: vi.fn(),
handleTabChange: vi.fn(),
handleTimeChange: vi.fn(),
historyData: {
items: [],
page: 1,
pageSize: 20,
total: 0,
},
historyLoading: false,
metricsData: null,
metricsLoading: false,
openDrawer: vi.fn(),
selectedTarget: null,
timeFrom: "",
timeTo: "",
}));
void vi.mock("../../../src/web/hooks/use-queries", () => ({
useDashboard: useDashboardMock,
useMeta: useMetaMock,
}));
void vi.mock("../../../src/web/hooks/use-target-detail", () => ({
useTargetDetail: useTargetDetailMock,
}));
function installMatchMedia(initialMatches: boolean) {
const originalMatchMedia = window.matchMedia;
let matches = initialMatches;
@@ -65,43 +95,11 @@ function installMatchMedia(initialMatches: boolean) {
};
}
// Mock hooks
void vi.mock("../../../src/web/hooks/use-queries", () => ({
useDashboard: vi.fn(() => createDashboardResult()),
useMeta: vi.fn(() => ({
data: { checkerTypes: ["http", "cmd"], version: "0.1.0" },
})),
}));
void vi.mock("../../../src/web/hooks/use-target-detail", () => ({
useTargetDetail: vi.fn(() => ({
activeTab: "overview",
closeDrawer: vi.fn(),
handlePageChange: vi.fn(),
handleTabChange: vi.fn(),
handleTimeChange: vi.fn(),
historyData: {
items: [],
page: 1,
pageSize: 20,
total: 0,
},
historyLoading: false,
metricsData: null,
metricsLoading: false,
openDrawer: vi.fn(),
selectedTarget: null,
timeFrom: "",
timeTo: "",
})),
}));
describe("App", () => {
let matchMediaController: ReturnType<typeof installMatchMedia>;
beforeEach(() => {
const { useDashboard } = require("../../../src/web/hooks/use-queries");
useDashboard.mockReturnValue(createDashboardResult());
useDashboardMock.mockReturnValue(createDashboardResult());
window.localStorage.clear();
document.documentElement.removeAttribute("theme-mode");
matchMediaController = installMatchMedia(false);
@@ -117,8 +115,7 @@ describe("App", () => {
});
test("loading 状态不崩溃", () => {
const { useDashboard } = require("../../../src/web/hooks/use-queries");
useDashboard.mockReturnValue(
useDashboardMock.mockReturnValue(
createDashboardResult({
data: null,
dataUpdatedAt: 0,
@@ -134,12 +131,11 @@ describe("App", () => {
});
test("错误状态不崩溃", () => {
const { useDashboard } = require("../../../src/web/hooks/use-queries");
useDashboard.mockReturnValue(
useDashboardMock.mockReturnValue(
createDashboardResult({
data: null,
dataUpdatedAt: 0,
error: { message: "Network error" },
error: new Error("Network error"),
isFetching: false,
isLoading: false,
refetch: vi.fn(),
@@ -151,8 +147,7 @@ describe("App", () => {
});
test("有数据状态不崩溃", () => {
const { useDashboard } = require("../../../src/web/hooks/use-queries");
useDashboard.mockReturnValue(
useDashboardMock.mockReturnValue(
createDashboardResult({
data: {
summary: {
@@ -215,9 +210,8 @@ describe("App", () => {
});
test("缺失版本时不展示版本占位", () => {
const { useMeta } = require("../../../src/web/hooks/use-queries");
useMeta.mockReturnValue({
data: { checkerTypes: ["http", "cmd"] },
useMetaMock.mockReturnValue({
data: { checkerTypes: ["http", "cmd"], version: undefined as unknown as string },
});
render(<App />);
@@ -225,8 +219,7 @@ describe("App", () => {
});
test("复用 useMeta 查询结果", () => {
const { useMeta } = require("../../../src/web/hooks/use-queries");
render(<App />);
expect(useMeta).toHaveBeenCalled();
expect(useMetaMock).toHaveBeenCalled();
});
});