Bindet die neuen AppView-Endpoints an: sechs IPC-Commands
(fetch_notifications, notification_count, mark_notifications_seen,
fetch_followers, fetch_following, fetch_thread) plus profile_get_by_did,
dazu die TS-Gegenstücke.
* NotificationsView: Liste mit Icon/Text je Art, Avatar, Vorschau des
subject_text, Cursor-Pagination; Klick auf eine Zeile mit Subject öffnet
den Thread. Beim Öffnen wird mit dem indexed_at der obersten Zeile als
Wasserzeichen quittiert.
* NavRail: Eintrag mit Unread-Badge, gepollt im vorhandenen 5s-Timer und
über den bestehenden Teardown-Pfad abgeräumt; der Poll pausiert, solange
die Liste offen ist.
* ProfileView: Follower- und Following-Zahlen sind jetzt Buttons und öffnen
die jeweilige Liste inline.
Navigiert wird über die DID, nicht über den Handle: für Actors, die weder
profiles noch posts kennen, liefert die AppView einen abgeschnittenen
Platzhalter im handle-Feld ("did:plc:abcd…"), und ein Klick darauf landete
in einem synthetischen Leerprofil. Die echte DID steht im DTO und wird jetzt
explizit durchgereicht — keine Heuristik auf das Platzhalter-Format.
Dabei aufgefallen und mitgefixt: ProfileView lud nur in onMount, obwohl die
Komponente bei Profil-zu-Profil-Navigation gemountet bleibt — Posts und
Zahlen des vorherigen Nutzers wären unter dem neuen Namen stehengeblieben.
Jetzt ein auf (did, handle) gekeyter Effekt.
Das Thread-Overlay hing im else-Zweig der leeren Timeline und wäre aus dem
Notifications-View unsichtbar gewesen; es ist jetzt ein Snippet, das beide
Views rendern.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
113 lines
4.2 KiB
TypeScript
113 lines
4.2 KiB
TypeScript
// 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<typeof mount> | 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");
|
|
});
|
|
});
|