| |
| |
| |
| |
| |
|
|
| import { describe, it, expect } from 'vitest'; |
| import { renderWithProviders } from '../../../test-utils/render.js'; |
| import { TabHeader, type Tab } from './TabHeader.js'; |
|
|
| const MOCK_TABS: Tab[] = [ |
| { key: '0', header: 'Tab 1' }, |
| { key: '1', header: 'Tab 2' }, |
| { key: '2', header: 'Tab 3' }, |
| ]; |
|
|
| describe('TabHeader', () => { |
| describe('rendering', () => { |
| it('renders null for single tab', async () => { |
| const { lastFrame, unmount } = await renderWithProviders( |
| <TabHeader |
| tabs={[{ key: '0', header: 'Only Tab' }]} |
| currentIndex={0} |
| />, |
| ); |
| expect(lastFrame({ allowEmpty: true })).toBe(''); |
| unmount(); |
| }); |
|
|
| it('renders all tab headers', async () => { |
| const { lastFrame, unmount } = await renderWithProviders( |
| <TabHeader tabs={MOCK_TABS} currentIndex={0} />, |
| ); |
| const frame = lastFrame(); |
| expect(frame).toContain('Tab 1'); |
| expect(frame).toContain('Tab 2'); |
| expect(frame).toContain('Tab 3'); |
| expect(frame).toMatchSnapshot(); |
| unmount(); |
| }); |
|
|
| it('renders separators between tabs', async () => { |
| const { lastFrame, unmount } = await renderWithProviders( |
| <TabHeader tabs={MOCK_TABS} currentIndex={0} />, |
| ); |
| const frame = lastFrame(); |
| |
| const separatorCount = (frame?.match(/│/g) || []).length; |
| expect(separatorCount).toBe(2); |
| expect(frame).toMatchSnapshot(); |
| unmount(); |
| }); |
| }); |
|
|
| describe('arrows', () => { |
| it('shows arrows by default', async () => { |
| const { lastFrame, unmount } = await renderWithProviders( |
| <TabHeader tabs={MOCK_TABS} currentIndex={0} />, |
| ); |
| const frame = lastFrame(); |
| expect(frame).toContain('←'); |
| expect(frame).toContain('→'); |
| expect(frame).toMatchSnapshot(); |
| unmount(); |
| }); |
|
|
| it('hides arrows when showArrows is false', async () => { |
| const { lastFrame, unmount } = await renderWithProviders( |
| <TabHeader tabs={MOCK_TABS} currentIndex={0} showArrows={false} />, |
| ); |
| const frame = lastFrame(); |
| expect(frame).not.toContain('←'); |
| expect(frame).not.toContain('→'); |
| expect(frame).toMatchSnapshot(); |
| unmount(); |
| }); |
| }); |
|
|
| describe('status icons', () => { |
| it('shows status icons by default', async () => { |
| const { lastFrame, unmount } = await renderWithProviders( |
| <TabHeader tabs={MOCK_TABS} currentIndex={0} />, |
| ); |
| const frame = lastFrame(); |
| |
| expect(frame).toContain('□'); |
| expect(frame).toMatchSnapshot(); |
| unmount(); |
| }); |
|
|
| it('hides status icons when showStatusIcons is false', async () => { |
| const { lastFrame, unmount } = await renderWithProviders( |
| <TabHeader tabs={MOCK_TABS} currentIndex={0} showStatusIcons={false} />, |
| ); |
| const frame = lastFrame(); |
| expect(frame).not.toContain('□'); |
| expect(frame).not.toContain('✓'); |
| expect(frame).toMatchSnapshot(); |
| unmount(); |
| }); |
|
|
| it('shows checkmark for completed tabs', async () => { |
| const { lastFrame, unmount } = await renderWithProviders( |
| <TabHeader |
| tabs={MOCK_TABS} |
| currentIndex={0} |
| completedIndices={new Set([0, 2])} |
| />, |
| ); |
| const frame = lastFrame(); |
| |
| const checkmarkCount = (frame?.match(/✓/g) || []).length; |
| const boxCount = (frame?.match(/□/g) || []).length; |
| expect(checkmarkCount).toBe(2); |
| expect(boxCount).toBe(1); |
| expect(frame).toMatchSnapshot(); |
| unmount(); |
| }); |
|
|
| it('shows special icon for special tabs', async () => { |
| const tabsWithSpecial: Tab[] = [ |
| { key: '0', header: 'Tab 1' }, |
| { key: '1', header: 'Review', isSpecial: true }, |
| ]; |
| const { lastFrame, unmount } = await renderWithProviders( |
| <TabHeader tabs={tabsWithSpecial} currentIndex={0} />, |
| ); |
| const frame = lastFrame(); |
| |
| expect(frame).toContain('≡'); |
| expect(frame).toMatchSnapshot(); |
| unmount(); |
| }); |
|
|
| it('uses tab statusIcon when provided', async () => { |
| const tabsWithCustomIcon: Tab[] = [ |
| { key: '0', header: 'Tab 1', statusIcon: '★' }, |
| { key: '1', header: 'Tab 2' }, |
| ]; |
| const { lastFrame, unmount } = await renderWithProviders( |
| <TabHeader tabs={tabsWithCustomIcon} currentIndex={0} />, |
| ); |
| const frame = lastFrame(); |
| expect(frame).toContain('★'); |
| expect(frame).toMatchSnapshot(); |
| unmount(); |
| }); |
|
|
| it('uses custom renderStatusIcon when provided', async () => { |
| const renderStatusIcon = () => '•'; |
| const { lastFrame, unmount } = await renderWithProviders( |
| <TabHeader |
| tabs={MOCK_TABS} |
| currentIndex={0} |
| renderStatusIcon={renderStatusIcon} |
| />, |
| ); |
| const frame = lastFrame(); |
| const bulletCount = (frame?.match(/•/g) || []).length; |
| expect(bulletCount).toBe(3); |
| expect(frame).toMatchSnapshot(); |
| unmount(); |
| }); |
|
|
| it('truncates long headers when not selected', async () => { |
| const longTabs: Tab[] = [ |
| { key: '0', header: 'ThisIsAVeryLongHeaderThatShouldBeTruncated' }, |
| { key: '1', header: 'AnotherVeryLongHeader' }, |
| ]; |
| const { lastFrame, unmount } = await renderWithProviders( |
| <TabHeader tabs={longTabs} currentIndex={0} />, |
| ); |
| const frame = lastFrame(); |
|
|
| |
| expect(frame).toContain('ThisIsAVeryLongHeaderThatShouldBeTruncated'); |
|
|
| |
| const expectedTruncated = 'AnotherVeryLong…'; |
| expect(frame).toContain(expectedTruncated); |
| expect(frame).not.toContain('AnotherVeryLongHeader'); |
|
|
| unmount(); |
| }); |
|
|
| it('falls back to default when renderStatusIcon returns undefined', async () => { |
| const renderStatusIcon = () => undefined; |
| const { lastFrame, unmount } = await renderWithProviders( |
| <TabHeader |
| tabs={MOCK_TABS} |
| currentIndex={0} |
| renderStatusIcon={renderStatusIcon} |
| />, |
| ); |
| const frame = lastFrame(); |
| expect(frame).toContain('□'); |
| expect(frame).toMatchSnapshot(); |
| unmount(); |
| }); |
| }); |
| }); |
|
|