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:
@ -190,4 +190,66 @@ describe("WsLiveClient connection state + reconnection", () => {
|
||||
expect(h.ws.getConnectionState()).toBe("closed");
|
||||
expect(h.hasTimer()).toBe(false);
|
||||
});
|
||||
|
||||
it("disconnect() closes and stops reconnecting but stays reusable (F6 sign-out)", async () => {
|
||||
const h = harness();
|
||||
h.ws.setDomainEventHandler(() => {});
|
||||
await h.ws.ensureConnected();
|
||||
await vi.waitFor(() => expect(h.ws.getConnectionState()).toBe("connected"));
|
||||
|
||||
// Sign-out: soft teardown. State is closed, the reconnect loop is dropped, and
|
||||
// the detached socket's onclose cannot flip the state back to reconnecting.
|
||||
h.ws.disconnect();
|
||||
expect(h.ws.getConnectionState()).toBe("closed");
|
||||
expect(h.hasTimer()).toBe(false);
|
||||
expect(h.ws.needsReconnect()).toBe(false);
|
||||
|
||||
// Reusable: a later ensureConnected reconnects as a fresh socket (re-pairing).
|
||||
await h.ws.ensureConnected();
|
||||
await vi.waitFor(() => expect(h.sockets.length).toBe(2));
|
||||
await vi.waitFor(() => expect(h.ws.getConnectionState()).toBe("connected"));
|
||||
});
|
||||
|
||||
it("calls onReconnectFailed when a reconnect attempt fails (auth probe seam)", async () => {
|
||||
const failed = vi.fn();
|
||||
let openNextSocket = true;
|
||||
let timerFn: (() => void) | null = null;
|
||||
const sockets: FakeSocket[] = [];
|
||||
const ws = new WsLiveClient({
|
||||
wsUrl: "wss://host",
|
||||
reconnectBaseMs: 1,
|
||||
onReconnectFailed: failed,
|
||||
socketFactory: () => {
|
||||
const s = new FakeSocket();
|
||||
sockets.push(s);
|
||||
// First socket connects; the reconnect socket errors (upgrade rejected).
|
||||
if (openNextSocket) queueMicrotask(() => s.onopen?.());
|
||||
else queueMicrotask(() => s.onerror?.(new Error("upgrade rejected")));
|
||||
return s;
|
||||
},
|
||||
setTimeoutImpl: (fn) => {
|
||||
timerFn = fn;
|
||||
return 1;
|
||||
},
|
||||
clearTimeoutImpl: () => {
|
||||
timerFn = null;
|
||||
},
|
||||
});
|
||||
|
||||
ws.setDomainEventHandler(() => {});
|
||||
await ws.ensureConnected();
|
||||
await vi.waitFor(() => expect(ws.getConnectionState()).toBe("connected"));
|
||||
|
||||
// Drop the socket; the next connect attempt will fail the upgrade.
|
||||
openNextSocket = false;
|
||||
sockets[0].close();
|
||||
expect(ws.getConnectionState()).toBe("reconnecting");
|
||||
const fire = timerFn as (() => void) | null;
|
||||
timerFn = null;
|
||||
fire?.();
|
||||
|
||||
// The failed reconnect fires the probe hook and schedules another attempt.
|
||||
await vi.waitFor(() => expect(failed).toHaveBeenCalled());
|
||||
expect(timerFn).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user