test(frontend): expand admin and viewer test coverage for auth, typing, reconnect, and playlist badges

This commit is contained in:
zwitschi committed 2026-08-13 13:22:08 +02:00
1 parent 68a4d35334
commit 02ff53229c
3 files changed
+209 -1

No files matched your search

+50
View File
@@ -326,3 +326,53 @@ describe("admin responsive layout", () => {
expect(tabs).toContain("Dashboard");
});
});
describe("admin data refresh failures", () => {
beforeEach(() => {
document.body.innerHTML = `
<div id="overview-status"></div>
<div id="analytics-status"></div>
`;
});
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 = `<ul id="playlist-sortable"></ul>`;
});
it("renders badges for every media status", () => {
const statusMap = new Map<string, string>([
["/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);
});
});
+23
View File
@@ -0,0 +1,23 @@
/// <reference types="vitest" />
import { describe, it, expect } from "vitest";
import { setupCounter } from "../src/counter";
describe("setupCounter", () => {
it("initializes the button with count 0", () => {
const btn = document.createElement("button");
setupCounter(btn);
expect(btn.innerHTML).toBe("Count is 0");
});
it("increments the counter on each click", () => {
const btn = document.createElement("button");
setupCounter(btn);
btn.click();
expect(btn.innerHTML).toBe("Count is 1");
btn.click();
btn.click();
expect(btn.innerHTML).toBe("Count is 3");
});
});
+136 -1
View File
@@ -1,6 +1,12 @@
/// <reference types="vitest" />
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { resetViewerStateForTests, resolveManifestUrl, resolveWebSocketBaseUrl } from "../src/main";
import {
resetViewerStateForTests,
resolveManifestUrl,
resolveWebSocketBaseUrl,
showReconnecting,
hideReconnecting,
} from "../src/main";
const hlsInstance = {
loadSource: vi.fn(),
@@ -114,6 +120,22 @@ beforeEach(() => {
<div id="reconnecting-overlay" role="alert" aria-live="assertive" hidden>
<span id="reconnecting-text">Reconnecting...</span>
</div>
<div id="chat-user-label" hidden></div>
<button id="auth-toggle-btn" type="button">Sign in</button>
<button id="auth-logout-btn" type="button" hidden>Logout</button>
<div id="auth-panel" hidden>
<input id="auth-username" type="text" />
<input id="auth-password" type="password" />
<span id="auth-status"></span>
<button id="auth-login-btn" type="button">Login</button>
<button id="auth-register-btn" type="button">Register</button>
</div>
<button id="users-toggle-btn" type="button" aria-expanded="true">Users</button>
<div id="typing-indicator" hidden></div>
<div id="dm-search-row" hidden>
<input id="dm-search-input" type="text" />
<button id="dm-search-btn" type="button">Search</button>
</div>
`;
// Stub prompt for DM
@@ -359,6 +381,119 @@ describe("message rendering", () => {
});
});
describe("reconnecting overlay", () => {
it("shows and hides the overlay with a custom message", () => {
showReconnecting("Custom message");
const overlay = document.getElementById("reconnecting-overlay")!;
expect(overlay.classList.contains("hidden")).toBe(false);
expect(document.getElementById("reconnecting-text")!.textContent).toBe("Custom message");
hideReconnecting();
expect(overlay.classList.contains("hidden")).toBe(true);
});
});
describe("typing indicator", () => {
it("shows typing on a typing event and clears on the next message", async () => {
await import("../src/main").then((mod) => mod.initPlayerAndChat(async () => ({ default: HlsMock }) as any));
const ws = websocketInstances[0];
ws.onmessage?.({ data: JSON.stringify({ type: "typing", username: "alice" }) });
const indicator = document.getElementById("typing-indicator")!;
expect(indicator.textContent).toBe("alice is typing...");
expect(indicator.hidden).toBe(false);
ws.onmessage?.({ data: JSON.stringify({ type: "public", text: "hi", username: "alice", timestamp: Date.now() }) });
expect(indicator.hidden).toBe(true);
expect(indicator.textContent).toBe("");
});
it("ignores typing events from the current user", async () => {
await import("../src/main").then((mod) => mod.initPlayerAndChat(async () => ({ default: HlsMock }) as any));
const ws = websocketInstances[0];
ws.onmessage?.({ data: JSON.stringify({ type: "typing", username: "viewer" }) });
const indicator = document.getElementById("typing-indicator")!;
expect(indicator.textContent).toBe("");
expect(indicator.hidden).toBe(true);
});
});
describe("sendMessage over websocket", () => {
it("sends a JSON message and clears the input", async () => {
await import("../src/main").then((mod) => mod.initPlayerAndChat(async () => ({ default: HlsMock }) as any));
const input = document.getElementById("message-input") as HTMLInputElement;
input.value = "hello world";
window.sendMessage();
const ws = websocketInstances[0];
expect(ws.send).toHaveBeenCalledTimes(1);
expect(ws.send).toHaveBeenCalledWith(expect.stringContaining("hello world"));
expect(input.value).toBe("");
});
});
describe("auth and user list wiring", () => {
it("logs in and updates the auth UI", async () => {
await import("../src/main");
globalThis.fetch = vi.fn(async (url: any, opts?: any) => {
const u = String(url);
if (u.includes("/api/auth/login")) {
return {
ok: true,
status: 200,
json: async () => ({ username: "alice", authenticated: true }),
text: async () => "",
};
}
return { ok: true, status: 200, json: async () => ({ authenticated: false }), text: async () => "" };
}) as any;
document.dispatchEvent(new Event("DOMContentLoaded"));
const usernameInput = document.getElementById("auth-username") as HTMLInputElement;
const passwordInput = document.getElementById("auth-password") as HTMLInputElement;
usernameInput.value = "alice";
passwordInput.value = "secret";
document.getElementById("auth-login-btn")!.click();
await new Promise((r) => setTimeout(r, 0));
expect(document.getElementById("auth-status")!.textContent).toContain("Signed in as alice");
expect(document.getElementById("chat-user-label")!.textContent).toBe("alice");
});
it("toggles the user list visibility", async () => {
await import("../src/main");
globalThis.fetch = vi
.fn()
.mockResolvedValue({
ok: true,
json: async () => ({ authenticated: false, entries: [] }),
text: async () => "",
}) as any;
document.dispatchEvent(new Event("DOMContentLoaded"));
const panel = document.getElementById("presence-panel")!;
const btn = document.getElementById("users-toggle-btn")!;
expect(panel.hidden).toBe(false);
btn.click();
expect(panel.hidden).toBe(true);
expect(btn.getAttribute("aria-expanded")).toBe("false");
btn.click();
expect(panel.hidden).toBe(false);
expect(btn.getAttribute("aria-expanded")).toBe("true");
});
});
describe("player error recovery", () => {
it("shows reconnecting overlay on fatal network error", async () => {
const main = await import("../src/main");