90 lines
3.5 KiB
TypeScript
90 lines
3.5 KiB
TypeScript
/**
|
|
* Klient-adaptern (kernel-transport över host-API): offline = no-op + varning
|
|
* (INGEN fetch, inget tyst förlorat), 409 -> ConflictError med aktuell
|
|
* version, presence via kernelns upsertPresence.
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
OfflineError,
|
|
OrderNotesClient,
|
|
OrderNotesTransport,
|
|
} from "@/lib/order-notes-client";
|
|
|
|
describe("OrderNotesTransport (kernel CollaborationTransport)", () => {
|
|
it("send() i offline-läge kastar OfflineError utan nätverksanrop (no-op, ej tyst)", async () => {
|
|
const t = new OrderNotesTransport("admin-simon");
|
|
t.setOffline(true);
|
|
await expect(
|
|
t.send({ id: "n1", actorId: "x", clock: 1, type: "note.update", payload: { body: "y" } }),
|
|
).rejects.toBeInstanceOf(OfflineError);
|
|
});
|
|
|
|
it("connect/disconnect växlar presence via upsertPresence (online -> offline)", async () => {
|
|
const t = new OrderNotesTransport("admin-simon");
|
|
await t.connect();
|
|
expect(t.getPresence().find((p) => p.participantId === "admin-simon")?.status).toBe("online");
|
|
await t.disconnect();
|
|
expect(t.getPresence().find((p) => p.participantId === "admin-simon")?.status).toBe("offline");
|
|
});
|
|
|
|
it("observeRemote advanserar Lamport-klockan och notifyar listeners", async () => {
|
|
const t = new OrderNotesTransport("me");
|
|
const seen: string[] = [];
|
|
t.onOperation((op) => seen.push(`${op.id}@${op.clock}`));
|
|
t.observeRemote({ id: "n1", actorId: "anna", clock: 4, type: "note.upsert", payload: { body: "x" } });
|
|
expect(seen).toEqual(["n1@4"]);
|
|
expect(t.getPresence().find((p) => p.participantId === "anna")?.status).toBe("online");
|
|
});
|
|
});
|
|
|
|
describe("OrderNotesClient över host-API", () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("offline-läge: add() är no-op — fetch anropas EJ, OfflineError kastas (ingen tyst förlust)", async () => {
|
|
const fetchSpy = vi.spyOn(globalThis, "fetch");
|
|
const client = new OrderNotesClient("admin-simon");
|
|
client.transport.setOffline(true);
|
|
await expect(client.add("order-1", "utkast")).rejects.toBeInstanceOf(OfflineError);
|
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("409 från servern -> ConflictError med serverns aktuella version", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({ error: "version_conflict", current_version: 3 }), { status: 409 }),
|
|
);
|
|
const client = new OrderNotesClient("admin-simon");
|
|
await expect(
|
|
client.update(
|
|
{ note_id: "n1", order_id: "o1", author_id: "a", body: "old", version: 1, clock: 1, updated_at: "" },
|
|
"new",
|
|
1,
|
|
),
|
|
).rejects.toMatchObject({ currentVersion: 3 });
|
|
});
|
|
|
|
it("load(): varje servernot replays som kernel-operation (klockor advanserar)", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(
|
|
JSON.stringify({
|
|
notes: [
|
|
{ note_id: "n1", order_id: "o1", author_id: "anna", body: "A", version: 1, clock: 2, updated_at: "" },
|
|
{ note_id: "n2", order_id: "o1", author_id: "bosse", body: "B", version: 2, clock: 5, updated_at: "" },
|
|
],
|
|
}),
|
|
{ status: 200 },
|
|
),
|
|
);
|
|
const client = new OrderNotesClient("admin-simon");
|
|
const { notes, versions } = await client.load("o1");
|
|
expect(notes).toHaveLength(2);
|
|
expect(versions.get("n2")).toBe(2);
|
|
expect(client.transport.getPresence().find((p) => p.participantId === "bosse")).toBeTruthy();
|
|
});
|
|
});
|