summaryrefslogtreecommitdiffhomepage
path: root/src/app/App.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-07 02:20:51 +0900
committerAdam Malczewski <[email protected]>2026-06-07 02:20:51 +0900
commit5f867c6711ed693aa2a029ae1fb07eb1106ee32c (patch)
tree3d2942b455454d8c4e241b6d3fe22bb3526e7ed8 /src/app/App.test.ts
parent529c6a2bb56447fe93796111df3d4cc5a05fdd93 (diff)
downloaddispatch-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.ts94
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!",
},