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-import` | 导入路径验证、循环依赖检测、重复导入合并 |
| `eslint-plugin-prettier` recommended + `eslint-config-prettier` | 将 Prettier 格式集成为 ESLint 规则,禁用冲突规则 | | `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 配置 ### Prettier 配置
配置文件:`.prettierrc.json`,通过 `eslint-plugin-prettier` 集成为 ESLint 规则(`lint` 命令同时检查格式),也可通过 `format` 命令独立运行。 配置文件:`.prettierrc.json`,通过 `eslint-plugin-prettier` 集成为 ESLint 规则(`lint` 命令同时检查格式),也可通过 `format` 命令独立运行。

View File

@@ -124,3 +124,26 @@
#### Scenario: 完整验证失败 #### Scenario: 完整验证失败
- **WHEN** `verify` 中任一阶段失败 - **WHEN** `verify` 中任一阶段失败
- **THEN** `verify` MUST 以非零状态退出且不能继续声明验证成功 - **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 { mkdir, rm } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@@ -45,17 +45,12 @@ describe("parseTargets", () => {
test("无效 target 导致进程退出", () => { test("无效 target 导致进程退出", () => {
const exitCalls: number[] = []; const exitCalls: number[] = [];
// eslint-disable-next-line @typescript-eslint/unbound-method const exitSpy = spyOn(process, "exit").mockImplementation(((code: number) => {
const originalExit = process.exit;
process.exit = ((code: number) => {
exitCalls.push(code); exitCalls.push(code);
}) as never; }) as typeof process.exit);
try { parseTargets(["--target", "invalid-target"]);
parseTargets(["--target", "invalid-target"]); exitSpy.mockRestore();
} finally {
process.exit = originalExit;
}
expect(exitCalls).toEqual([1]); expect(exitCalls).toEqual([1]);
}); });

View File

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

View File

