mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
- Renders tabs from useFileStore.openTabs with title + dirty dot + close button - Uses Zustand selectors for granular re-renders - Active tab highlighted with aria-current and accent background - Close button stops propagation; does not trigger tab activation - Horizontal scroll on overflow, h-9 fixed bar with border - Empty state preserved when no tabs open Tests: 6 cases (empty, render, highlight, click-switch, click-close, dirty indicator) - 75 total tests (was 70, gained 5 new) - All pass - Build: succeeds Amit Haridas
94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { render, screen, act } from '@testing-library/react';
|
|
import { TabBar } from '@/components/layout/TabBar';
|
|
import { useFileStore } from '@/stores/file-store';
|
|
|
|
describe('TabBar', () => {
|
|
beforeEach(() => {
|
|
useFileStore.setState({ openTabs: [], activeTabId: null });
|
|
});
|
|
|
|
it('renders "No files open" when openTabs is empty', () => {
|
|
render(<TabBar />);
|
|
expect(screen.getByText(/no files open/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders a tab for each OpenTab', () => {
|
|
useFileStore.setState({
|
|
openTabs: [
|
|
{ id: '/tmp/foo.md', path: '/tmp/foo.md', title: 'foo.md', dirty: false },
|
|
{ id: '/tmp/bar.md', path: '/tmp/bar.md', title: 'bar.md', dirty: false },
|
|
],
|
|
activeTabId: '/tmp/foo.md',
|
|
});
|
|
render(<TabBar />);
|
|
expect(screen.getByText('foo.md')).toBeInTheDocument();
|
|
expect(screen.getByText('bar.md')).toBeInTheDocument();
|
|
});
|
|
|
|
it('highlights the active tab with aria-current', () => {
|
|
useFileStore.setState({
|
|
openTabs: [
|
|
{ id: '/tmp/foo.md', path: '/tmp/foo.md', title: 'foo.md', dirty: false },
|
|
{ id: '/tmp/bar.md', path: '/tmp/bar.md', title: 'bar.md', dirty: false },
|
|
],
|
|
activeTabId: '/tmp/foo.md',
|
|
});
|
|
render(<TabBar />);
|
|
const fooTab = screen.getByText('foo.md').closest('[role="tab"]');
|
|
expect(fooTab).toHaveAttribute('aria-current', 'page');
|
|
const barTab = screen.getByText('bar.md').closest('[role="tab"]');
|
|
expect(barTab).not.toHaveAttribute('aria-current');
|
|
});
|
|
|
|
it('clicking a tab calls setActiveTab', async () => {
|
|
useFileStore.setState({
|
|
openTabs: [
|
|
{ id: '/tmp/foo.md', path: '/tmp/foo.md', title: 'foo.md', dirty: false },
|
|
{ id: '/tmp/bar.md', path: '/tmp/bar.md', title: 'bar.md', dirty: false },
|
|
],
|
|
activeTabId: '/tmp/foo.md',
|
|
});
|
|
render(<TabBar />);
|
|
await act(async () => {
|
|
screen.getByText('bar.md').closest('[role="tab"]')!.click();
|
|
});
|
|
expect(useFileStore.getState().activeTabId).toBe('/tmp/bar.md');
|
|
});
|
|
|
|
it('clicking the close button calls closeTab and does not call setActiveTab separately', async () => {
|
|
// Use a non-active tab to test that closeTab doesn't change activeTabId
|
|
useFileStore.setState({
|
|
openTabs: [
|
|
{ id: '/tmp/foo.md', path: '/tmp/foo.md', title: 'foo.md', dirty: false },
|
|
{ id: '/tmp/bar.md', path: '/tmp/bar.md', title: 'bar.md', dirty: false },
|
|
],
|
|
activeTabId: '/tmp/bar.md',
|
|
});
|
|
render(<TabBar />);
|
|
|
|
const fooTab = screen.getByText('foo.md');
|
|
const closeBtn = fooTab.closest('[role="tab"]')!.querySelector('button')!;
|
|
await act(async () => {
|
|
closeBtn.click();
|
|
});
|
|
|
|
const state = useFileStore.getState();
|
|
expect(state.openTabs.find((t) => t.id === '/tmp/foo.md')).toBeUndefined();
|
|
// activeTabId stays on bar since we closed a non-active tab
|
|
expect(state.activeTabId).toBe('/tmp/bar.md');
|
|
});
|
|
|
|
it('renders a dirty indicator when dirty === true', () => {
|
|
useFileStore.setState({
|
|
openTabs: [
|
|
{ id: '/tmp/foo.md', path: '/tmp/foo.md', title: 'foo.md', dirty: true },
|
|
],
|
|
activeTabId: '/tmp/foo.md',
|
|
});
|
|
render(<TabBar />);
|
|
const dirtyDot = screen.getByLabelText('Unsaved changes');
|
|
expect(dirtyDot).toBeInTheDocument();
|
|
});
|
|
});
|