test(frontend): expand admin and viewer test coverage for auth, typing, reconnect, and playlist badges
This commit is contained in:
1 parent
68a4d35334
commit
02ff53229c
3 files changed
+209
-1
No files matched your search
@@ -326,3 +326,53 @@ describe("admin responsive layout", () => {
|
|||||||
expect(tabs).toContain("Dashboard");
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
@@ -1,6 +1,12 @@
|
|||||||
/// <reference types="vitest" />
|
/// <reference types="vitest" />
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "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 = {
|
const hlsInstance = {
|
||||||
loadSource: vi.fn(),
|
loadSource: vi.fn(),
|
||||||
@@ -114,6 +120,22 @@ beforeEach(() => {
|
|||||||
<div id="reconnecting-overlay" role="alert" aria-live="assertive" hidden>
|
<div id="reconnecting-overlay" role="alert" aria-live="assertive" hidden>
|
||||||
<span id="reconnecting-text">Reconnecting...</span>
|
<span id="reconnecting-text">Reconnecting...</span>
|
||||||
</div>
|
</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
|
// 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", () => {
|
describe("player error recovery", () => {
|
||||||
it("shows reconnecting overlay on fatal network error", async () => {
|
it("shows reconnecting overlay on fatal network error", async () => {
|
||||||
const main = await import("../src/main");
|
const main = await import("../src/main");
|
||||||
|
|||||||
Reference in new issue
Block a user