// End-to-end NavRail test: verify that clicks flip the parent's view // state AND the active button class follows. We observe the DOM — // that's what the running Tauri webview shows, and it's what the user // sees. // // Pattern: `let view = $state("home")` in the harness, then the // `on_select` callback updates it (same shape as App.svelte). // // The rail order is: home, notifications, compose, profile, search, // settings. The indices below follow that order — if you reorder the // `items` array in NavRail.svelte, update them here too. import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { mount, unmount, tick } from "svelte"; import NavRailHarness from "./NavRailHarness.svelte"; let target: HTMLDivElement; let app: ReturnType | null = null; beforeEach(() => { target = document.createElement("div"); document.body.appendChild(target); }); afterEach(() => { if (app) unmount(app); target.remove(); }); async function mountHarness(props: { unread?: number } = {}) { app = mount(NavRailHarness, { target, props }); await tick(); } describe("NavRail (callback-prop pattern)", () => { it("renders 6 buttons with home active", async () => { await mountHarness(); const btns = target.querySelectorAll("button.rail__btn"); expect(btns.length).toBe(6); expect(btns[0].classList.contains("active")).toBe(true); expect(target.querySelector('[data-testid="view-value"]')?.textContent).toBe("home"); }); it("flips view + active class on click", async () => { await mountHarness(); const btns = target.querySelectorAll("button.rail__btn"); btns[4].dispatchEvent(new MouseEvent("click", { bubbles: true })); await tick(); expect(target.querySelector('[data-testid="view-value"]')?.textContent).toBe("search"); expect(btns[4].classList.contains("active")).toBe(true); expect(btns[0].classList.contains("active")).toBe(false); btns[2].dispatchEvent(new MouseEvent("click", { bubbles: true })); await tick(); expect(target.querySelector('[data-testid="view-value"]')?.textContent).toBe("compose"); expect(btns[2].classList.contains("active")).toBe(true); btns[3].dispatchEvent(new MouseEvent("click", { bubbles: true })); await tick(); expect(target.querySelector('[data-testid="view-value"]')?.textContent).toBe("profile"); expect(btns[3].classList.contains("active")).toBe(true); btns[5].dispatchEvent(new MouseEvent("click", { bubbles: true })); await tick(); expect(target.querySelector('[data-testid="view-value"]')?.textContent).toBe("settings"); expect(btns[5].classList.contains("active")).toBe(true); }); it("routes to the notifications view", async () => { await mountHarness(); const btns = target.querySelectorAll("button.rail__btn"); btns[1].dispatchEvent(new MouseEvent("click", { bubbles: true })); await tick(); expect(target.querySelector('[data-testid="view-value"]')?.textContent).toBe( "notifications", ); expect(btns[1].classList.contains("active")).toBe(true); }); }); describe("NavRail unread badge", () => { it("renders no badge at zero unread", async () => { await mountHarness({ unread: 0 }); expect(target.querySelector('[data-testid="notif-badge"]')).toBeNull(); }); it("renders the count on the notifications button", async () => { await mountHarness({ unread: 3 }); const badge = target.querySelector('[data-testid="notif-badge"]'); expect(badge).not.toBeNull(); expect(badge?.textContent?.trim()).toBe("3"); // The badge must sit on the notifications entry, not on some // other rail button. const btns = target.querySelectorAll("button.rail__btn"); expect(btns[1].contains(badge!)).toBe(true); }); it("caps a large backlog at 99+ so the 88px rail can't widen", async () => { await mountHarness({ unread: 1234 }); const badge = target.querySelector('[data-testid="notif-badge"]'); expect(badge?.textContent?.trim()).toBe("99+"); }); it("keeps 99 unabbreviated (the cap is exclusive)", async () => { await mountHarness({ unread: 99 }); expect( target.querySelector('[data-testid="notif-badge"]')?.textContent?.trim(), ).toBe("99"); }); });