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