cc-haha/desktop/src/components/layout/TabBar.test.tsx
程序员阿江(Relakkes) ba1ab98c2e fix: reduce risky sidebar controls to project organization
The sidebar now groups conversations by project, so the embedded project picker in the search box duplicated the main navigation model and hid state inside a compact icon. Remove that picker and its store state, leaving search as plain text filtering and keeping the project header menu focused on organization and sorting.

The archive-all entry was also hidden because a broad destructive action does not belong in the lightweight project menu.

Constraint: Project grouping is now the primary project navigation surface.
Rejected: Keep the embedded project picker hidden in place | it would leave dead filtering state and a stale recovery path.
Rejected: Keep archive-all in the header menu | broad destructive actions are too risky for this surface.
Confidence: high
Scope-risk: moderate
Directive: Do not reintroduce broad session deletion or hidden project filters into the sidebar header without a dedicated reviewed management flow.
Tested: cd desktop && bun run test src/components/layout/Sidebar.test.tsx src/stores/sessionStore.test.ts src/components/layout/TabBar.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
2026-05-15 19:36:52 +08:00

752 lines
25 KiB
TypeScript

import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import '@testing-library/jest-dom'
const startDraggingMock = vi.hoisted(() => vi.fn(() => Promise.resolve()))
const getCurrentWindowMock = vi.hoisted(() => vi.fn(() => ({
startDragging: startDraggingMock,
})))
const windowControlsMock = vi.hoisted(() => ({
show: true,
}))
const scrollIntoViewMock = vi.hoisted(() => vi.fn())
const openProjectMenuMock = vi.hoisted(() => ({
paths: [] as Array<string | null | undefined>,
}))
vi.mock('@tauri-apps/api/window', () => ({
getCurrentWindow: getCurrentWindowMock,
}))
vi.mock('../../i18n', () => ({
useTranslation: () => (key: string) => {
const translations: Record<string, string> = {
'tabs.close': 'Close',
'tabs.closeOthers': 'Close Others',
'tabs.closeLeft': 'Close Left',
'tabs.closeRight': 'Close Right',
'tabs.closeAll': 'Close All',
'tabs.closeConfirmTitle': 'Session Running',
'tabs.closeConfirmMessage': 'Still running',
'tabs.closeConfirmKeep': 'Keep Running',
'tabs.closeConfirmStop': 'Stop & Close',
'tabs.openTerminal': 'Open Terminal',
'tabs.showWorkspace': 'Show Workspace',
'tabs.hideWorkspace': 'Hide Workspace',
'openProject.openProject': 'Open project',
'openProject.openIn': 'Open in {target}',
'openProject.openFailed': 'Could not open project',
'common.cancel': 'Cancel',
}
return translations[key] ?? key
},
}))
vi.mock('./OpenProjectMenu', () => ({
OpenProjectMenu: ({ path }: { path: string | null | undefined }) => {
if (!path) return null
openProjectMenuMock.paths.push(path)
return <div data-testid="open-project-menu">{path}</div>
},
}))
vi.mock('./WindowControls', () => ({
WindowControls: () => (windowControlsMock.show ? <div data-testid="window-controls" /> : null),
get showWindowControls() {
return windowControlsMock.show
},
}))
describe('TabBar', () => {
beforeEach(() => {
class ResizeObserverMock {
constructor(_callback: ResizeObserverCallback) {}
observe(_target: Element) {}
disconnect() {}
unobserve() {}
}
Object.defineProperty(window, 'ResizeObserver', {
configurable: true,
value: ResizeObserverMock,
})
Object.defineProperty(window, '__TAURI__', {
configurable: true,
value: {},
})
Object.defineProperty(window.HTMLElement.prototype, 'scrollIntoView', {
configurable: true,
value: scrollIntoViewMock,
})
startDraggingMock.mockClear()
getCurrentWindowMock.mockClear()
scrollIntoViewMock.mockClear()
openProjectMenuMock.paths = []
windowControlsMock.show = true
vi.resetModules()
})
afterEach(async () => {
cleanup()
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
const { useSessionStore } = await import('../../stores/sessionStore')
const { useWorkspacePanelStore } = await import('../../stores/workspacePanelStore')
const { useTerminalPanelStore } = await import('../../stores/terminalPanelStore')
useTabStore.setState({ tabs: [], activeTabId: null })
useChatStore.setState({
sessions: {},
} as Partial<ReturnType<typeof useChatStore.getState>>)
useSessionStore.setState({
sessions: [],
activeSessionId: null,
isLoading: false,
error: null,
isBatchMode: false,
selectedSessionIds: new Set(),
} as Partial<ReturnType<typeof useSessionStore.getState>>)
useWorkspacePanelStore.setState(useWorkspacePanelStore.getInitialState(), true)
useTerminalPanelStore.setState(useTerminalPanelStore.getInitialState(), true)
delete (window as typeof window & { __TAURI__?: unknown }).__TAURI__
})
it('scrolls the active tab into view when the active tab changes', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'First Session', type: 'session', status: 'idle' },
{ sessionId: 'tab-2', title: 'Second Session', type: 'session', status: 'idle' },
{ sessionId: 'tab-3', title: 'Third Session', type: 'session', status: 'idle' },
{ sessionId: 'tab-4', title: 'Fourth Session', type: 'session', status: 'idle' },
{ sessionId: 'tab-5', title: 'New Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
scrollIntoViewMock.mockClear()
await act(async () => {
useTabStore.getState().setActiveTab('tab-5')
})
expect(scrollIntoViewMock).toHaveBeenCalledWith({
block: 'nearest',
inline: 'nearest',
behavior: 'smooth',
})
})
it('keeps the overflow button flush against window controls on Windows', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'Untitled Session', type: 'session', status: 'idle' },
{ sessionId: 'tab-2', title: 'Settings', type: 'settings', status: 'idle' },
{ sessionId: 'tab-3', title: 'hello', type: 'session', status: 'idle' },
{ sessionId: 'tab-4', title: 'overflow', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
const scrollRegion = screen.getByTestId('tab-bar').querySelector('.overflow-x-hidden')
expect(scrollRegion).toBeInTheDocument()
Object.defineProperty(scrollRegion!, 'clientWidth', {
configurable: true,
get: () => 240,
})
Object.defineProperty(scrollRegion!, 'scrollWidth', {
configurable: true,
get: () => 720,
})
Object.defineProperty(scrollRegion!, 'scrollLeft', {
configurable: true,
get: () => 0,
})
Object.defineProperty(scrollRegion!, 'scrollBy', {
configurable: true,
value: vi.fn(),
})
act(() => {
fireEvent.scroll(scrollRegion!)
})
await waitFor(() => {
expect(screen.getByTestId('window-controls')).toBeInTheDocument()
expect(screen.getByText('chevron_right').closest('button')).toBeInTheDocument()
})
const rightButton = screen.getByText('chevron_right').closest('button')
expect(rightButton?.nextElementSibling).toBe(screen.getByTestId('window-controls'))
})
it('shows the terminal toolbar when no tabs are open', async () => {
windowControlsMock.show = false
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
useTabStore.setState({ tabs: [], activeTabId: null })
await act(async () => {
render(<TabBar />)
})
fireEvent.click(screen.getByRole('button', { name: 'Open Terminal' }))
const terminalTabs = useTabStore.getState().tabs.filter((tab) => tab.type === 'terminal')
expect(terminalTabs).toHaveLength(1)
expect(useTabStore.getState().activeTabId).toBe(terminalTabs[0]?.sessionId)
expect(screen.queryByTestId('window-controls')).not.toBeInTheDocument()
})
it('marks the tab bar as a native drag region', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'Untitled Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
expect(screen.getByTestId('tab-bar')).not.toHaveAttribute('data-tauri-drag-region')
expect(screen.getByTestId('tab-bar-drag-gutter')).toHaveAttribute('data-tauri-drag-region')
})
it('keeps the desktop tab strip at a roomier titlebar height', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'Untitled Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
const tabBar = screen.getByTestId('tab-bar')
const tab = screen.getByText('Untitled Session').closest('.tab-bar-hit-area')
expect(tabBar).toHaveClass('min-h-11')
expect(tab).toHaveClass('min-h-11')
expect(screen.getByTestId('tab-bar-drag-gutter')).toHaveClass('min-h-11')
})
it('passes the active session workdir into the open-project control', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
const { useSessionStore } = await import('../../stores/sessionStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'Workspace Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
useSessionStore.setState({
sessions: [{
id: 'tab-1',
title: 'Workspace Session',
createdAt: '2026-05-13T00:00:00.000Z',
modifiedAt: '2026-05-13T00:00:00.000Z',
messageCount: 0,
projectPath: '/repo',
workDir: '/repo/worktree',
workDirExists: true,
}],
activeSessionId: 'tab-1',
})
await act(async () => {
render(<TabBar />)
})
expect(screen.getByTestId('open-project-menu')).toHaveTextContent('/repo/worktree')
expect(openProjectMenuMock.paths[openProjectMenuMock.paths.length - 1]).toBe('/repo/worktree')
})
it('hides the open-project control when the active session workdir is unavailable', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
const { useSessionStore } = await import('../../stores/sessionStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'Workspace Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
useSessionStore.setState({
sessions: [{
id: 'tab-1',
title: 'Workspace Session',
createdAt: '2026-05-13T00:00:00.000Z',
modifiedAt: '2026-05-13T00:00:00.000Z',
messageCount: 0,
projectPath: '/repo',
workDir: '/repo/worktree',
workDirExists: false,
}],
activeSessionId: 'tab-1',
})
await act(async () => {
render(<TabBar />)
})
expect(screen.queryByTestId('open-project-menu')).not.toBeInTheDocument()
})
it('hides the open-project control outside the desktop shell', async () => {
delete (window as typeof window & { __TAURI__?: unknown }).__TAURI__
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
const { useSessionStore } = await import('../../stores/sessionStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'Workspace Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
useSessionStore.setState({
sessions: [{
id: 'tab-1',
title: 'Workspace Session',
createdAt: '2026-05-13T00:00:00.000Z',
modifiedAt: '2026-05-13T00:00:00.000Z',
messageCount: 0,
projectPath: '/repo',
workDir: '/repo/worktree',
workDirExists: true,
}],
activeSessionId: 'tab-1',
})
await act(async () => {
render(<TabBar />)
})
expect(screen.queryByTestId('open-project-menu')).not.toBeInTheDocument()
})
it('starts dragging when clicking the empty tab-bar gutter', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'Untitled Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
await waitFor(() => {
expect(getCurrentWindowMock).toHaveBeenCalled()
})
const scrollRegion = screen.getByTestId('tab-bar').querySelector('.overflow-x-hidden')
expect(scrollRegion).toBeInTheDocument()
fireEvent.mouseDown(scrollRegion!)
await waitFor(() => {
expect(startDraggingMock).toHaveBeenCalledTimes(1)
})
})
it('does not start dragging when clicking a tab', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'Untitled Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
await waitFor(() => {
expect(getCurrentWindowMock).toHaveBeenCalled()
})
fireEvent.mouseDown(screen.getByText('Untitled Session'))
expect(startDraggingMock).not.toHaveBeenCalled()
})
it('reorders tabs via pointer drag', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'First Session', type: 'session', status: 'idle' },
{ sessionId: 'tab-2', title: 'Second Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
expect(screen.getByTestId('tab-bar').querySelector('.tab-bar-hit-area')).toBeInTheDocument()
const firstTab = screen.getByText('First Session').closest('.tab-bar-hit-area')
const secondTab = screen.getByText('Second Session').closest('.tab-bar-hit-area')
expect(firstTab).toBeTruthy()
expect(secondTab).toBeTruthy()
Object.defineProperty(firstTab!, 'getBoundingClientRect', {
configurable: true,
value: () => ({ left: 0, width: 180 }),
})
Object.defineProperty(secondTab!, 'getBoundingClientRect', {
configurable: true,
value: () => ({ left: 180, width: 180 }),
})
fireEvent.mouseDown(firstTab!, { button: 0, clientX: 20, clientY: 10 })
fireEvent.mouseMove(window, { clientX: 260, clientY: 10 })
expect(firstTab).toHaveAttribute('data-dragging', 'true')
fireEvent.mouseUp(window)
expect(useTabStore.getState().tabs.map((tab) => tab.sessionId)).toEqual(['tab-2', 'tab-1'])
})
it('does not reorder on a simple click without dragging', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'First Session', type: 'session', status: 'idle' },
{ sessionId: 'tab-2', title: 'Second Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
const firstTab = screen.getByText('First Session').closest('.tab-bar-hit-area')
expect(firstTab).toBeTruthy()
fireEvent.mouseDown(firstTab!, { button: 0, clientX: 20, clientY: 10 })
fireEvent.mouseUp(window)
fireEvent.click(firstTab!)
expect(useTabStore.getState().tabs.map((tab) => tab.sessionId)).toEqual(['tab-1', 'tab-2'])
expect(useTabStore.getState().activeTabId).toBe('tab-1')
})
it('closes a tab from the close button without activating drag behavior', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
const disconnectSession = vi.fn()
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'First Session', type: 'session', status: 'idle' },
{ sessionId: 'tab-2', title: 'Second Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-2',
})
useChatStore.setState({
sessions: {},
disconnectSession,
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
const firstTab = screen.getByText('First Session').closest('.tab-bar-hit-area')
const closeButton = screen.getByLabelText('Close First Session')
expect(firstTab).toHaveClass('group')
fireEvent.mouseDown(closeButton, { button: 0, clientX: 20, clientY: 10 })
fireEvent.click(closeButton)
fireEvent.mouseMove(window, { clientX: 260, clientY: 10 })
fireEvent.mouseUp(window)
expect(disconnectSession).toHaveBeenCalledWith('tab-1')
expect(useTabStore.getState().tabs.map((tab) => tab.sessionId)).toEqual(['tab-2'])
expect(useTabStore.getState().activeTabId).toBe('tab-2')
})
it('closes terminal tabs without disconnecting chat sessions', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
const disconnectSession = vi.fn()
useTabStore.setState({
tabs: [
{ sessionId: '__terminal__1', title: 'Terminal 1', type: 'terminal', status: 'idle' },
],
activeTabId: '__terminal__1',
})
useChatStore.setState({
sessions: {},
disconnectSession,
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
expect(screen.getByLabelText('Open Terminal')).toBeInTheDocument()
fireEvent.click(screen.getByLabelText('Close Terminal 1'))
expect(disconnectSession).not.toHaveBeenCalled()
expect(useTabStore.getState().tabs).toEqual([])
})
it('opens the bottom terminal panel from the toolbar for an active session', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
const { useTerminalPanelStore } = await import('../../stores/terminalPanelStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'First Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
fireEvent.click(screen.getByRole('button', { name: 'Open Terminal' }))
const terminalTabs = useTabStore.getState().tabs.filter((tab) => tab.type === 'terminal')
expect(terminalTabs).toHaveLength(0)
expect(useTerminalPanelStore.getState().isPanelOpen('tab-1')).toBe(true)
})
it('treats legacy session tabs without a type as bottom-panel terminal targets', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
const { useTerminalPanelStore } = await import('../../stores/terminalPanelStore')
useTabStore.setState({
tabs: [
{ sessionId: 'legacy-session', title: 'Legacy Session', status: 'idle' } as ReturnType<typeof useTabStore.getState>['tabs'][number],
],
activeTabId: 'legacy-session',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
fireEvent.click(screen.getByRole('button', { name: 'Open Terminal' }))
expect(useTabStore.getState().tabs.some((tab) => tab.type === 'terminal')).toBe(false)
expect(useTerminalPanelStore.getState().isPanelOpen('legacy-session')).toBe(true)
})
it('toggles the workspace panel for the active session from the toolbar', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
const { useWorkspacePanelStore } = await import('../../stores/workspacePanelStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'First Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
fireEvent.click(screen.getByRole('button', { name: 'Show Workspace' }))
expect(useWorkspacePanelStore.getState().isPanelOpen('tab-1')).toBe(true)
fireEvent.click(screen.getByRole('button', { name: 'Hide Workspace' }))
expect(useWorkspacePanelStore.getState().isPanelOpen('tab-1')).toBe(false)
})
it('hides the workspace toolbar button for non-session tabs', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
useTabStore.setState({
tabs: [
{ sessionId: '__terminal__1', title: 'Terminal 1', type: 'terminal', status: 'idle' },
{ sessionId: '__settings__', title: 'Settings', type: 'settings', status: 'idle' },
],
activeTabId: '__terminal__1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
const { rerender } = render(<TabBar />)
expect(screen.queryByRole('button', { name: 'Show Workspace' })).not.toBeInTheDocument()
await act(async () => {
useTabStore.getState().setActiveTab('__settings__')
})
rerender(<TabBar />)
expect(screen.queryByRole('button', { name: 'Show Workspace' })).not.toBeInTheDocument()
})
it('clears session panel state when closing a session tab', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
const { useWorkspacePanelStore } = await import('../../stores/workspacePanelStore')
const { useTerminalPanelStore } = await import('../../stores/terminalPanelStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'First Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
useWorkspacePanelStore.getState().openPanel('tab-1')
useTerminalPanelStore.getState().openPanel('tab-1')
await act(async () => {
render(<TabBar />)
})
fireEvent.click(screen.getByLabelText('Close First Session'))
expect(useWorkspacePanelStore.getState().panelBySession['tab-1']).toBeUndefined()
expect(useTerminalPanelStore.getState().panelBySession['tab-1']).toBeUndefined()
})
})