mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-12 20:37:08 -07:00
215 lines
6.9 KiB
TypeScript
215 lines
6.9 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
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, waitUntilReady, unmount } = renderWithProviders(
|
|
<TabHeader
|
|
tabs={[{ key: '0', header: 'Only Tab' }]}
|
|
currentIndex={0}
|
|
/>,
|
|
);
|
|
await waitUntilReady();
|
|
expect(lastFrame({ allowEmpty: true })).toBe('');
|
|
unmount();
|
|
});
|
|
|
|
it('renders all tab headers', async () => {
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
|
<TabHeader tabs={MOCK_TABS} currentIndex={0} />,
|
|
);
|
|
await waitUntilReady();
|
|
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, waitUntilReady, unmount } = renderWithProviders(
|
|
<TabHeader tabs={MOCK_TABS} currentIndex={0} />,
|
|
);
|
|
await waitUntilReady();
|
|
const frame = lastFrame();
|
|
// Should have 2 separators for 3 tabs
|
|
const separatorCount = (frame?.match(/│/g) || []).length;
|
|
expect(separatorCount).toBe(2);
|
|
expect(frame).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('arrows', () => {
|
|
it('shows arrows by default', async () => {
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
|
<TabHeader tabs={MOCK_TABS} currentIndex={0} />,
|
|
);
|
|
await waitUntilReady();
|
|
const frame = lastFrame();
|
|
expect(frame).toContain('←');
|
|
expect(frame).toContain('→');
|
|
expect(frame).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
|
|
it('hides arrows when showArrows is false', async () => {
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
|
<TabHeader tabs={MOCK_TABS} currentIndex={0} showArrows={false} />,
|
|
);
|
|
await waitUntilReady();
|
|
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, waitUntilReady, unmount } = renderWithProviders(
|
|
<TabHeader tabs={MOCK_TABS} currentIndex={0} />,
|
|
);
|
|
await waitUntilReady();
|
|
const frame = lastFrame();
|
|
// Default uncompleted icon is □
|
|
expect(frame).toContain('□');
|
|
expect(frame).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
|
|
it('hides status icons when showStatusIcons is false', async () => {
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
|
<TabHeader tabs={MOCK_TABS} currentIndex={0} showStatusIcons={false} />,
|
|
);
|
|
await waitUntilReady();
|
|
const frame = lastFrame();
|
|
expect(frame).not.toContain('□');
|
|
expect(frame).not.toContain('✓');
|
|
expect(frame).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
|
|
it('shows checkmark for completed tabs', async () => {
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
|
<TabHeader
|
|
tabs={MOCK_TABS}
|
|
currentIndex={0}
|
|
completedIndices={new Set([0, 2])}
|
|
/>,
|
|
);
|
|
await waitUntilReady();
|
|
const frame = lastFrame();
|
|
// Should have 2 checkmarks and 1 box
|
|
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, waitUntilReady, unmount } = renderWithProviders(
|
|
<TabHeader tabs={tabsWithSpecial} currentIndex={0} />,
|
|
);
|
|
await waitUntilReady();
|
|
const frame = lastFrame();
|
|
// Special tab shows ≡ icon
|
|
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, waitUntilReady, unmount } = renderWithProviders(
|
|
<TabHeader tabs={tabsWithCustomIcon} currentIndex={0} />,
|
|
);
|
|
await waitUntilReady();
|
|
const frame = lastFrame();
|
|
expect(frame).toContain('★');
|
|
expect(frame).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
|
|
it('uses custom renderStatusIcon when provided', async () => {
|
|
const renderStatusIcon = () => '•';
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
|
<TabHeader
|
|
tabs={MOCK_TABS}
|
|
currentIndex={0}
|
|
renderStatusIcon={renderStatusIcon}
|
|
/>,
|
|
);
|
|
await waitUntilReady();
|
|
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, waitUntilReady, unmount } = renderWithProviders(
|
|
<TabHeader tabs={longTabs} currentIndex={0} />,
|
|
);
|
|
await waitUntilReady();
|
|
const frame = lastFrame();
|
|
|
|
// Current tab (index 0) should NOT be truncated
|
|
expect(frame).toContain('ThisIsAVeryLongHeaderThatShouldBeTruncated');
|
|
|
|
// Inactive tab (index 1) SHOULD be truncated to 16 chars (15 chars + …)
|
|
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, waitUntilReady, unmount } = renderWithProviders(
|
|
<TabHeader
|
|
tabs={MOCK_TABS}
|
|
currentIndex={0}
|
|
renderStatusIcon={renderStatusIcon}
|
|
/>,
|
|
);
|
|
await waitUntilReady();
|
|
const frame = lastFrame();
|
|
expect(frame).toContain('□');
|
|
expect(frame).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
});
|
|
});
|