feat(frontend): polish web (logout, 401→pairing, reconnexion globale) (#13)

Flow logout : POST /api/logout puis retour à l'écran de pairing et
déconnexion du live. 401 renvoie au pairing sans boucle. Bannière de
reconnexion couvrant le cas live-only et le serveur indisponible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 08:23:11 +02:00
parent d538808a0d
commit 6117177957
10 changed files with 288 additions and 14 deletions

View File

@ -90,3 +90,32 @@ describe("WebSession unauthorized handling", () => {
expect(session.isPaired()).toBe(false);
});
});
describe("WebSession logout", () => {
it("POSTs /api/logout same-origin and clears the flag on success", async () => {
const store = memStore();
const { fetchImpl, calls } = fetchReturning(200, { revoked: true });
const session = new WebSession({ baseUrl: "https://host/", fetchImpl, store });
session.markPaired();
await session.logout();
expect(session.isPaired()).toBe(false);
expect(calls[0].url).toBe("https://host/api/logout");
const init = calls[0].init as { method: string; credentials: string };
expect(init.method).toBe("POST");
expect(init.credentials).toBe("same-origin");
});
it("still clears the flag when the server is unreachable (best-effort)", async () => {
const store = memStore();
const fetchImpl: FetchLike = async () => {
throw new Error("network down");
};
const session = new WebSession({ baseUrl: "https://host", fetchImpl, store });
session.markPaired();
await expect(session.logout()).resolves.toBeUndefined();
expect(session.isPaired()).toBe(false);
});
});