lint / go (push) Canceled after 0s
lint / go_mod (push) Canceled after 0s
lint / conf (push) Canceled after 0s
lint / docslinks (push) Canceled after 0s
lint / docsorder (push) Canceled after 0s
lint / apidocs (push) Canceled after 0s
lint / other (push) Canceled after 0s
test / test_64 (push) Canceled after 0s
test / test_32 (push) Canceled after 0s
test / test_e2e (push) Canceled after 0s
176 lines
5.8 KiB
JavaScript
176 lines
5.8 KiB
JavaScript
"use strict";
|
|
|
|
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
|
|
const ListTools = require("../internal/api/admin/list-tools.js");
|
|
|
|
function devices(count) {
|
|
return Array.from({ length: count }, (_, index) => ({
|
|
name: `device-${String(index + 1).padStart(3, "0")}`,
|
|
sourceMasked: `rtsp://camera.invalid/${index + 1}`,
|
|
status: ["online", "offline", "idle"][index % 3],
|
|
}));
|
|
}
|
|
|
|
function paginatedFetcher(items, options = {}) {
|
|
let active = 0;
|
|
let maxActive = 0;
|
|
const calls = [];
|
|
const fetcher = async (url) => {
|
|
const parsed = new URL(url, "http://localhost");
|
|
const page = Number(parsed.searchParams.get("page"));
|
|
const pageSize = Number(parsed.searchParams.get("itemsPerPage"));
|
|
calls.push({ page, pageSize });
|
|
if (page === options.failPage) {
|
|
const error = new Error("测试页不可用。");
|
|
error.status = 503;
|
|
throw error;
|
|
}
|
|
active += 1;
|
|
maxActive = Math.max(maxActive, active);
|
|
await new Promise((resolve) => setTimeout(resolve, options.delay || 0));
|
|
active -= 1;
|
|
return {
|
|
itemCount: items.length,
|
|
pageCount: Math.ceil(items.length / pageSize),
|
|
items: items.slice(page * pageSize, (page + 1) * pageSize),
|
|
};
|
|
};
|
|
return { fetcher, calls, maxActive: () => maxActive };
|
|
}
|
|
|
|
test("fetchAllPages loads all 235 items in API pages of 100", async () => {
|
|
const source = devices(235);
|
|
const mock = paginatedFetcher(source);
|
|
const progress = [];
|
|
const result = await ListTools.fetchAllPages(mock.fetcher, "/v3/config/paths/list", {
|
|
label: "设备配置",
|
|
pageSize: 100,
|
|
concurrency: 4,
|
|
onProgress: (value) => progress.push(value),
|
|
});
|
|
|
|
assert.equal(result.items.length, 235);
|
|
assert.equal(result.itemCount, 235);
|
|
assert.equal(result.pageCount, 3);
|
|
assert.deepEqual(mock.calls.map((call) => call.page).sort((a, b) => a - b), [0, 1, 2]);
|
|
assert.ok(mock.calls.every((call) => call.pageSize === 100));
|
|
assert.equal(progress.at(-1).loaded, 235);
|
|
assert.equal(progress.at(-1).total, 235);
|
|
});
|
|
|
|
test("fetchAllPages honors the concurrency limit", async () => {
|
|
const mock = paginatedFetcher(devices(701), { delay: 5 });
|
|
await ListTools.fetchAllPages(mock.fetcher, "/v3/paths/list", {
|
|
pageSize: 100,
|
|
concurrency: 3,
|
|
});
|
|
assert.ok(mock.maxActive() <= 3);
|
|
assert.ok(mock.maxActive() >= 2);
|
|
});
|
|
|
|
test("limitConcurrency bounds requests shared by multiple resources", async () => {
|
|
let active = 0;
|
|
let maxActive = 0;
|
|
const limited = ListTools.limitConcurrency(async (value) => {
|
|
active += 1;
|
|
maxActive = Math.max(maxActive, active);
|
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
active -= 1;
|
|
return value;
|
|
}, 4);
|
|
|
|
const values = await Promise.all(Array.from({ length: 12 }, (_, index) => limited(index)));
|
|
assert.deepEqual(values, Array.from({ length: 12 }, (_, index) => index));
|
|
assert.equal(maxActive, 4);
|
|
});
|
|
|
|
test("fetchAllPages reports the failed resource page without returning partial data", async () => {
|
|
const mock = paginatedFetcher(devices(235), { failPage: 2 });
|
|
await assert.rejects(
|
|
ListTools.fetchAllPages(mock.fetcher, "/v3/paths/list", {
|
|
label: "运行状态",
|
|
pageSize: 100,
|
|
}),
|
|
(error) => {
|
|
assert.equal(error.name, "PageLoadError");
|
|
assert.equal(error.status, 503);
|
|
assert.equal(error.page, 2);
|
|
assert.match(error.message, /运行状态第 3 页加载失败/);
|
|
return true;
|
|
},
|
|
);
|
|
});
|
|
|
|
test("uniqueSortedByName removes duplicates and sorts deterministically", () => {
|
|
const result = ListTools.uniqueSortedByName([
|
|
{ name: "camera-2", value: "old" },
|
|
{ name: "camera-1" },
|
|
{ name: "camera-2", value: "new" },
|
|
{ name: "" },
|
|
]);
|
|
assert.deepEqual(result, [
|
|
{ name: "camera-1" },
|
|
{ name: "camera-2", value: "new" },
|
|
]);
|
|
});
|
|
|
|
test("complete statistics and filters cover all devices before pagination", () => {
|
|
const source = devices(235);
|
|
const getStatus = (item) => item.status;
|
|
assert.deepEqual(ListTools.countStatuses(source, getStatus), {
|
|
total: 235,
|
|
online: 79,
|
|
offline: 78,
|
|
idle: 78,
|
|
});
|
|
|
|
const idle = ListTools.filterConfigs(source, "camera.invalid", "idle", getStatus);
|
|
assert.equal(idle.length, 78);
|
|
assert.deepEqual(
|
|
ListTools.filterConfigs(source, "device-235", "all", getStatus).map((item) => item.name),
|
|
["device-235"],
|
|
);
|
|
});
|
|
|
|
test("paginateItems handles empty, boundary and clamped pages", () => {
|
|
assert.deepEqual(ListTools.paginateItems([], 9, 50), {
|
|
items: [], page: 1, pageSize: 50, totalItems: 0, totalPages: 1, start: 0, end: 0,
|
|
});
|
|
|
|
const source = devices(235);
|
|
const last = ListTools.paginateItems(source, 5, 50);
|
|
assert.equal(last.items.length, 35);
|
|
assert.equal(last.start, 201);
|
|
assert.equal(last.end, 235);
|
|
assert.equal(last.totalPages, 5);
|
|
|
|
const clamped = ListTools.paginateItems(source.slice(0, 100), 5, 50);
|
|
assert.equal(clamped.page, 2);
|
|
assert.equal(clamped.start, 51);
|
|
assert.equal(clamped.end, 100);
|
|
|
|
const invalidSize = ListTools.paginateItems(source, 1, 17);
|
|
assert.equal(invalidSize.pageSize, 50);
|
|
});
|
|
|
|
test("paginateItems covers the documented device-count boundaries", () => {
|
|
const expectations = [
|
|
{ count: 0, pages: 1, lastPageItems: 0 },
|
|
{ count: 1, pages: 1, lastPageItems: 1 },
|
|
{ count: 50, pages: 1, lastPageItems: 50 },
|
|
{ count: 51, pages: 2, lastPageItems: 1 },
|
|
{ count: 100, pages: 2, lastPageItems: 50 },
|
|
{ count: 101, pages: 3, lastPageItems: 1 },
|
|
{ count: 235, pages: 5, lastPageItems: 35 },
|
|
];
|
|
|
|
for (const expectation of expectations) {
|
|
const result = ListTools.paginateItems(devices(expectation.count), 999, 50);
|
|
assert.equal(result.totalPages, expectation.pages, `count ${expectation.count}`);
|
|
assert.equal(result.page, expectation.pages, `count ${expectation.count}`);
|
|
assert.equal(result.items.length, expectation.lastPageItems, `count ${expectation.count}`);
|
|
}
|
|
});
|