src/vendor 语义是'第三方/参考',却装着线上氢能看板并反向 import
src/modules,依赖方向倒挂。本次把生产代码归位并建立氢能分层:
src/modules/energy/hydrogen/
index.tsx 导航入口(原 HydrogenModule.tsx)
api.ts types.ts HTTP 与 DTO
dev-mock-api.ts 仅 dev 的 vite 插件(仍由 vite.config.ts 引用)
model/ 纯逻辑(format / bearing-labels / daily-detail-format)
board/ 经营看板 UI(原 vendor energy-h2-bi-board)
station-daily/ 单站日报 UI(原 vendor energy-h2-station-daily)
drill/ 下钻弹层 UI(原 hydrogen-bi-v2 的 UI 部分)
common/ 工具与共享组件(原 vendor common + prototype-download)
fonts/ JetBrains Mono(原 vendor resources/design-system)
- UI 与 CSS 逐字节保留,仅移动位置与改写相对 import。
- 删除自挂载原型入口后遗留的 2 个 annotation-source.json。
- vite.config.ts 与 tsconfig 的路径/exclude 同步清理。
- independent-entry.test.ts 不再硬编码路径,改为断言'两个入口解析到同一文件'。
导入改写用一次性 codemod 完成(按旧位置解析、按新位置重写),
lint / test(131) / build 全绿,可达性分析仍为 0 未引用文件。
144 lines
5.3 KiB
TypeScript
144 lines
5.3 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { fetchAllH2BiDrill, fetchAllH2BiDrillRecords } from "./api";
|
|
import type { H2BiDrillResponse } from "./types";
|
|
|
|
const query = {
|
|
year: 2026,
|
|
vehicleScope: "all" as const,
|
|
verifyScope: "all" as const,
|
|
};
|
|
|
|
function response(overrides: Partial<H2BiDrillResponse> = {}): H2BiDrillResponse {
|
|
return {
|
|
groupBy: "station",
|
|
amountScope: "all",
|
|
filters: {},
|
|
summary: {},
|
|
groups: [],
|
|
records: [],
|
|
page: { page: 1, pageSize: 2, hasMore: false },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function withFetch(
|
|
handler: (url: URL) => H2BiDrillResponse | Promise<H2BiDrillResponse>,
|
|
run: () => Promise<void>,
|
|
) {
|
|
const original = globalThis.fetch;
|
|
globalThis.fetch = (async (input: string | URL | Request) => {
|
|
const url = new URL(String(input), "http://ln-bi.local");
|
|
return new Response(JSON.stringify(await handler(url)), { status: 200 });
|
|
}) as typeof fetch;
|
|
try {
|
|
await run();
|
|
} finally {
|
|
globalThis.fetch = original;
|
|
}
|
|
}
|
|
|
|
test("完整分组读取跨页,并以短页而非旧服务 hasMore 字段确认结束", async () => {
|
|
const pages: number[] = [];
|
|
await withFetch((url) => {
|
|
const page = Number(url.searchParams.get("page"));
|
|
pages.push(page);
|
|
return response({
|
|
groups: page === 1
|
|
? ["甲", "乙"].map((name, index) => ({ id: String(index), name, province: null, city: null, recordCount: 1, stationCount: 1, customerCount: 1, kg: 1, cost: 1, revenue: 1, lingniuKg: 1, externalKg: 0 }))
|
|
: [{ id: "3", name: "丙", province: null, city: null, recordCount: 1, stationCount: 1, customerCount: 1, kg: 1, cost: 1, revenue: 1, lingniuKg: 1, externalKg: 0 }],
|
|
});
|
|
}, async () => {
|
|
const result = await fetchAllH2BiDrill({ ...query, groupBy: "station" }, { pageSize: 2 });
|
|
assert.deepEqual(result.groups.map((row) => row.name), ["甲", "乙", "丙"]);
|
|
assert.equal(result.page.complete, true);
|
|
assert.equal(result.page.pagesRead, 2);
|
|
});
|
|
assert.deepEqual(pages, [1, 2]);
|
|
});
|
|
|
|
test("完整记录读取跨页后保留全部订单", async () => {
|
|
await withFetch((url) => {
|
|
const page = Number(url.searchParams.get("page"));
|
|
return response({
|
|
groupBy: "record",
|
|
records: page === 1
|
|
? [{ id: "a" }, { id: "b" }]
|
|
: [{ id: "c" }],
|
|
});
|
|
}, async () => {
|
|
const result = await fetchAllH2BiDrillRecords(query, { pageSize: 2 });
|
|
assert.deepEqual(result.records.map((row) => row.id), ["a", "b", "c"]);
|
|
assert.equal(result.page.hasMore, false);
|
|
});
|
|
});
|
|
|
|
test("全量读取在中途请求失败时拒绝,不返回部分结果", async () => {
|
|
const original = globalThis.fetch;
|
|
globalThis.fetch = (async (input: string | URL | Request) => {
|
|
const page = new URL(String(input), "http://ln-bi.local").searchParams.get("page");
|
|
if (page === "2") return new Response("failed", { status: 502, statusText: "Bad Gateway" });
|
|
return new Response(JSON.stringify(response({
|
|
groups: ["甲", "乙"].map((name, index) => ({ id: String(index), name, province: null, city: null, recordCount: 1, stationCount: 1, customerCount: 1, kg: 1, cost: 1, revenue: 1, lingniuKg: 1, externalKg: 0 })),
|
|
})), { status: 200 });
|
|
}) as typeof fetch;
|
|
try {
|
|
await assert.rejects(
|
|
fetchAllH2BiDrill({ ...query, groupBy: "station" }, { pageSize: 2 }),
|
|
/API error: 502/,
|
|
);
|
|
} finally {
|
|
globalThis.fetch = original;
|
|
}
|
|
});
|
|
|
|
test("空结果是完整结果而不是加载失败", async () => {
|
|
await withFetch(() => response(), async () => {
|
|
const result = await fetchAllH2BiDrill({ ...query, groupBy: "station" });
|
|
assert.deepEqual(result.groups, []);
|
|
assert.equal(result.page.pagesRead, 1);
|
|
});
|
|
});
|
|
|
|
test("取消的全量读取不会发起请求或生成部分数据", async () => {
|
|
const controller = new AbortController();
|
|
controller.abort();
|
|
await assert.rejects(
|
|
fetchAllH2BiDrill({ ...query, groupBy: "station" }, { signal: controller.signal }),
|
|
(error: Error) => error.name === "AbortError",
|
|
);
|
|
});
|
|
|
|
test("全量读取在第一页完成后也会响应取消,不会请求下一页", async () => {
|
|
const controller = new AbortController();
|
|
let calls = 0;
|
|
await withFetch(() => {
|
|
calls += 1;
|
|
controller.abort();
|
|
return response({
|
|
groups: ["甲", "乙"].map((name, index) => ({ id: String(index), name, province: null, city: null, recordCount: 1, stationCount: 1, customerCount: 1, kg: 1, cost: 1, revenue: 1, lingniuKg: 1, externalKg: 0 })),
|
|
});
|
|
}, async () => {
|
|
await assert.rejects(
|
|
fetchAllH2BiDrill({ ...query, groupBy: "station" }, { signal: controller.signal, pageSize: 2 }),
|
|
(error: Error) => error.name === "AbortError",
|
|
);
|
|
});
|
|
assert.equal(calls, 1);
|
|
});
|
|
|
|
test("maxRows 和 maxPages 均拒绝不完整的全量读取", async () => {
|
|
await withFetch(() => response({
|
|
groups: ["甲", "乙"].map((name, index) => ({ id: String(index), name, province: null, city: null, recordCount: 1, stationCount: 1, customerCount: 1, kg: 1, cost: 1, revenue: 1, lingniuKg: 1, externalKg: 0 })),
|
|
}), async () => {
|
|
await assert.rejects(
|
|
fetchAllH2BiDrill({ ...query, groupBy: "station" }, { pageSize: 2, maxRows: 1 }),
|
|
/超过 1 条保护上限/,
|
|
);
|
|
await assert.rejects(
|
|
fetchAllH2BiDrill({ ...query, groupBy: "station" }, { pageSize: 2, maxPages: 1 }),
|
|
/超过 1 页保护上限/,
|
|
);
|
|
});
|
|
});
|