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