Files
OwnCord/Client/tauri-client/tests/unit/volume-menu.test.ts
T
Claude 0918f859a0 test: close measured test-coverage gaps across server, client and Rust
Audits what actually has tests, then closes the gaps it found. Full write-up
with before/after numbers in docs/audit-test-coverage-2026-07-25.md.

Measurement first: `go test ./... -coverprofile` (what CI runs) instruments
each package only for itself, so code exercised through another package's
tests reads as uncovered — `service` reported 36.7% against a real 85%. All
analysis here uses -coverpkg=./..., and both views now have Makefile targets.

Features that had zero coverage at every layer:
- user blocking (db + service + the /api/v1/blocks routes)
- auth lockout persistence — the DB round-trip that survives a restart
- plugin install/enable/disable/uninstall and the plugin KV namespace
- event replay bounds (GetMaxEventSeq, PruneEventsOlderThan)
- LiveKit participant_joined webhook (replayed-token guard), the room-service
  client, and proxyWebSocket/copyWS
- ws_proxy.rs and livekit_proxy.rs — pure helpers extracted, matching the
  existing tofu.rs pattern, so cert-pin and header-injection checks are testable

Gaps that were hidden rather than absent:
- Server/admin reported 0.3% coverage with 307 tests passing. TestSpawnDetached_*
  re-execs the test binary; the child inherited GOCOVERDIR and the parent's
  stdout, clobbering the profile and printing "[no tests to run]". Now 71.4%,
  and CI's uploaded artifact is correct.
- vitest.config.ts excluded 2.2k LOC unexplained, including two files that
  already had tests. Trimmed to three entries, each justified inline.
- api.HandleLiveKitHealthForTest re-implemented the handler it claimed to
  expose, so eight call sites tested a copy. Added a hook to the real one.

Two bugs found and pinned rather than silently patched: logctx.WithGroup nests
req_id under the group, and drag-reorder.ts takes one listener ref per channel
but releases one per sidebar, so the count never reaches zero.

Coverage: client 92.93% -> 94.87% statements (3371 -> 3572 tests) even after
un-excluding hidden files; Rust 47 -> 74 tests; Go zero-coverage functions
~70 -> 21, with plugin 61->77%, admin 67->86%, db 76->84%, service 85->91%.

Verified: go vet, all four build-tag variants, go test -race, -tags deadlock,
vitest --coverage, cargo test --lib, cargo clippy --all-targets, playwright.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEETs3Vh6sAHHb1jMBL75g
2026-07-25 14:47:21 +00:00

242 lines
8.6 KiB
TypeScript

