diff options
Diffstat (limited to 'src/features/tabs/tabs.test.ts')
| -rw-r--r-- | src/features/tabs/tabs.test.ts | 348 |
1 files changed, 191 insertions, 157 deletions
diff --git a/src/features/tabs/tabs.test.ts b/src/features/tabs/tabs.test.ts index 3034e76..ec93076 100644 --- a/src/features/tabs/tabs.test.ts +++ b/src/features/tabs/tabs.test.ts @@ -1,191 +1,225 @@ import { describe, expect, it } from "vitest"; import type { Tab, TabsState } from "./tabs"; import { - activeTab, - closeTab, - createTab, - deriveTitle, - initialState, - newDraft, - selectTab, - setModel, - setTitle, + activeTab, + closeTab, + createTab, + deriveTitle, + initialState, + MIN_HANDLE_LENGTH, + newDraft, + selectTab, + setModel, + setTitle, + shortHandle, } from "./tabs"; const tab = (conversationId: string, model = "default", title = "Chat"): Tab => ({ - conversationId, - model, - title, + conversationId, + model, + title, + workspaceId: "default", }); describe("initialState", () => { - it("returns empty draft state when no persisted state", () => { - const state = initialState(); - expect(state.tabs).toEqual([]); - expect(state.activeConversationId).toBeNull(); - }); - - it("returns persisted state when provided", () => { - const persisted: TabsState = { - tabs: [tab("c1")], - activeConversationId: "c1", - }; - const state = initialState(persisted); - expect(state.tabs).toHaveLength(1); - expect(state.activeConversationId).toBe("c1"); - }); + it("returns empty draft state when no persisted state", () => { + const state = initialState(); + expect(state.tabs).toEqual([]); + expect(state.activeConversationId).toBeNull(); + }); + + it("returns persisted state when provided", () => { + const persisted: TabsState = { + tabs: [tab("c1")], + activeConversationId: "c1", + }; + const state = initialState(persisted); + expect(state.tabs).toHaveLength(1); + expect(state.activeConversationId).toBe("c1"); + }); }); describe("newDraft", () => { - it("sets activeConversationId to null", () => { - const state: TabsState = { tabs: [tab("c1")], activeConversationId: "c1" }; - const next = newDraft(state); - expect(next.activeConversationId).toBeNull(); - }); - - it("keeps existing tabs", () => { - const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c1" }; - const next = newDraft(state); - expect(next.tabs).toHaveLength(2); - }); + it("sets activeConversationId to null", () => { + const state: TabsState = { tabs: [tab("c1")], activeConversationId: "c1" }; + const next = newDraft(state); + expect(next.activeConversationId).toBeNull(); + }); + + it("keeps existing tabs", () => { + const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c1" }; + const next = newDraft(state); + expect(next.tabs).toHaveLength(2); + }); }); describe("createTab", () => { - it("appends and activates", () => { - const state = initialState(); - const next = createTab(state, tab("c1")); - expect(next.tabs).toHaveLength(1); - expect(next.tabs[0]?.conversationId).toBe("c1"); - expect(next.activeConversationId).toBe("c1"); - }); - - it("does not duplicate an existing conversationId", () => { - const state: TabsState = { tabs: [tab("c1")], activeConversationId: "c1" }; - const next = createTab(state, tab("c1")); - expect(next.tabs).toHaveLength(1); - }); - - it("activates an already-existing tab when createTab is called again", () => { - const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c2" }; - const next = createTab(state, tab("c1")); - expect(next.activeConversationId).toBe("c1"); - }); + it("appends and activates", () => { + const state = initialState(); + const next = createTab(state, tab("c1")); + expect(next.tabs).toHaveLength(1); + expect(next.tabs[0]?.conversationId).toBe("c1"); + expect(next.activeConversationId).toBe("c1"); + }); + + it("does not duplicate an existing conversationId", () => { + const state: TabsState = { tabs: [tab("c1")], activeConversationId: "c1" }; + const next = createTab(state, tab("c1")); + expect(next.tabs).toHaveLength(1); + }); + + it("activates an already-existing tab when createTab is called again", () => { + const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c2" }; + const next = createTab(state, tab("c1")); + expect(next.activeConversationId).toBe("c1"); + }); }); describe("selectTab", () => { - it("changes active", () => { - const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c1" }; - const next = selectTab(state, "c2"); - expect(next.activeConversationId).toBe("c2"); - }); + it("changes active", () => { + const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c1" }; + const next = selectTab(state, "c2"); + expect(next.activeConversationId).toBe("c2"); + }); }); describe("closeTab", () => { - it("removes the tab", () => { - const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c1" }; - const next = closeTab(state, "c2"); - expect(next.tabs).toHaveLength(1); - expect(next.tabs[0]?.conversationId).toBe("c1"); - }); - - it("closing the active tab activates a neighbour (previous preferred)", () => { - const state: TabsState = { - tabs: [tab("c1"), tab("c2"), tab("c3")], - activeConversationId: "c2", - }; - const next = closeTab(state, "c2"); - expect(next.activeConversationId).toBe("c1"); - }); - - it("closing the first active tab activates the next", () => { - const state: TabsState = { - tabs: [tab("c1"), tab("c2"), tab("c3")], - activeConversationId: "c1", - }; - const next = closeTab(state, "c1"); - expect(next.activeConversationId).toBe("c2"); - }); - - it("closing the last tab returns to draft (null active)", () => { - const state: TabsState = { tabs: [tab("c1")], activeConversationId: "c1" }; - const next = closeTab(state, "c1"); - expect(next.tabs).toHaveLength(0); - expect(next.activeConversationId).toBeNull(); - }); - - it("closing a non-active tab does not change active", () => { - const state: TabsState = { - tabs: [tab("c1"), tab("c2"), tab("c3")], - activeConversationId: "c3", - }; - const next = closeTab(state, "c1"); - expect(next.activeConversationId).toBe("c3"); - }); - - it("closing a non-existent tab is a no-op", () => { - const state: TabsState = { tabs: [tab("c1")], activeConversationId: "c1" }; - const next = closeTab(state, "missing"); - expect(next).toEqual(state); - }); + it("removes the tab", () => { + const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c1" }; + const next = closeTab(state, "c2"); + expect(next.tabs).toHaveLength(1); + expect(next.tabs[0]?.conversationId).toBe("c1"); + }); + + it("closing the active tab activates a neighbour (previous preferred)", () => { + const state: TabsState = { + tabs: [tab("c1"), tab("c2"), tab("c3")], + activeConversationId: "c2", + }; + const next = closeTab(state, "c2"); + expect(next.activeConversationId).toBe("c1"); + }); + + it("closing the first active tab activates the next", () => { + const state: TabsState = { + tabs: [tab("c1"), tab("c2"), tab("c3")], + activeConversationId: "c1", + }; + const next = closeTab(state, "c1"); + expect(next.activeConversationId).toBe("c2"); + }); + + it("closing the last tab returns to draft (null active)", () => { + const state: TabsState = { tabs: [tab("c1")], activeConversationId: "c1" }; + const next = closeTab(state, "c1"); + expect(next.tabs).toHaveLength(0); + expect(next.activeConversationId).toBeNull(); + }); + + it("closing a non-active tab does not change active", () => { + const state: TabsState = { + tabs: [tab("c1"), tab("c2"), tab("c3")], + activeConversationId: "c3", + }; + const next = closeTab(state, "c1"); + expect(next.activeConversationId).toBe("c3"); + }); + + it("closing a non-existent tab is a no-op", () => { + const state: TabsState = { tabs: [tab("c1")], activeConversationId: "c1" }; + const next = closeTab(state, "missing"); + expect(next).toEqual(state); + }); }); describe("setModel", () => { - it("updates the right tab", () => { - const state: TabsState = { tabs: [tab("c1", "old"), tab("c2")], activeConversationId: "c1" }; - const next = setModel(state, "c1", "new-model"); - expect(next.tabs[0]?.model).toBe("new-model"); - expect(next.tabs[1]?.model).toBe("default"); - }); + it("updates the right tab", () => { + const state: TabsState = { tabs: [tab("c1", "old"), tab("c2")], activeConversationId: "c1" }; + const next = setModel(state, "c1", "new-model"); + expect(next.tabs[0]?.model).toBe("new-model"); + expect(next.tabs[1]?.model).toBe("default"); + }); }); describe("setTitle", () => { - it("updates the right tab", () => { - const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c1" }; - const next = setTitle(state, "c1", "Updated title"); - expect(next.tabs[0]?.title).toBe("Updated title"); - expect(next.tabs[1]?.title).toBe("Chat"); - }); + it("updates the right tab", () => { + const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c1" }; + const next = setTitle(state, "c1", "Updated title"); + expect(next.tabs[0]?.title).toBe("Updated title"); + expect(next.tabs[1]?.title).toBe("Chat"); + }); }); describe("activeTab", () => { - it("returns the active tab", () => { - const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c2" }; - expect(activeTab(state)?.conversationId).toBe("c2"); - }); - - it("returns null when activeConversationId is null", () => { - const state: TabsState = { tabs: [tab("c1")], activeConversationId: null }; - expect(activeTab(state)).toBeNull(); - }); - - it("returns null when active tab is not found in tabs", () => { - const state: TabsState = { tabs: [tab("c1")], activeConversationId: "missing" }; - expect(activeTab(state)).toBeNull(); - }); + it("returns the active tab", () => { + const state: TabsState = { tabs: [tab("c1"), tab("c2")], activeConversationId: "c2" }; + expect(activeTab(state)?.conversationId).toBe("c2"); + }); + + it("returns null when activeConversationId is null", () => { + const state: TabsState = { tabs: [tab("c1")], activeConversationId: null }; + expect(activeTab(state)).toBeNull(); + }); + + it("returns null when active tab is not found in tabs", () => { + const state: TabsState = { tabs: [tab("c1")], activeConversationId: "missing" }; + expect(activeTab(state)).toBeNull(); + }); }); describe("deriveTitle", () => { - it("truncates long messages with ellipsis", () => { - const msg = "This is a very long message that should be truncated at some point"; - expect(deriveTitle(msg, 20)).toBe("This is a very long \u2026"); - }); - - it("returns full message when under max", () => { - expect(deriveTitle("Short", 40)).toBe("Short"); - }); - - it("collapses whitespace", () => { - expect(deriveTitle(" hello world ")).toBe("hello world"); - }); - - it("falls back to 'New chat' for empty input", () => { - expect(deriveTitle("")).toBe("New chat"); - expect(deriveTitle(" ")).toBe("New chat"); - }); - - it("uses default max of ~40 chars", () => { - const msg = "a".repeat(50); - const result = deriveTitle(msg); - expect(result).toBe(`${"a".repeat(40)}\u2026`); - }); + it("truncates long messages with ellipsis", () => { + const msg = "This is a very long message that should be truncated at some point"; + expect(deriveTitle(msg, 20)).toBe("This is a very long \u2026"); + }); + + it("returns full message when under max", () => { + expect(deriveTitle("Short", 40)).toBe("Short"); + }); + + it("collapses whitespace", () => { + expect(deriveTitle(" hello world ")).toBe("hello world"); + }); + + it("falls back to 'New chat' for empty input", () => { + expect(deriveTitle("")).toBe("New chat"); + expect(deriveTitle(" ")).toBe("New chat"); + }); + + it("uses default max of ~40 chars", () => { + const msg = "a".repeat(50); + const result = deriveTitle(msg); + expect(result).toBe(`${"a".repeat(40)}\u2026`); + }); +}); + +describe("shortHandle", () => { + it("uses the minimum length when the id is unique", () => { + const h = shortHandle("3f9a1b2c-aaaa", ["3f9a1b2c-aaaa", "7c2d-bbbb"]); + expect(h).toBe("3f9a"); + expect(h.length).toBe(MIN_HANDLE_LENGTH); + }); + + it("grows the prefix until unique among open tabs", () => { + // two ids share the first 5 chars → handle grows to 6 to disambiguate + expect(shortHandle("abcde1-xxxx", ["abcde1-xxxx", "abcde2-yyyy"])).toBe("abcde1"); + expect(shortHandle("abcde2-yyyy", ["abcde1-xxxx", "abcde2-yyyy"])).toBe("abcde2"); + }); + + it("shrinks back to the minimum when the colliding sibling is gone", () => { + expect(shortHandle("abcde1-xxxx", ["abcde1-xxxx"])).toBe("abcd"); + }); + + it("ignores the id itself when present in the list", () => { + expect(shortHandle("deadbeef", ["deadbeef"])).toBe("dead"); + }); + + it("returns the whole id when shorter than the minimum length", () => { + expect(shortHandle("ab", ["ab", "cd"])).toBe("ab"); + }); + + it("falls back to the full id when one id is a prefix of another", () => { + // "abcd" is a prefix of "abcd1234" → no unique shorter prefix exists for it + expect(shortHandle("abcd", ["abcd", "abcd1234"])).toBe("abcd"); + }); }); |