@@ -4,7 +4,6 @@
* 组件测试使用各自的 test-utils.tsx * 组件测试使用各自的 test-utils.tsx
*/ */
/* eslint-disable @typescript-eslint/no-empty-function */
// Set up jsdom for ALL tests (both backend and frontend) // Set up jsdom for ALL tests (both backend and frontend)
import { JSDOM } from "jsdom"; import { JSDOM } from "jsdom";
@@ -34,8 +33,10 @@ const nodeProto = dom.window.Node.prototype;
const elementProto = dom.window.Element.prototype; const elementProto = dom.window.Element.prototype;
const htmlElementProto = dom.window.HTMLElement.prototype; const htmlElementProto = dom.window.HTMLElement.prototype;
const attachEventFn = () => {}; const noop = () => undefined;
const detachEventFn = () => {};
const attachEventFn = noop;
const detachEventFn = noop;
Object.defineProperty(nodeProto, "attachEvent", { configurable: true, value: attachEventFn, writable: true }); Object.defineProperty(nodeProto, "attachEvent", { configurable: true, value: attachEventFn, writable: true });
Object.defineProperty(nodeProto, "detachEvent", { configurable: true, value: detachEventFn, 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 // Other polyfills
globalThis.ResizeObserver = class { globalThis.ResizeObserver = class {
disconnect() {} disconnect() {
observe() {} return undefined;
unobserve() {} }
observe() {
return undefined;
}
unobserve() {
return undefined;
}
}; };
globalThis.MutationObserver = class { globalThis.MutationObserver = class {
disconnect() {} disconnect() {
observe() {} return undefined;
}
observe() {
return undefined;
}
takeRecords() { takeRecords() {
return []; return [];
} }
unobserve() {}
unobserve() {
return undefined;
}
}; };
globalThis.IntersectionObserver = class { globalThis.IntersectionObserver = class {
disconnect() {} disconnect() {
observe() {} return undefined;
}
observe() {
return undefined;
}
takeRecords() { takeRecords() {
return []; return [];
} }
unobserve() {}
unobserve() {
return undefined;
}
} as unknown as typeof IntersectionObserver; } as unknown as typeof IntersectionObserver;
globalThis.requestAnimationFrame = (cb: FrameRequestCallback) => setTimeout(cb, 16); globalThis.requestAnimationFrame = (cb: FrameRequestCallback) => setTimeout(cb, 16);
@@ -74,24 +101,24 @@ globalThis.cancelAnimationFrame = (id: number) => clearTimeout(id);
Object.defineProperty(dom.window, "matchMedia", { Object.defineProperty(dom.window, "matchMedia", {
value: (query: string) => ({ value: (query: string) => ({
addEventListener: () => {}, addEventListener: noop,
addListener: () => {}, addListener: noop,
dispatchEvent: () => true, dispatchEvent: () => true,
matches: false, matches: false,
media: query, media: query,
onchange: null, onchange: null,
removeEventListener: () => {}, removeEventListener: noop,
removeListener: () => {}, removeListener: noop,
}), }),
writable: true, writable: true,
}); });
dom.window.Element.prototype.scrollTo = () => {}; dom.window.Element.prototype.scrollTo = noop;
dom.window.Element.prototype.scrollIntoView = () => {}; dom.window.Element.prototype.scrollIntoView = noop;
Object.defineProperty(dom.window, "customElements", { Object.defineProperty(dom.window, "customElements", {
value: { value: {
define: () => {}, define: noop,
get: () => undefined, get: () => undefined,
}, },
writable: true, 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 "../../../tests/web/test-utils";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, test, vi } from "bun:test"; 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 { App } from "../../../src/web/app";
import { THEME_MEDIA_QUERY, THEME_PREFERENCE_STORAGE_KEY } from "../../../src/web/hooks/use-theme-preference"; 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 { return {
data: { data: {
summary: { 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) { function installMatchMedia(initialMatches: boolean) {
const originalMatchMedia = window.matchMedia; const originalMatchMedia = window.matchMedia;
let matches = initialMatches; 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", () => { describe("App", () => {
let matchMediaController: ReturnType<typeof installMatchMedia>; let matchMediaController: ReturnType<typeof installMatchMedia>;
beforeEach(() => { beforeEach(() => {
const { useDashboard } = require("../../../src/web/hooks/use-queries"); useDashboardMock.mockReturnValue(createDashboardResult());
useDashboard.mockReturnValue(createDashboardResult());
window.localStorage.clear(); window.localStorage.clear();
document.documentElement.removeAttribute("theme-mode"); document.documentElement.removeAttribute("theme-mode");
matchMediaController = installMatchMedia(false); matchMediaController = installMatchMedia(false);
@@ -117,8 +115,7 @@ describe("App", () => {
}); });
test("loading 状态不崩溃", () => { test("loading 状态不崩溃", () => {
const { useDashboard } = require("../../../src/web/hooks/use-queries"); useDashboardMock.mockReturnValue(
useDashboard.mockReturnValue(
createDashboardResult({ createDashboardResult({
data: null, data: null,
dataUpdatedAt: 0, dataUpdatedAt: 0,
@@ -134,12 +131,11 @@ describe("App", () => {
}); });
test("错误状态不崩溃", () => { test("错误状态不崩溃", () => {
const { useDashboard } = require("../../../src/web/hooks/use-queries"); useDashboardMock.mockReturnValue(
useDashboard.mockReturnValue(
createDashboardResult({ createDashboardResult({
data: null, data: null,
dataUpdatedAt: 0, dataUpdatedAt: 0,
error: { message: "Network error" }, error: new Error("Network error"),
isFetching: false, isFetching: false,
isLoading: false, isLoading: false,
refetch: vi.fn(), refetch: vi.fn(),
@@ -151,8 +147,7 @@ describe("App", () => {
}); });
test("有数据状态不崩溃", () => { test("有数据状态不崩溃", () => {
const { useDashboard } = require("../../../src/web/hooks/use-queries"); useDashboardMock.mockReturnValue(
useDashboard.mockReturnValue(
createDashboardResult({ createDashboardResult({
data: { data: {
summary: { summary: {
@@ -215,9 +210,8 @@ describe("App", () => {
}); });
test("缺失版本时不展示版本占位", () => { test("缺失版本时不展示版本占位", () => {
const { useMeta } = require("../../../src/web/hooks/use-queries"); useMetaMock.mockReturnValue({
useMeta.mockReturnValue({ data: { checkerTypes: ["http", "cmd"], version: undefined as unknown as string },
data: { checkerTypes: ["http", "cmd"] },
}); });
render(<App />); render(<App />);
@@ -225,8 +219,7 @@ describe("App", () => {
}); });
test("复用 useMeta 查询结果", () => { test("复用 useMeta 查询结果", () => {
const { useMeta } = require("../../../src/web/hooks/use-queries");
render(<App />); render(<App />);
expect(useMeta).toHaveBeenCalled(); expect(useMetaMock).toHaveBeenCalled();
}); });
}); });