diff options
| author | Adam Malczewski <[email protected]> | 2026-06-07 02:20:51 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-07 02:20:51 +0900 |
| commit | 5f867c6711ed693aa2a029ae1fb07eb1106ee32c (patch) | |
| tree | 3d2942b455454d8c4e241b6d3fe22bb3526e7ed8 /src/app/App.test.ts | |
| parent | 529c6a2bb56447fe93796111df3d4cc5a05fdd93 (diff) | |
| download | dispatch-web-5f867c6711ed693aa2a029ae1fb07eb1106ee32c.tar.gz dispatch-web-5f867c6711ed693aa2a029ae1fb07eb1106ee32c.zip | |
Slice 3 wave B: tabbed multi-conversation app + model selector (DaisyUI)
- store.svelte.ts: tabs store over injected localStorage; one chat store per
conversation (Map); single WS routes chat.delta/error by conversationId;
draft (null active) mints a conversationId and becomes a tab on first send
(title from deriveTitle); GET /models catalog; default model flash; close tab
= dispose + cache.delete (local forget) + neighbour activation; restore tabs
from storage + load() on construct
- App.svelte: DaisyUI tab strip (+ / close), model selector, chat, surfaces
- AppStore: tabs/activeConversationId/activeChat/models/activeModel +
send/selectModel/newDraft/selectTab/closeTab; +localStorage inject opt
Verified: svelte-check 0/0, vitest 281 (stable x2), biome clean, build ok.
Diffstat (limited to 'src/app/App.test.ts')
| -rw-r--r-- | src/app/App.test.ts | 94 |
1 files changed, 59 insertions, 35 deletions
diff --git a/src/app/App.test.ts b/src/app/App.test.ts index b21b39f..8110d41 100644 --- a/src/app/App.test.ts +++ b/src/app/App.test.ts @@ -55,37 +55,76 @@ function fakeSocket(): FakeSocket { } function fakeFetchImpl(): typeof fetch { - return async (): Promise<Response> => - new Response(JSON.stringify({ chunks: [], latestSeq: 0 }), { status: 200 }); + return async (input: string | URL | Request): Promise<Response> => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + if (url.endsWith("/models")) { + return new Response(JSON.stringify({ models: ["opencode/deepseek-v4-flash"] }), { + status: 200, + }); + } + return new Response(JSON.stringify({ chunks: [], latestSeq: 0 }), { status: 200 }); + }; +} + +function createFakeStorage(): Storage { + const map = new Map<string, string>(); + return { + get length() { + return map.size; + }, + clear() { + map.clear(); + }, + getItem(key: string): string | null { + return map.get(key) ?? null; + }, + key(_index: number): string | null { + return null; + }, + removeItem(key: string) { + map.delete(key); + }, + setItem(key: string, value: string) { + map.set(key, value); + }, + }; } function sentMessages(ws: FakeSocket) { return ws.sent.map((s) => JSON.parse(s)); } +function activeConversationId(store: ReturnType<typeof createAppStore>): string { + const id = store.activeConversationId; + expect(id).not.toBeNull(); + return id as string; +} + describe("App component interaction tests", () => { - it("renders empty state when catalog is empty", () => { + it("renders the model selector and composer in draft mode", () => { const ws = fakeSocket(); const store = createAppStore({ socketFactory: () => ws, fetchImpl: fakeFetchImpl(), - conversationId: "test-conv", + localStorage: createFakeStorage(), }); ws.resolveOpen(); render(App, { props: { store } }); - expect(screen.getByText("No surfaces available")).toBeInTheDocument(); + expect(screen.getByRole("textbox", { name: "Message input" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Send" })).toBeInTheDocument(); + expect(screen.getByRole("combobox", { name: "Model selector" })).toBeInTheDocument(); store.dispose(); }); - it("renders a catalog button per entry after a catalog message", () => { + it("renders catalog buttons when surfaces are available", () => { const ws = fakeSocket(); const store = createAppStore({ socketFactory: () => ws, fetchImpl: fakeFetchImpl(), - conversationId: "test-conv", + localStorage: createFakeStorage(), }); ws.resolveOpen(); @@ -114,7 +153,7 @@ describe("App component interaction tests", () => { const store = createAppStore({ socketFactory: () => ws, fetchImpl: fakeFetchImpl(), - conversationId: "test-conv", + localStorage: createFakeStorage(), }); ws.resolveOpen(); @@ -158,7 +197,7 @@ describe("App component interaction tests", () => { const store = createAppStore({ socketFactory: () => ws, fetchImpl: fakeFetchImpl(), - conversationId: "test-conv", + localStorage: createFakeStorage(), }); ws.resolveOpen(); @@ -193,7 +232,7 @@ describe("App component interaction tests", () => { const store = createAppStore({ socketFactory: () => ws, fetchImpl: fakeFetchImpl(), - conversationId: "test-conv", + localStorage: createFakeStorage(), }); ws.resolveOpen(); @@ -227,7 +266,7 @@ describe("App component interaction tests", () => { const store = createAppStore({ socketFactory: () => ws, fetchImpl: fakeFetchImpl(), - conversationId: "test-conv", + localStorage: createFakeStorage(), }); ws.resolveOpen(); @@ -249,7 +288,7 @@ describe("App component interaction tests", () => { const store = createAppStore({ socketFactory: () => ws, fetchImpl: fakeFetchImpl(), - conversationId: "test-conv", + localStorage: createFakeStorage(), }); ws.resolveOpen(); @@ -297,31 +336,12 @@ describe("App component interaction tests", () => { store.dispose(); }); - it("renders the chat section with composer", () => { - const ws = fakeSocket(); - const store = createAppStore({ - socketFactory: () => ws, - fetchImpl: fakeFetchImpl(), - conversationId: "test-conv", - }); - ws.resolveOpen(); - - render(App, { props: { store } }); - - expect(screen.getByRole("heading", { name: "Chat" })).toBeInTheDocument(); - expect(screen.getByRole("log")).toBeInTheDocument(); - expect(screen.getByRole("textbox", { name: "Message input" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Send" })).toBeInTheDocument(); - - store.dispose(); - }); - it("typing and sending a message posts chat.send on the socket", async () => { const ws = fakeSocket(); const store = createAppStore({ socketFactory: () => ws, fetchImpl: fakeFetchImpl(), - conversationId: "test-conv", + localStorage: createFakeStorage(), }); ws.resolveOpen(); @@ -350,17 +370,21 @@ describe("App component interaction tests", () => { const store = createAppStore({ socketFactory: () => ws, fetchImpl: fakeFetchImpl(), - conversationId: "test-conv", + localStorage: createFakeStorage(), }); ws.resolveOpen(); + // Promote draft to tab + store.send("test"); + const convId = activeConversationId(store); + render(App, { props: { store } }); ws.feedServerMessage({ type: "chat.delta", event: { type: "turn-start", - conversationId: "test-conv", + conversationId: convId, turnId: "turn-1", }, }); @@ -369,7 +393,7 @@ describe("App component interaction tests", () => { type: "chat.delta", event: { type: "text-delta", - conversationId: "test-conv", + conversationId: convId, turnId: "turn-1", delta: "Hi there!", }, |
