summaryrefslogtreecommitdiffhomepage
path: root/src/features/tabs/tabs.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/tabs/tabs.test.ts')
-rw-r--r--src/features/tabs/tabs.test.ts348
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");
+ });
});