Files
ln-bi/src/modules/energy/hydrogen/drill-pagination.test.ts
T
dsh-agent 7556dbcebc refactor(stage2): 生产代码移出 src/vendor,氢能收敛为分层 feature
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 未引用文件。
2026-09-11 10:14:37 +08:00

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 页保护上限/,
);
});
});