summaryrefslogtreecommitdiffhomepage
path: root/src/app/store.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-10 16:29:01 +0900
committerAdam Malczewski <[email protected]>2026-06-10 16:29:01 +0900
commit871957b930203c019e631c4606cfdf8266d222fa (patch)
tree50c522018c3ce4127ffa76f4b3b6c7843e90db43 /src/app/store.test.ts
parent7b345f132763fa6405ae858b74e46229629c19d9 (diff)
downloaddispatch-web-871957b930203c019e631c4606cfdf8266d222fa.tar.gz
dispatch-web-871957b930203c019e631c4606cfdf8266d222fa.zip
feat(views,surface-host): Extensions sidebar view — auto-expanded surfaces + tables
views (new feature): - pure panel-stack reducer + thin generic ViewSidebar (dropdown picker + add/remove), switches on view KIND, never a surface id Extensions view (composition root): - folds frontend modules + backend surfaces into one "Extensions" view - frontend module list AGGREGATED from each feature's public `manifest` export (can't drift); no per-module version (FE features are internal to dispatch-web) - surfaces are AUTO-SUBSCRIBED on catalog + rendered expanded (no catalog buttons) surface-host: - consecutive `stat` fields coalesce into one aligned label/value table (StatTable) - generic custom-field renderer: dispatch on rendererId === "table" → SurfaceTable (pure parseTablePayload), so a backend `custom`/table field renders generically - shared presentational components/Table.svelte (used by both, neither feature depends on the other) store: - auto-subscribe every catalog entry, unsubscribe vanished ones, re-subscribe all on reconnect; expose all received specs via `surfaces` (drops single-selection) backend-handoff: CR-1 — emit Loaded Extensions as a custom/table field; notes what's already covered FE-side (renderer shipped, stat-table fallback works).
Diffstat (limited to 'src/app/store.test.ts')
-rw-r--r--src/app/store.test.ts72
1 files changed, 38 insertions, 34 deletions
diff --git a/src/app/store.test.ts b/src/app/store.test.ts
index 86a21d6..19530e2 100644
--- a/src/app/store.test.ts
+++ b/src/app/store.test.ts
@@ -105,7 +105,7 @@ function activeConversationId(store: ReturnType<typeof createAppStore>): string
}
describe("createAppStore", () => {
- it("starts with empty catalog and no selection", () => {
+ it("starts with empty catalog and no surfaces", () => {
const ws = fakeSocket();
const store = createAppStore({
socketFactory: () => ws,
@@ -116,8 +116,7 @@ describe("createAppStore", () => {
ws.resolveOpen();
expect(store.catalog).toEqual([]);
- expect(store.selectedId).toBeNull();
- expect(store.selectedSpec).toBeNull();
+ expect(store.surfaces).toEqual([]);
expect(store.lastError).toBeNull();
store.dispose();
@@ -148,7 +147,7 @@ describe("createAppStore", () => {
store.dispose();
});
- it("select sends subscribe and sets selectedId", () => {
+ it("auto-subscribes to every catalog entry when the catalog arrives", () => {
const ws = fakeSocket();
const store = createAppStore({
socketFactory: () => ws,
@@ -158,25 +157,26 @@ describe("createAppStore", () => {
});
ws.resolveOpen();
+ ws.sent.length = 0;
ws.feedSurfaceMessage({
type: "catalog",
- catalog: [{ id: "s1", region: "sidebar", title: "Surface One" }],
+ catalog: [
+ { id: "s1", region: "sidebar", title: "Surface One" },
+ { id: "s2", region: "panel", title: "Surface Two" },
+ ],
});
- 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();
+ const subscribed = ws.sent
+ .map((s) => JSON.parse(s))
+ .filter((p) => p.type === "subscribe")
+ .map((p) => p.surfaceId);
+ expect(subscribed).toContain("s1");
+ expect(subscribed).toContain("s2");
store.dispose();
});
- it("selecting a different surface unsubscribes from previous", () => {
+ it("unsubscribes from entries that vanish from a new catalog", () => {
const ws = fakeSocket();
const store = createAppStore({
socketFactory: () => ws,
@@ -195,25 +195,22 @@ describe("createAppStore", () => {
});
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";
+ ws.feedSurfaceMessage({
+ type: "catalog",
+ catalog: [{ id: "s1", region: "sidebar", title: "Surface One" }],
});
- expect(unsubscribeMsg).toBeTruthy();
- const subscribeMsg = ws.sent.find((s) => {
- const parsed = JSON.parse(s);
- return parsed.type === "subscribe" && parsed.surfaceId === "s2";
- });
- expect(subscribeMsg).toBeTruthy();
+ const unsubscribed = ws.sent
+ .map((s) => JSON.parse(s))
+ .filter((p) => p.type === "unsubscribe")
+ .map((p) => p.surfaceId);
+ expect(unsubscribed).toContain("s2");
+ expect(unsubscribed).not.toContain("s1");
store.dispose();
});
- it("surface message updates selectedSpec", () => {
+ it("exposes received surface specs via `surfaces`, in catalog order", () => {
const ws = fakeSocket();
const store = createAppStore({
socketFactory: () => ws,
@@ -225,11 +222,13 @@ describe("createAppStore", () => {
ws.feedSurfaceMessage({
type: "catalog",
- catalog: [{ id: "s1", region: "sidebar", title: "Surface One" }],
+ catalog: [
+ { id: "s1", region: "sidebar", title: "Surface One" },
+ { id: "s2", region: "panel", title: "Surface Two" },
+ ],
});
- store.select("s1");
-
+ // Only s1's spec has arrived: surfaces reflects what's actually received.
ws.feedSurfaceMessage({
type: "surface",
spec: {
@@ -239,10 +238,15 @@ describe("createAppStore", () => {
fields: [{ kind: "stat", label: "Tokens", value: "1,234" }],
},
});
+ expect(store.surfaces.map((s) => s.id)).toEqual(["s1"]);
- expect(store.selectedSpec).not.toBeNull();
- expect(store.selectedSpec?.id).toBe("s1");
- expect(store.selectedSpec?.fields).toHaveLength(1);
+ ws.feedSurfaceMessage({
+ type: "surface",
+ spec: { id: "s2", region: "panel", title: "Surface Two", fields: [] },
+ });
+ // Catalog order preserved (s1 before s2).
+ expect(store.surfaces.map((s) => s.id)).toEqual(["s1", "s2"]);
+ expect(store.surfaces[0]?.fields).toHaveLength(1);
store.dispose();
});