/**
* Tests for src/components/channel-sidebar/volume-menu.ts (was 77.7% statements
* / 50% functions, no test file — every event handler was unexercised).
*
* The menu is transient DOM appended to document.body with two AbortControllers
* governing its teardown, so the interesting failures are leaks: a menu that
* outlives its sidebar, or a dismiss listener that survives its menu.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const setUserVolume = vi.fn();
const getUserVolume = vi.fn();
vi.mock("@lib/livekitSession", () => ({
setUserVolume: (...args: unknown[]) => setUserVolume(...args) as unknown,
getUserVolume: (...args: unknown[]) => getUserVolume(...args) as unknown,
}));
const { showUserVolumeMenu } = await import("@components/channel-sidebar/volume-menu");
function menuEl(): HTMLElement | null {
return document.querySelector(".user-vol-menu");
}
function sliderEl(): HTMLInputElement | null {
return document.querySelector<HTMLInputElement>(".user-vol-menu input[type=range]");
}
function itemTexts(): string[] {
return [...document.querySelectorAll(".user-vol-menu .context-menu-item")].map(
(el) => el.textContent ?? "",
);
}
beforeEach(() => {
vi.useFakeTimers();
setUserVolume.mockReset();
getUserVolume.mockReset().mockReturnValue(100);
document.body.innerHTML = "";
});
afterEach(() => {
vi.useRealTimers();
document.body.innerHTML = "";
});
// ── rendering ──────────────────────────────────────────────────────────────
describe("showUserVolumeMenu rendering", () => {
it("renders the username, the current volume and a 0-200 slider", () => {
getUserVolume.mockReturnValue(140);
showUserVolumeMenu(7, "alice", 10, 20, new AbortController().signal);
expect(menuEl()).not.toBeNull();
expect(itemTexts()).toContain("alice");
expect(itemTexts()).toContain("User Volume: 140%");
const slider = sliderEl();
expect(slider?.value).toBe("140");
expect(slider?.min).toBe("0");
expect(slider?.max).toBe("200");
});
it("positions the menu at the supplied coordinates", () => {
showUserVolumeMenu(7, "alice", 123, 456, new AbortController().signal);
expect(menuEl()?.style.left).toBe("123px");
expect(menuEl()?.style.top).toBe("456px");
});
it("reads the current volume for the requested user", () => {
showUserVolumeMenu(42, "bob", 0, 0, new AbortController().signal);
expect(getUserVolume).toHaveBeenCalledWith(42);
});
it("renders a volume of 0 rather than treating it as absent", () => {
getUserVolume.mockReturnValue(0);
showUserVolumeMenu(7, "alice", 0, 0, new AbortController().signal);
expect(itemTexts()).toContain("User Volume: 0%");
expect(sliderEl()?.value).toBe("0");
});
});
// ── slider ─────────────────────────────────────────────────────────────────
describe("volume slider", () => {
it("applies the new volume and updates both labels", () => {
showUserVolumeMenu(7, "alice", 0, 0, new AbortController().signal);
const slider = sliderEl();
if (slider === null) throw new Error("no slider rendered");
slider.value = "55";
slider.dispatchEvent(new Event("input", { bubbles: true }));
expect(setUserVolume).toHaveBeenCalledWith(7, 55);
expect(itemTexts()).toContain("User Volume: 55%");
expect(document.querySelector(".slider-val")?.textContent).toBe("55%");
});
it("supports boosting above 100%", () => {
showUserVolumeMenu(7, "alice", 0, 0, new AbortController().signal);
const slider = sliderEl();
if (slider === null) throw new Error("no slider rendered");
slider.value = "200";
slider.dispatchEvent(new Event("input", { bubbles: true }));
expect(setUserVolume).toHaveBeenCalledWith(7, 200);
});
it("supports muting to 0%", () => {
showUserVolumeMenu(7, "alice", 0, 0, new AbortController().signal);
const slider = sliderEl();
if (slider === null) throw new Error("no slider rendered");
slider.value = "0";
slider.dispatchEvent(new Event("input", { bubbles: true }));
expect(setUserVolume).toHaveBeenCalledWith(7, 0);
expect(itemTexts()).toContain("User Volume: 0%");
});
});
// ── reset ──────────────────────────────────────────────────────────────────
describe("reset button", () => {
it("restores 100% in the store, the slider and both labels", () => {
getUserVolume.mockReturnValue(30);
showUserVolumeMenu(7, "alice", 0, 0, new AbortController().signal);
const reset = [
...document.querySelectorAll<HTMLElement>(".user-vol-menu .context-menu-item"),
].find((el) => el.textContent === "Reset Volume");
reset?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(setUserVolume).toHaveBeenCalledWith(7, 100);
expect(sliderEl()?.value).toBe("100");
expect(itemTexts()).toContain("User Volume: 100%");
expect(document.querySelector(".slider-val")?.textContent).toBe("100%");
});
});
// ── dismissal and teardown ─────────────────────────────────────────────────
describe("dismissal", () => {
it("closes on a mousedown outside the menu", () => {
showUserVolumeMenu(7, "alice", 0, 0, new AbortController().signal);
vi.runAllTimers(); // the outside-click listener is attached on a macrotask
document.body.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
expect(menuEl()).toBeNull();
});
it("stays open on a mousedown inside the menu", () => {
showUserVolumeMenu(7, "alice", 0, 0, new AbortController().signal);
vi.runAllTimers();
sliderEl()?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
expect(menuEl()).not.toBeNull();
});
it("ignores clicks landing before the listener is attached", () => {
showUserVolumeMenu(7, "alice", 0, 0, new AbortController().signal);
// The setTimeout(0) exists so the right-click that opened the menu does not
// immediately close it again.
document.body.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
expect(menuEl()).not.toBeNull();
});
it("removes the menu when the parent component aborts", () => {
const ac = new AbortController();
showUserVolumeMenu(7, "alice", 0, 0, ac.signal);
vi.runAllTimers();
ac.abort();
expect(menuEl()).toBeNull();
});
it("does not re-attach the dismiss listener when aborted before the timer fires", () => {
const ac = new AbortController();
showUserVolumeMenu(7, "alice", 0, 0, ac.signal);
ac.abort();
// The scheduled callback checks the dismiss signal first, so no listener is
// registered against an already-removed menu.
expect(() => {
vi.runAllTimers();
}).not.toThrow();
expect(menuEl()).toBeNull();
});
});
describe("re-opening", () => {
it("replaces any menu already on screen", () => {
showUserVolumeMenu(7, "alice", 0, 0, new AbortController().signal);
vi.runAllTimers();
showUserVolumeMenu(9, "bob", 50, 60, new AbortController().signal);
expect(document.querySelectorAll(".user-vol-menu")).toHaveLength(1);
expect(itemTexts()).toContain("bob");
expect(itemTexts()).not.toContain("alice");
});
it("the replaced menu's dismiss listener no longer closes the new menu", () => {
showUserVolumeMenu(7, "alice", 0, 0, new AbortController().signal);
vi.runAllTimers(); // first menu's outside-click listener is live
showUserVolumeMenu(9, "bob", 0, 0, new AbortController().signal);
// Deliberately do NOT run timers: only the stale listener from the first
// menu is attached. It was aborted on replace, so this click must not close
// the freshly opened menu.
document.body.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
expect(menuEl()).not.toBeNull();
expect(itemTexts()).toContain("bob");
});
it("the new menu operates on the new user", () => {
showUserVolumeMenu(7, "alice", 0, 0, new AbortController().signal);
showUserVolumeMenu(9, "bob", 0, 0, new AbortController().signal);
const slider = sliderEl();
if (slider === null) throw new Error("no slider rendered");
slider.value = "20";
slider.dispatchEvent(new Event("input", { bubbles: true }));
expect(setUserVolume).toHaveBeenCalledWith(9, 20);
expect(setUserVolume).not.toHaveBeenCalledWith(7, 20);
});
});