summaryrefslogtreecommitdiffhomepage
path: root/src/app/store.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/store.test.ts')
-rw-r--r--src/app/store.test.ts220
1 files changed, 220 insertions, 0 deletions
diff --git a/src/app/store.test.ts b/src/app/store.test.ts
new file mode 100644
index 0000000..b521975
--- /dev/null
+++ b/src/app/store.test.ts
@@ -0,0 +1,220 @@
+import type { SurfaceServerMessage } from "@dispatch/ui-contract";
+import { describe, expect, it } from "vitest";
+import type { WebSocketLike } from "../adapters/ws";
+import { createAppStore } from "./store.svelte";
+
+interface FakeSocket extends WebSocketLike {
+ sent: string[];
+ resolveOpen(): void;
+ feedMessage(data: SurfaceServerMessage): void;
+}
+
+function fakeSocket(): FakeSocket {
+ let onopen: (() => void) | null = null;
+ let onmessage: ((ev: { data: string }) => void) | null = null;
+ const sent: string[] = [];
+
+ const ws: FakeSocket = {
+ send(data: string) {
+ sent.push(data);
+ },
+ close() {},
+ get onopen() {
+ return onopen;
+ },
+ set onopen(fn) {
+ onopen = fn;
+ },
+ get onmessage() {
+ return onmessage;
+ },
+ set onmessage(fn) {
+ onmessage = fn;
+ },
+ get onclose() {
+ return null;
+ },
+ set onclose(_fn) {},
+ resolveOpen() {
+ onopen?.();
+ },
+ feedMessage(msg: SurfaceServerMessage) {
+ onmessage?.({ data: JSON.stringify(msg) });
+ },
+ sent,
+ };
+ return ws;
+}
+
+describe("createAppStore", () => {
+ it("starts with empty catalog and no selection", () => {
+ const ws = fakeSocket();
+ const store = createAppStore({ socketFactory: () => ws });
+ ws.resolveOpen();
+
+ expect(store.catalog).toEqual([]);
+ expect(store.selectedId).toBeNull();
+ expect(store.selectedSpec).toBeNull();
+ expect(store.lastError).toBeNull();
+
+ store.dispose();
+ });
+
+ it("updates catalog when catalog message arrives", () => {
+ const ws = fakeSocket();
+ const store = createAppStore({ socketFactory: () => ws });
+ ws.resolveOpen();
+
+ ws.feedMessage({
+ type: "catalog",
+ catalog: [
+ { id: "s1", region: "sidebar", title: "Surface One" },
+ { id: "s2", region: "panel", title: "Surface Two" },
+ ],
+ });
+
+ expect(store.catalog).toHaveLength(2);
+ expect(store.catalog[0]?.id).toBe("s1");
+ expect(store.catalog[1]?.id).toBe("s2");
+
+ store.dispose();
+ });
+
+ it("select sends subscribe and sets selectedId", () => {
+ const ws = fakeSocket();
+ const store = createAppStore({ socketFactory: () => ws });
+ ws.resolveOpen();
+
+ ws.feedMessage({
+ type: "catalog",
+ catalog: [{ id: "s1", region: "sidebar", title: "Surface One" }],
+ });
+
+ ws.sent.length = 0;
+ store.select("s1");
+
+ expect(store.selectedId).toBe("s1");
+ const subscribeMsg = ws.sent.find((s) => {
+ const parsed = JSON.parse(s);
+ return parsed.type === "subscribe" && parsed.surfaceId === "s1";
+ });
+ expect(subscribeMsg).toBeTruthy();
+
+ store.dispose();
+ });
+
+ it("selecting a different surface unsubscribes from previous", () => {
+ const ws = fakeSocket();
+ const store = createAppStore({ socketFactory: () => ws });
+ ws.resolveOpen();
+
+ ws.feedMessage({
+ type: "catalog",
+ catalog: [
+ { id: "s1", region: "sidebar", title: "Surface One" },
+ { id: "s2", region: "panel", title: "Surface Two" },
+ ],
+ });
+
+ ws.sent.length = 0;
+ store.select("s1");
+ store.select("s2");
+
+ const unsubscribeMsg = ws.sent.find((s) => {
+ const parsed = JSON.parse(s);
+ return parsed.type === "unsubscribe" && parsed.surfaceId === "s1";
+ });
+ expect(unsubscribeMsg).toBeTruthy();
+
+ const subscribeMsg = ws.sent.find((s) => {
+ const parsed = JSON.parse(s);
+ return parsed.type === "subscribe" && parsed.surfaceId === "s2";
+ });
+ expect(subscribeMsg).toBeTruthy();
+
+ store.dispose();
+ });
+
+ it("surface message updates selectedSpec", () => {
+ const ws = fakeSocket();
+ const store = createAppStore({ socketFactory: () => ws });
+ ws.resolveOpen();
+
+ ws.feedMessage({
+ type: "catalog",
+ catalog: [{ id: "s1", region: "sidebar", title: "Surface One" }],
+ });
+
+ store.select("s1");
+
+ ws.feedMessage({
+ type: "surface",
+ spec: {
+ id: "s1",
+ region: "sidebar",
+ title: "Surface One",
+ fields: [{ kind: "stat", label: "Tokens", value: "1,234" }],
+ },
+ });
+
+ expect(store.selectedSpec).not.toBeNull();
+ expect(store.selectedSpec?.id).toBe("s1");
+ expect(store.selectedSpec?.fields).toHaveLength(1);
+
+ store.dispose();
+ });
+
+ it("invoke sends an invoke message", () => {
+ const ws = fakeSocket();
+ const store = createAppStore({ socketFactory: () => ws });
+ ws.resolveOpen();
+
+ ws.sent.length = 0;
+ store.invoke("s1", "toggle-dark", true);
+
+ const invokeMsg = ws.sent.find((s) => {
+ const parsed = JSON.parse(s);
+ return (
+ parsed.type === "invoke" &&
+ parsed.surfaceId === "s1" &&
+ parsed.actionId === "toggle-dark" &&
+ parsed.payload === true
+ );
+ });
+ expect(invokeMsg).toBeTruthy();
+
+ store.dispose();
+ });
+
+ it("error message updates lastError", () => {
+ const ws = fakeSocket();
+ const store = createAppStore({ socketFactory: () => ws });
+ ws.resolveOpen();
+
+ ws.feedMessage({
+ type: "error",
+ message: "Something went wrong",
+ });
+
+ expect(store.lastError).not.toBeNull();
+ expect(store.lastError?.message).toBe("Something went wrong");
+
+ store.dispose();
+ });
+
+ it("dispose closes the socket", () => {
+ const ws = fakeSocket();
+ const closeSpy = { called: false };
+ const origClose = ws.close.bind(ws);
+ ws.close = () => {
+ closeSpy.called = true;
+ origClose();
+ };
+
+ const store = createAppStore({ socketFactory: () => ws });
+ ws.resolveOpen();
+
+ store.dispose();
+ expect(closeSpy.called).toBe(true);
+ });
+});