/// import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { refreshAnalytics, refreshOverview, renderPlaylistSortable, savePlaylist } from "../src/admin"; // Mock fetch globally let fetchCalls: { url: string; options?: RequestInit }[] = []; let mockFetchResponse: any = { ok: true, payload: {} }; beforeEach(() => { fetchCalls = []; globalThis.fetch = vi.fn(async (url: string, options?: RequestInit) => { fetchCalls.push({ url, options }); const payload = mockFetchResponse.payload ?? {}; return Promise.resolve( new Response(JSON.stringify(payload), { status: mockFetchResponse.ok ? 200 : mockFetchResponse.status || 500, headers: { "Content-Type": "application/json" }, }), ); }) as any; // Mock localStorage const storage = new Map(); Object.defineProperty(window, "localStorage", { value: { getItem: vi.fn((key: string) => storage.get(key) ?? null), setItem: vi.fn((key: string, val: string) => storage.set(key, val)), removeItem: vi.fn((key: string) => storage.delete(key)), }, writable: true, }); // Mock window.confirm/prompt vi.spyOn(window, "confirm").mockReturnValue(true); vi.spyOn(window, "prompt").mockReturnValue(null); vi.spyOn(console, "error").mockImplementation(() => {}); }); afterEach(() => { vi.restoreAllMocks(); }); // Import after mocks are set import "../src/admin"; describe("admin.ts initialization", () => { it("loads and exposes tab switching", () => { // After import, init() runs on DOMContentLoaded // Test that the document has admin page structure expect(document.body).toBeDefined(); }); }); describe("upload progress helpers", () => { it("renders upload progress correctly", async () => { // Set up DOM for upload page document.body.innerHTML = `
`; // Re-import triggers init again — but we need fresh module state // For now test internal helper access via body const wrap = document.getElementById("media-upload-progress-wrap")!; const bar = document.getElementById("media-upload-progress") as HTMLProgressElement; const text = document.getElementById("media-upload-progress-text")!; expect(wrap.hidden).toBe(true); // Simulate what setUploadProgress would do wrap.hidden = false; bar.max = 100; bar.value = 50; text.textContent = "Uploading (50%)"; expect(wrap.hidden).toBe(false); expect(bar.value).toBe(50); expect(text.textContent).toBe("Uploading (50%)"); }); }); describe("file fingerprint", () => { it("generates unique fingerprints for different files", () => { const f1 = new File(["a"], "video1.mp4", { lastModified: 1 }); const f2 = new File(["b"], "video2.mp4", { lastModified: 2 }); const fp1 = `${f1.name}:${f1.size}:${f1.lastModified}`; const fp2 = `${f2.name}:${f2.size}:${f2.lastModified}`; expect(fp1).not.toBe(fp2); expect(fp1).toBe("video1.mp4:1:1"); expect(fp2).toBe("video2.mp4:1:2"); }); }); describe("MAX_UPLOAD_BYTES constant", () => { it("equals 2GB", () => { expect(2 * 1024 * 1024 * 1024).toBe(2147483648); }); }); describe("API calls on tab load", () => { beforeEach(() => { document.body.innerHTML = `
`; }); it("can be imported without crashing", () => { // admin.ts initialization should not throw expect(true).toBe(true); }); }); describe("admin data refresh", () => { beforeEach(() => { document.body.innerHTML = `
`; }); it("renders overview stats from the admin overview API", async () => { mockFetchResponse = { ok: true, payload: { viewers: 12, storedMessages: 345, moderationActions: 9, mediaTotalRuntime: "01:23:45", mediaCount: 7, redisClients: 4, redisEnabled: true, redisMemoryUsed: "18 MB", databaseDriver: "sqlite", generatedAt: "2026-08-08T12:00:00Z", }, }; await refreshOverview(); expect(document.getElementById("stat-viewers")?.textContent).toBe("12"); expect(document.getElementById("stat-stored-messages")?.textContent).toBe("345"); expect(document.getElementById("system-redis-enabled")?.textContent).toBe("yes"); expect(document.getElementById("overview-status")?.textContent).toContain("2026-08-08T12:00:00Z"); }); it("renders analytics rows from the analytics API", async () => { mockFetchResponse = { ok: true, payload: { totalMessages: 1000, messagesLast24Hours: 75, distinctRooms: 3, moderationActions: 11, generatedAt: "2026-08-08T12:30:00Z", topRooms: [ { room: "main", messageCount: 55 }, { room: "lobby", messageCount: 20 }, ], }, }; await refreshAnalytics(); expect(document.getElementById("analytics-total-messages")?.textContent).toBe("1000"); expect(document.getElementById("analytics-messages-24h")?.textContent).toBe("75"); expect(document.getElementById("analytics-table-body")?.textContent).toContain("main"); expect(document.getElementById("analytics-table-body")?.textContent).toContain("lobby"); }); }); describe("playlist management", () => { beforeEach(() => { document.body.innerHTML = `
`; }); it("saves playlist via PUT request", async () => { mockFetchResponse = { ok: true, payload: {} }; await savePlaylist(); const putCall = fetchCalls.find((c) => c.options?.method === "PUT"); expect(putCall).toBeDefined(); expect(putCall?.url).toBe("/api/admin/playlist"); expect(putCall?.options?.headers).toEqual({ "Content-Type": "application/json" }); const body = JSON.parse(putCall?.options?.body as string); expect(body.content).toContain("demo1.mp4"); }); it("sets error status on save failure", async () => { mockFetchResponse = { ok: false, status: 500, payload: {} }; await savePlaylist(); const status = document.getElementById("playlist-status"); expect(status?.textContent).toContain("Failed"); }); it("renders sortable playlist with indexes and paths", () => { renderPlaylistSortable("file '/app/media/demo1.mp4'\nfile '/app/media/demo2.mp4'\n"); const list = document.getElementById("playlist-sortable")!; expect(list.children).toHaveLength(2); expect(list.innerHTML).toContain("demo1.mp4"); expect(list.innerHTML).toContain("demo2.mp4"); expect(list.querySelector(".sortable-index")?.textContent).toBe("1"); expect(list.querySelector("[data-remove]")).toBeTruthy(); }); it("skips comment and blank lines in sortable", () => { renderPlaylistSortable("# comment\n\nfile '/app/media/demo1.mp4'\n"); const list = document.getElementById("playlist-sortable")!; expect(list.children).toHaveLength(1); expect(list.innerHTML).toContain("demo1.mp4"); }); }); describe("admin accessibility", () => { beforeEach(() => { document.body.innerHTML = `
`; }); it("nav has aria-label for screen readers", () => { const nav = document.querySelector(".admin-nav"); expect(nav?.getAttribute("aria-label")).toBe("Admin navigation"); }); it("active tab has aria-current attribute", () => { const active = document.querySelector("[aria-current]"); expect(active).toBeTruthy(); expect(active?.getAttribute("aria-current")).toBe("page"); }); it("all nav buttons are focusable", () => { const buttons = document.querySelectorAll(".admin-nav button"); buttons.forEach((btn) => { expect(btn.getAttribute("tabindex")).toBeNull(); expect(btn.getAttribute("disabled")).toBeNull(); }); }); }); describe("admin responsive layout", () => { beforeEach(() => { document.body.innerHTML = `
Overview data
`; }); it("tab content is present and visible", () => { const tabContent = document.getElementById("tab-content"); expect(tabContent).toBeTruthy(); const style = window.getComputedStyle(tabContent!); expect(style.display).not.toBe("none"); }); it("admin nav contains essential tabs", () => { const buttons = document.querySelectorAll(".admin-nav button"); const tabs = Array.from(buttons).map((b) => b.textContent?.trim()); expect(tabs).toContain("Dashboard"); }); }); describe("admin data refresh failures", () => { beforeEach(() => { document.body.innerHTML = `
`; }); it("sets error status when overview fails", async () => { mockFetchResponse = { ok: false, status: 500, payload: {} }; await refreshOverview(); expect(document.getElementById("overview-status")?.textContent).toContain("Failed"); }); it("sets error status when analytics fails", async () => { mockFetchResponse = { ok: false, status: 500, payload: {} }; await refreshAnalytics(); expect(document.getElementById("analytics-status")?.textContent).toContain("Failed"); }); }); describe("playlist status badges", () => { beforeEach(() => { document.body.innerHTML = ``; }); it("renders badges for every media status", () => { const statusMap = new Map([ ["/app/media/ready.mp4", "ready"], ["/app/media/needs.mp4", "needs_normalization"], ["/app/media/normalizing.mp4", "normalizing"], ["/app/media/failed.mp4", "normalization_failed"], ["/app/media/other.mp4", "weird"], ]); renderPlaylistSortable( "file '/app/media/ready.mp4'\nfile '/app/media/needs.mp4'\nfile '/app/media/normalizing.mp4'\nfile '/app/media/failed.mp4'\nfile '/app/media/other.mp4'\nfile '/app/media/nopath.mp4'\n", statusMap, ); const list = document.getElementById("playlist-sortable")!; expect(list.innerHTML).toContain("badge-ok"); expect(list.innerHTML).toContain("badge-warn"); expect(list.innerHTML).toContain("badge-info"); expect(list.innerHTML).toContain("badge-danger"); expect(list.innerHTML).toContain("badge-muted"); // default + unknown fall back to muted expect(list.children).toHaveLength(6); }); });