Files
OwnCord/Client/tauri-client/tests/unit/server-banner.test.ts
T
Claude 45e51cca1c fix(client): review follow-ups for the connection-status batch
Fixes from an adversarial review of the previous commit:

- MainPage banner: sync the banner with the current store status at mount.
  The selector subscription baselines on the current value and only fires
  on change, so a MainPage mounted mid-outage (status already
  "reconnecting") would never show the banner — the whole retry cycle maps
  to the same 3-state value. The status→banner dispatch is extracted to
  ServerBanner.applyConnectionStatus and unit-tested.
- History-fetch failure is no longer silent when the channel already has
  rows (live broadcasts / optimistic sends): the inline error region only
  renders in an empty channel, so loadMessages now also raises a toast in
  that case.
- Composer disable reason distinguishes "Reconnecting…" from
  "Not connected" per the spec §3 table (it previously showed
  "Reconnecting…" while disconnected, contradicting the banner).
- The single-writer wiring is extracted to
  dispatcher.wireConnectionStatus(ws) and pinned by a test (it was
  previously an untestable main.ts module-scope line — deleting it would
  have failed zero tests).
- Docs honesty: messaging.md's transport-drop diagram arm now shows both
  codes (channel full → NETWORK, closed/not-open → OFFLINE) instead of
  claiming NETWORK for both; README §3's callout now explicitly lists the
  voice column ("frozen" during reconnect) as a remaining gap instead of
  implying the section is fully closed; the composer table documents both
  offline reasons.
- New pinning tests: SidebarArea passes ws to UserBar (the production-bug
  fix was previously unasserted), ServerBanner.showDisconnected,
  applyConnectionStatus mapping, ChannelController onRetryLoad /
  onRetry-resend / onDeleteDraft, composer reason per status, and the
  history-failure toast fallback.

Verified: tsc + full client unit suite (3234 tests) + oxlint/eslint +
prettier all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:42:36 +00:00

115 lines
3.5 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { createServerBanner, applyConnectionStatus } from "@components/ServerBanner";
describe("ServerBanner", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("creates element with reconnecting-banner class", () => {
const banner = createServerBanner();
expect(banner.element.classList.contains("reconnecting-banner")).toBe(true);
banner.destroy();
});
it("showRestart adds visible class and shows countdown text", () => {
const banner = createServerBanner();
banner.showRestart(5);
expect(banner.element.classList.contains("visible")).toBe(true);
expect(banner.element.textContent).toBe("Server restarting in 5 seconds...");
banner.destroy();
});
it('showReconnecting adds visible class with "Reconnecting..." text', () => {
const banner = createServerBanner();
banner.showReconnecting();
expect(banner.element.classList.contains("visible")).toBe(true);
expect(banner.element.textContent).toBe("Reconnecting...");
banner.destroy();
});
it("hide removes visible class", () => {
const banner = createServerBanner();
banner.showReconnecting();
expect(banner.element.classList.contains("visible")).toBe(true);
banner.hide();
expect(banner.element.classList.contains("visible")).toBe(false);
banner.destroy();
});
it('showDisconnected adds visible class with "Disconnected" text', () => {
const banner = createServerBanner();
banner.showDisconnected();
expect(banner.element.classList.contains("visible")).toBe(true);
expect(banner.element.textContent).toBe("Disconnected");
banner.destroy();
});
it("applyConnectionStatus maps each store status to the right banner state", () => {
const banner = createServerBanner();
applyConnectionStatus(banner, "reconnecting");
expect(banner.element.classList.contains("visible")).toBe(true);
expect(banner.element.textContent).toBe("Reconnecting...");
applyConnectionStatus(banner, "disconnected");
expect(banner.element.classList.contains("visible")).toBe(true);
expect(banner.element.textContent).toBe("Disconnected");
applyConnectionStatus(banner, "connected");
expect(banner.element.classList.contains("visible")).toBe(false);
banner.destroy();
});
it("countdown decrements every second", () => {
const banner = createServerBanner();
banner.showRestart(3);
expect(banner.element.textContent).toBe("Server restarting in 3 seconds...");
vi.advanceTimersByTime(1000);
expect(banner.element.textContent).toBe("Server restarting in 2 seconds...");
vi.advanceTimersByTime(1000);
expect(banner.element.textContent).toBe("Server restarting in 1 seconds...");
banner.destroy();
});
it('countdown transitions to "Reconnecting..." at 0', () => {
const banner = createServerBanner();
banner.showRestart(2);
vi.advanceTimersByTime(1000); // remaining = 1
vi.advanceTimersByTime(1000); // remaining = 0 → showReconnecting
expect(banner.element.textContent).toBe("Reconnecting...");
banner.destroy();
});
it("destroy removes element from DOM", () => {
const banner = createServerBanner();
const parent = document.createElement("div");
parent.appendChild(banner.element);
expect(parent.contains(banner.element)).toBe(true);
banner.destroy();
expect(parent.contains(banner.element)).toBe(false);
});
});