import React from 'react';
import { describe, it, expect, vi } from 'vitest';
import { render, fireEvent } from '@testing-library/react';
import { PixelTabs } from '../../navigation';
import type { TabItem } from '../../common';
const ITEMS: TabItem[] = [
{ id: 'a', label: 'Alpha', content: alpha-content },
{ id: 'b', label: 'Beta', content: beta-content },
{ id: 'c', label: 'Gamma', content: gamma-content },
];
describe('PixelTabs (sugar items API — backward compatibility)', () => {
it('renders all triggers and shows the default panel', () => {
const { getByRole, getAllByRole, queryByText } = render(
,
);
const list = getByRole('tablist');
expect(list).toHaveAttribute('aria-label', 'Tabs');
const tabs = getAllByRole('tab');
expect(tabs).toHaveLength(3);
expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
expect(queryByText('alpha-content')).toBeTruthy();
expect(queryByText('beta-content')).toBeNull();
});
it('switches active tab on click and calls onChange', () => {
const onChange = vi.fn();
const { getByRole, getAllByRole, queryByText } = render(
,
);
const tabs = getAllByRole('tab');
fireEvent.click(tabs[1]);
expect(onChange).toHaveBeenCalledWith('b');
expect(tabs[1]).toHaveAttribute('aria-selected', 'true');
expect(queryByText('beta-content')).toBeTruthy();
expect(queryByText('alpha-content')).toBeNull();
expect(getByRole('tablist')).toHaveAttribute('aria-orientation', 'horizontal');
});
it('supports controlled value', () => {
const onChange = vi.fn();
const { getAllByRole, queryByText, rerender } = render(
,
);
expect(queryByText('alpha-content')).toBeTruthy();
fireEvent.click(getAllByRole('tab')[1]);
expect(onChange).toHaveBeenCalledWith('b');
// Without rerender, controlled stays on 'a'
expect(queryByText('alpha-content')).toBeTruthy();
rerender();
expect(queryByText('beta-content')).toBeTruthy();
});
it('arrow-right + arrow-left rove and activate automatically', () => {
const onChange = vi.fn();
const { getAllByRole } = render(
,
);
const tabs = getAllByRole('tab') as HTMLButtonElement[];
tabs[0].focus();
fireEvent.keyDown(tabs[0], { key: 'ArrowRight' });
expect(document.activeElement).toBe(tabs[1]);
// automatic activation: selection follows focus
expect(onChange).toHaveBeenLastCalledWith('b');
fireEvent.keyDown(tabs[1], { key: 'ArrowLeft' });
expect(document.activeElement).toBe(tabs[0]);
expect(onChange).toHaveBeenLastCalledWith('a');
});
it('Home / End jump to edges', () => {
const { getAllByRole } = render();
const tabs = getAllByRole('tab') as HTMLButtonElement[];
tabs[1].focus();
fireEvent.keyDown(tabs[1], { key: 'End' });
expect(document.activeElement).toBe(tabs[2]);
fireEvent.keyDown(tabs[2], { key: 'Home' });
expect(document.activeElement).toBe(tabs[0]);
});
});
describe('PixelTabs orientation="vertical"', () => {
it('uses arrow-up / arrow-down for navigation and reports aria-orientation', () => {
const onChange = vi.fn();
const { getByRole, getAllByRole } = render(
,
);
expect(getByRole('tablist')).toHaveAttribute('aria-orientation', 'vertical');
const tabs = getAllByRole('tab') as HTMLButtonElement[];
tabs[0].focus();
fireEvent.keyDown(tabs[0], { key: 'ArrowDown' });
expect(document.activeElement).toBe(tabs[1]);
expect(onChange).toHaveBeenLastCalledWith('b');
fireEvent.keyDown(tabs[1], { key: 'ArrowUp' });
expect(document.activeElement).toBe(tabs[0]);
// Horizontal arrows are no-ops in vertical mode (focus must not move).
fireEvent.keyDown(tabs[0], { key: 'ArrowRight' });
expect(document.activeElement).toBe(tabs[0]);
});
});
describe('PixelTabs keepMounted', () => {
it('mounts every panel but hides inactive ones', () => {
const { queryByText, getAllByRole, container } = render(
,
);
// All three panel contents are in the DOM
expect(queryByText('alpha-content')).toBeTruthy();
expect(queryByText('beta-content')).toBeTruthy();
expect(queryByText('gamma-content')).toBeTruthy();
const panels = container.querySelectorAll('[role="tabpanel"]');
expect(panels).toHaveLength(3);
expect(panels[0]).not.toHaveAttribute('hidden');
expect(panels[1]).toHaveAttribute('hidden');
// After switching, hidden flips
fireEvent.click(getAllByRole('tab')[1]);
const panelsAfter = container.querySelectorAll('[role="tabpanel"]');
expect(panelsAfter[0]).toHaveAttribute('hidden');
expect(panelsAfter[1]).not.toHaveAttribute('hidden');
});
it('without keepMounted, only the active panel is in the DOM', () => {
const { container } = render();
const panels = container.querySelectorAll('[role="tabpanel"]');
expect(panels).toHaveLength(1);
});
});
describe('PixelTabs scrollable', () => {
it('marks the tablist as scrollable and applies the fade-mask', () => {
const { getByRole } = render();
const list = getByRole('tablist');
expect(list).toHaveAttribute('data-scrollable', 'true');
expect(list.className).toContain('overflow-x-auto');
expect(list.className).toContain('flex-nowrap');
// jsdom only retains props it understands; scrollbarWidth=none survives.
const style = list.getAttribute('style') || '';
expect(style.toLowerCase()).toContain('scrollbar-width');
});
it('ignores scrollable when orientation is vertical', () => {
const { getByRole } = render(
,
);
const list = getByRole('tablist');
expect(list).not.toHaveAttribute('data-scrollable');
expect(list.className).not.toContain('overflow-x-auto');
});
});
describe('PixelTabs activationMode="manual"', () => {
it('roves focus without changing selection until Enter/Space', () => {
const onChange = vi.fn();
const { getAllByRole } = render(
,
);
const tabs = getAllByRole('tab') as HTMLButtonElement[];
tabs[0].focus();
fireEvent.keyDown(tabs[0], { key: 'ArrowRight' });
expect(document.activeElement).toBe(tabs[1]);
expect(onChange).not.toHaveBeenCalled();
expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
fireEvent.keyDown(tabs[1], { key: 'Enter' });
expect(onChange).toHaveBeenCalledWith('b');
});
});
describe('PixelTabs compositional API', () => {
it('renders List/Trigger/Panel and selects on click', () => {
const onChange = vi.fn();
const { getByRole, getAllByRole, queryByText } = render(
X
Y
x-panel
y-panel
,
);
expect(getByRole('tablist')).toHaveAttribute('aria-label', 'Pages');
expect(queryByText('x-panel')).toBeTruthy();
expect(queryByText('y-panel')).toBeNull();
fireEvent.click(getAllByRole('tab')[1]);
expect(onChange).toHaveBeenCalledWith('y');
expect(queryByText('y-panel')).toBeTruthy();
});
it('compositional panels honor per-panel keepMounted override', () => {
const { container } = render(
X
Y
x-panel
y-panel
,
);
const panels = container.querySelectorAll('[role="tabpanel"]');
expect(panels).toHaveLength(2);
// y is mounted (keepMounted) but hidden
expect(panels[1]).toHaveAttribute('hidden');
});
});