refactor: 清理测试代码 eslint-disable 指令,消除文件级和重复局部禁用
This commit is contained in:
@@ -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` 命令独立运行。
|
||||
|
||||
@@ -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` 指令的情况下通过
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user