Files
mediamtx/tests/admin_list_tools_test.js
QiuSW 9068712bb1
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
feat(admin): add complete device pagination (#6)
2026-08-28 16:48:17 +08:00

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}`);
}
});