carbon-tokenization / frontend /tests /refocus-sync.test.ts
tfrere's picture
tfrere HF Staff
test(editor): extract refocus re-sync into a unit-tested module
45d6656
/**
* Refocus re-sync guard tests.
*
* Exercises createRefocusSync across the four liveness cases it must
* distinguish on tab refocus, plus the offline grace release. Uses fake
* timers and a fake provider/editor - no network, no browser.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
createRefocusSync,
type RefocusProviderLike,
type RefocusEditorLike,
} from "../src/editor/refocus-sync";
interface Fakes {
provider: RefocusProviderLike & { emit: (event: string) => void };
editor: RefocusEditorLike & { setEditable: ReturnType<typeof vi.fn> };
socket: { status: string; webSocket: { readyState: number }; lastMessageReceived: number };
}
function makeFakes(opts: {
synced?: boolean;
status?: string;
readyState?: number;
/** Simulate a healthy socket: forceSync draws a server reply that advances lastMessageReceived. */
replyOnForceSync?: boolean;
} = {}): Fakes {
const listeners: Record<string, Array<(...a: any[]) => void>> = {};
const socket = {
status: opts.status ?? "connected",
webSocket: { readyState: opts.readyState ?? 1 },
lastMessageReceived: 0,
};
const provider: Fakes["provider"] = {
synced: opts.synced ?? true,
configuration: { websocketProvider: socket },
connect: vi.fn(() => Promise.resolve()),
forceSync: vi.fn(() => {
if (opts.replyOnForceSync) socket.lastMessageReceived += 1;
}),
on: (event, cb) => {
(listeners[event] ??= []).push(cb);
},
off: (event, cb) => {
listeners[event] = (listeners[event] ?? []).filter((f) => f !== cb);
},
emit: (event) => {
(listeners[event] ?? []).forEach((f) => f());
},
};
const editor: Fakes["editor"] = {
isEditable: true,
isDestroyed: false,
setEditable: vi.fn(function (this: unknown, editable: boolean) {
editor.isEditable = editable;
}),
};
return { provider, editor, socket };
}
function lastEditableArg(setEditable: ReturnType<typeof vi.fn>): boolean | undefined {
const call = setEditable.mock.calls.at(-1);
return call ? (call[0] as boolean) : undefined;
}
describe("createRefocusSync", () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it("healthy + synced socket: forces a sync but never locks", () => {
const { provider, editor } = makeFakes({ synced: true, replyOnForceSync: true });
const { onVisibility } = createRefocusSync({ provider, editor });
onVisibility();
expect(provider.connect).toHaveBeenCalled();
expect(provider.forceSync).toHaveBeenCalled();
expect(editor.setEditable).not.toHaveBeenCalled();
// Probe window passes; reply advanced lastMessageReceived -> still no lock.
vi.advanceTimersByTime(2000);
expect(editor.setEditable).not.toHaveBeenCalledWith(false);
});
it("disconnected socket: locks immediately, unlocks on synced", () => {
const { provider, editor } = makeFakes({ synced: false, status: "disconnected" });
const { onVisibility } = createRefocusSync({ provider, editor });
onVisibility();
expect(editor.setEditable).toHaveBeenCalledWith(false);
expect(editor.isEditable).toBe(false);
provider.emit("synced");
expect(lastEditableArg(editor.setEditable)).toBe(true);
expect(editor.isEditable).toBe(true);
});
it("closed readyState while status says connected: locks", () => {
const { provider, editor } = makeFakes({ synced: true, status: "connected", readyState: 3 });
const { onVisibility } = createRefocusSync({ provider, editor });
onVisibility();
expect(editor.setEditable).toHaveBeenCalledWith(false);
});
it("offline: never traps the user, releases after the grace window", () => {
const { provider, editor } = makeFakes({ synced: false });
createRefocusSync({ provider, editor }).onVisibility();
expect(editor.isEditable).toBe(false);
vi.advanceTimersByTime(4000);
expect(lastEditableArg(editor.setEditable)).toBe(true);
expect(editor.isEditable).toBe(true);
});
it("half-open socket: looks alive but no reply -> locks after probe", () => {
const { provider, editor } = makeFakes({
synced: true,
status: "connected",
readyState: 1,
replyOnForceSync: false,
});
const { onVisibility } = createRefocusSync({ provider, editor });
onVisibility();
// Looks alive, so no immediate lock.
expect(editor.setEditable).not.toHaveBeenCalledWith(false);
// No message landed within the probe window -> declare it stale.
vi.advanceTimersByTime(2000);
expect(editor.setEditable).toHaveBeenCalledWith(false);
expect(provider.connect).toHaveBeenCalledTimes(2); // initial + probe reconnect
});
it("dispose detaches the synced listener and releases the lock", () => {
const { provider, editor } = makeFakes({ synced: false });
const { onVisibility, dispose } = createRefocusSync({ provider, editor });
onVisibility();
expect(editor.isEditable).toBe(false);
dispose();
expect(editor.isEditable).toBe(true);
// A late synced event must not touch a disposed editor.
editor.setEditable.mockClear();
provider.emit("synced");
expect(editor.setEditable).not.toHaveBeenCalled();
});
});