diff options
| author | Adam Malczewski <[email protected]> | 2026-06-27 18:26:29 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-27 18:26:29 +0900 |
| commit | a20a8252dd4e4df3d82233ab2d6f1367ddb7cc32 (patch) | |
| tree | 384f75c00cbaacebdcb4bfe01c18898eeede26a7 /src/features/tabs/ui.test.ts | |
| parent | f4e6fb12c1ed446bc7b088a7d07b51c6de991af3 (diff) | |
| parent | 11dc42164937c99359f1dc1b01756d6409fac906 (diff) | |
| download | dispatch-web-a20a8252dd4e4df3d82233ab2d6f1367ddb7cc32.tar.gz dispatch-web-a20a8252dd4e4df3d82233ab2d6f1367ddb7cc32.zip | |
Merge branch 'dev' into feature/provider-concurrency
Diffstat (limited to 'src/features/tabs/ui.test.ts')
| -rw-r--r-- | src/features/tabs/ui.test.ts | 154 |
1 files changed, 104 insertions, 50 deletions
diff --git a/src/features/tabs/ui.test.ts b/src/features/tabs/ui.test.ts index 087b28c..9af4cd1 100644 --- a/src/features/tabs/ui.test.ts +++ b/src/features/tabs/ui.test.ts @@ -1,8 +1,8 @@ -import { render, screen } from "@testing-library/svelte"; +import { fireEvent, render, screen } from "@testing-library/svelte"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import type { Tab } from "./tabs"; -import TabBar from "./ui/TabBar.svelte"; +import TabList from "./ui/TabList.svelte"; const sampleTabs: readonly Tab[] = [ { conversationId: "c1", model: "openai/gpt-4", title: "First", workspaceId: "default" }, @@ -10,9 +10,9 @@ const sampleTabs: readonly Tab[] = [ { conversationId: "c3", model: "google/gemini", title: "Third", workspaceId: "default" }, ]; -describe("TabBar", () => { +describe("TabList", () => { it("renders one role=tab element per tab showing each title", () => { - render(TabBar, { + render(TabList, { props: { tabs: sampleTabs, activeConversationId: "c1", @@ -29,8 +29,8 @@ describe("TabBar", () => { expect(tabs[2]).toHaveTextContent("Third"); }); - it("applies tab-active to the active tab only", () => { - render(TabBar, { + it("marks the active tab as aria-selected", () => { + render(TabList, { props: { tabs: sampleTabs, activeConversationId: "c2", @@ -41,24 +41,9 @@ describe("TabBar", () => { }); const tabs = screen.getAllByRole("tab"); - expect(tabs[0]).not.toHaveClass("tab-active"); - expect(tabs[1]).toHaveClass("tab-active"); - expect(tabs[2]).not.toHaveClass("tab-active"); - }); - - it("applies tab-active to New chat button when activeConversationId is null", () => { - render(TabBar, { - props: { - tabs: sampleTabs, - activeConversationId: null, - onSelect: vi.fn(), - onClose: vi.fn(), - onNewDraft: vi.fn(), - }, - }); - - const newChat = screen.getByRole("button", { name: "New chat" }); - expect(newChat).toHaveClass("tab-active"); + expect(tabs[0]).toHaveAttribute("aria-selected", "false"); + expect(tabs[1]).toHaveAttribute("aria-selected", "true"); + expect(tabs[2]).toHaveAttribute("aria-selected", "false"); }); it("calls onSelect with the conversationId when a tab is clicked", async () => { @@ -66,7 +51,7 @@ describe("TabBar", () => { const onClose = vi.fn(); const user = userEvent.setup(); - render(TabBar, { + render(TabList, { props: { tabs: sampleTabs, activeConversationId: "c1", @@ -91,7 +76,7 @@ describe("TabBar", () => { const onClose = vi.fn(); const user = userEvent.setup(); - render(TabBar, { + render(TabList, { props: { tabs: sampleTabs, activeConversationId: "c1", @@ -115,7 +100,7 @@ describe("TabBar", () => { const onNewDraft = vi.fn(); const user = userEvent.setup(); - render(TabBar, { + render(TabList, { props: { tabs: sampleTabs, activeConversationId: "c1", @@ -131,23 +116,8 @@ describe("TabBar", () => { expect(onNewDraft).toHaveBeenCalledTimes(1); }); - it("the New chat button has the sticky class", () => { - render(TabBar, { - props: { - tabs: sampleTabs, - activeConversationId: "c1", - onSelect: vi.fn(), - onClose: vi.fn(), - onNewDraft: vi.fn(), - }, - }); - - const newChat = screen.getByRole("button", { name: "New chat" }); - expect(newChat).toHaveClass("sticky"); - }); - it("shows visible 'New Chat' text when activeConversationId is null", () => { - render(TabBar, { + render(TabList, { props: { tabs: sampleTabs, activeConversationId: null, @@ -162,7 +132,7 @@ describe("TabBar", () => { }); it("does not show 'New Chat' text when a real tab is active", () => { - render(TabBar, { + render(TabList, { props: { tabs: sampleTabs, activeConversationId: "c1", @@ -181,7 +151,7 @@ describe("TabBar", () => { { conversationId: "3f9a1b2c-1111", model: "m", title: "Alpha", workspaceId: "default" }, { conversationId: "7c2db4e5-2222", model: "m", title: "Beta", workspaceId: "default" }, ]; - render(TabBar, { + render(TabList, { props: { tabs, activeConversationId: "3f9a1b2c-1111", @@ -195,8 +165,8 @@ describe("TabBar", () => { expect(screen.getByText("7c2d")).toBeInTheDocument(); }); - it("renders fixed-width tabs", () => { - render(TabBar, { + it("renders each tab as a single vertical row (flex-col list, not a horizontal strip)", () => { + render(TabList, { props: { tabs: sampleTabs, activeConversationId: "c1", @@ -206,8 +176,92 @@ describe("TabBar", () => { }, }); - for (const t of screen.getAllByRole("tab")) { - expect(t).toHaveClass("w-48"); - } + // The scroll region containing the tab rows is a vertical flex column. + const tabs = screen.getAllByRole("tab"); + expect(tabs.length).toBeGreaterThan(0); + const region = tabs[0]?.parentElement; + expect(region).toHaveClass("flex-col"); + }); + + it("caps the tab list region at 80vh so a long set scrolls internally", () => { + render(TabList, { + props: { + tabs: sampleTabs, + activeConversationId: "c1", + onSelect: vi.fn(), + onClose: vi.fn(), + onNewDraft: vi.fn(), + }, + }); + + const tabs = screen.getAllByRole("tab"); + const region = tabs[0]?.parentElement; + expect(region).toHaveClass("max-h-[80vh]"); + expect(region).toHaveClass("overflow-y-auto"); + }); + + it("calls onRename when a tab title is double-clicked and committed with Enter", async () => { + const onRename = vi.fn(); + const user = userEvent.setup(); + + render(TabList, { + props: { + tabs: sampleTabs, + activeConversationId: "c1", + onSelect: vi.fn(), + onClose: vi.fn(), + onNewDraft: vi.fn(), + onRename, + }, + }); + + const titleButtons = screen.getAllByRole("button"); + // The inline-rename trigger is the title span (role=button) — find the one + // whose text matches the first tab's title. + const titleButton = titleButtons.find((b) => b.textContent === "First"); + if (!titleButton) throw new Error("title button not found"); + await user.dblClick(titleButton); + + const input = screen.getByRole("textbox"); + await user.clear(input); + await user.type(input, "Renamed{Enter}"); + + expect(onRename).toHaveBeenCalledTimes(1); + expect(onRename).toHaveBeenCalledWith("c1", "Renamed"); + }); + + it("copies the conversation id to the clipboard and highlights the badge text when clicked", async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { + value: { writeText }, + configurable: true, + }); + + const onSelect = vi.fn(); + + render(TabList, { + props: { + tabs: sampleTabs, + activeConversationId: "c1", + onSelect, + onClose: vi.fn(), + onNewDraft: vi.fn(), + }, + }); + + const idBadge = screen.getByRole("button", { name: "Copy conversation id c1" }); + await fireEvent.click(idBadge); + + expect(writeText).toHaveBeenCalledTimes(1); + expect(writeText).toHaveBeenCalledWith("c1"); + // The badge text is NOT swapped (no width shift) — the highlight (selection) + // is the only indicator. + expect(idBadge).toHaveTextContent("c1"); + expect(idBadge).not.toHaveTextContent("Copied"); + // The badge text is selected (highlighted) as the copy indicator. + const selection = window.getSelection(); + expect(selection?.toString()).toBe("c1"); + // Clicking the ID must NOT switch tabs (the badge stops propagation). + expect(onSelect).not.toHaveBeenCalled(); }); }); |
