import '@testing-library/jest-dom' import { fireEvent, render } from '@testing-library/react' import { axe, toHaveNoViolations } from 'jest-axe' import { vi } from 'vitest' import { PktTabItem, PktTabs } from './Tabs' expect.extend(toHaveNoViolations) describe('Tabs component', () => { const tabs = [ { text: 'Tab 1', href: '#', action: vi.fn() }, { text: 'Tab 2', action: vi.fn() }, ] it('renders tabs with links and buttons', () => { const { getByText } = render() tabs.forEach((tab) => { const tabElement = getByText(tab.text) expect(tabElement).toBeInTheDocument() if (tab.href) { expect(tabElement.tagName).toBe('A') } else { expect(tabElement.tagName).toBe('BUTTON') } }) }) it('calls action function when a tab is clicked', () => { const { getByText } = render() tabs.forEach((tab, index) => { const tabElement = getByText(tab.text) fireEvent.click(tabElement) expect(tab.action).toHaveBeenCalledWith(index) }) }) it('calls onTabSelected prop when a tab is clicked', () => { const onTabSelected = vi.fn() const { getByText } = render() tabs.forEach((tab, index) => { const tabElement = getByText(tab.text) fireEvent.click(tabElement) expect(onTabSelected).toHaveBeenCalledWith(index) }) }) describe('accessibility', () => { it('renders with no wcag errors with axe', async () => { const tabs = [ { text: 'Tab 1', href: '/page1', action: vi.fn() }, { text: 'Tab 2', action: vi.fn() }, ] const { container } = render() const results = await axe(container) expect(results).toHaveNoViolations() }) }) }) describe('PktTabItem children format', () => { it('renders tabs from children', () => { const { getByText } = render( First Tab Second Tab Third Tab , ) expect(getByText('First Tab')).toBeInTheDocument() expect(getByText('Second Tab')).toBeInTheDocument() expect(getByText('Third Tab')).toBeInTheDocument() }) it('applies active class to the active tab', () => { const { getByText } = render( First Tab Second Tab Third Tab , ) expect(getByText('Second Tab')).toHaveClass('active') expect(getByText('First Tab')).not.toHaveClass('active') expect(getByText('Third Tab')).not.toHaveClass('active') }) it('calls onClick handler when tab is clicked', () => { const handleClick = vi.fn() const { getByText } = render( First Tab Second Tab Third Tab , ) fireEvent.click(getByText('Second Tab')) expect(handleClick).toHaveBeenCalled() expect(handleClick.mock.calls[0][0]).toHaveProperty('type', 'click') }) it('calls onTabSelected when tab is clicked', () => { const handleTabSelected = vi.fn() const { getByText } = render( First Tab Second Tab Third Tab , ) fireEvent.click(getByText('Second Tab')) expect(handleTabSelected).toHaveBeenCalledWith(1) }) it('calls both onClick and onTabSelected when tab is clicked', () => { const handleClick = vi.fn() const handleTabSelected = vi.fn() const { getByText } = render( First Tab Second Tab Third Tab , ) fireEvent.click(getByText('Second Tab')) expect(handleClick).toHaveBeenCalled() expect(handleTabSelected).toHaveBeenCalledWith(1) }) it('renders as button when no href is provided', () => { const { getByText } = render( First Tab , ) const tab = getByText('First Tab') expect(tab.tagName).toBe('BUTTON') }) it('renders as link when href is provided', () => { const { getByText } = render( First Tab , ) const tab = getByText('First Tab') expect(tab.tagName).toBe('A') expect(tab).toHaveAttribute('href', '/first') }) it('renders icon when icon prop is provided', () => { const { container } = render( First Tab , ) const button = container.querySelector('.pkt-tabs__button') expect(button).toBeInTheDocument() const iconElement = container.querySelector('.pkt-icon--small') expect(iconElement).toBeInTheDocument() }) it('renders tag when tag and tagSkin props are provided', () => { const { getByText } = render( First Tab , ) expect(getByText('New')).toBeInTheDocument() }) it('handles keyboard navigation with ArrowRight', () => { const { getByText } = render( First Tab Second Tab Third Tab , ) const firstTab = getByText('First Tab') const secondTab = getByText('Second Tab') firstTab.focus() fireEvent.keyDown(firstTab, { key: 'ArrowRight' }) expect(secondTab).toHaveFocus() }) it('handles keyboard navigation with ArrowLeft', () => { const { getByText } = render( First Tab Second Tab Third Tab , ) const firstTab = getByText('First Tab') const secondTab = getByText('Second Tab') secondTab.focus() fireEvent.keyDown(secondTab, { key: 'ArrowLeft' }) expect(firstTab).toHaveFocus() }) it('stays on last tab when navigating past it with ArrowRight', () => { const { getByText } = render( First Tab Second Tab Third Tab , ) const thirdTab = getByText('Third Tab') thirdTab.focus() fireEvent.keyDown(thirdTab, { key: 'ArrowRight' }) expect(thirdTab).toHaveFocus() }) it('stays on first tab when navigating before it with ArrowLeft', () => { const { getByText } = render( First Tab Second Tab Third Tab , ) const firstTab = getByText('First Tab') firstTab.focus() fireEvent.keyDown(firstTab, { key: 'ArrowLeft' }) expect(firstTab).toHaveFocus() }) it('selects tab when Space key is pressed', () => { const handleTabSelected = vi.fn() const { getByText } = render( First Tab Second Tab , ) const secondTab = getByText('Second Tab') fireEvent.keyDown(secondTab, { key: ' ' }) expect(handleTabSelected).toHaveBeenCalledWith(1) }) it('selects tab when ArrowDown key is pressed', () => { const handleTabSelected = vi.fn() const { getByText } = render( First Tab Second Tab , ) const secondTab = getByText('Second Tab') fireEvent.keyDown(secondTab, { key: 'ArrowDown' }) expect(handleTabSelected).toHaveBeenCalledWith(1) }) it('disables keyboard navigation when arrowNav is false', () => { const { getByText } = render( First Tab Second Tab , ) const firstTab = getByText('First Tab') const secondTab = getByText('Second Tab') firstTab.focus() fireEvent.keyDown(firstTab, { key: 'ArrowRight' }) expect(secondTab).not.toHaveFocus() }) it('disables keyboard navigation when disableArrowNav is true', () => { const { getByText } = render( First Tab Second Tab , ) const firstTab = getByText('First Tab') const secondTab = getByText('Second Tab') firstTab.focus() fireEvent.keyDown(firstTab, { key: 'ArrowRight' }) expect(secondTab).not.toHaveFocus() }) it('disableArrowNav overrides arrowNav when both are set', () => { const { getByText } = render( First Tab Second Tab , ) const firstTab = getByText('First Tab') const secondTab = getByText('Second Tab') // Even though arrowNav is true, disableArrowNav should override it firstTab.focus() fireEvent.keyDown(firstTab, { key: 'ArrowRight' }) expect(secondTab).not.toHaveFocus() }) it('works with mapped children when explicit index is provided', () => { const items = ['First', 'Second', 'Third'] const handleTabSelected = vi.fn() const { getByText } = render( {items.map((item, i) => ( {item} ))} , ) fireEvent.click(getByText('Second')) expect(handleTabSelected).toHaveBeenCalledWith(1) fireEvent.click(getByText('Third')) expect(handleTabSelected).toHaveBeenCalledWith(2) }) describe('Disabled tabs', () => { it('does not call onTabSelected or action for disabled button tab from tabs prop', () => { const onTabSelected = vi.fn() const action = vi.fn() const { getByText } = render( , ) fireEvent.click(getByText('Disabled')) expect(action).not.toHaveBeenCalled() expect(onTabSelected).not.toHaveBeenCalled() }) it('renders disabled button tab as native disabled', () => { const { getByText } = render( Disabled button , ) expect(getByText('Disabled button')).toBeDisabled() }) it('renders disabled link tab with aria-disabled and blocks interaction', () => { const handleTabSelected = vi.fn() const handleClick = vi.fn() const { getByText } = render( Enabled link Disabled link , ) const disabledLink = getByText('Disabled link') fireEvent.click(disabledLink) fireEvent.keyDown(disabledLink, { key: 'Enter' }) expect(disabledLink).toHaveAttribute('aria-disabled', 'true') expect(disabledLink).toHaveAttribute('tabindex', '-1') expect(disabledLink).not.toHaveAttribute('href') expect(handleClick).not.toHaveBeenCalled() expect(handleTabSelected).not.toHaveBeenCalled() }) it('skips disabled tabs during arrow navigation', () => { const { getByText } = render( First Disabled Third , ) const first = getByText('First') const third = getByText('Third') first.focus() fireEvent.keyDown(first, { key: 'ArrowRight' }) expect(third).toHaveFocus() }) it('prevents disabled active tab from being selectable', () => { const handleTabSelected = vi.fn() const { getByText } = render( Disabled active Enabled , ) const disabledTab = getByText('Disabled active') expect(disabledTab).toHaveAttribute('aria-selected', 'false') fireEvent.keyDown(disabledTab, { key: 'Enter' }) expect(handleTabSelected).not.toHaveBeenCalled() }) }) describe('Accessibility', () => { it('should not have any accessibility violations', async () => { const { container } = render( First Tab Second Tab Third Tab , ) const results = await axe(container) expect(results).toHaveNoViolations() }) it('sets correct ARIA attributes on buttons', () => { const { getByText } = render( First Tab Second Tab , ) const firstTab = getByText('First Tab') const secondTab = getByText('Second Tab') expect(firstTab).toHaveAttribute('role', 'tab') expect(firstTab).toHaveAttribute('aria-selected', 'true') expect(secondTab).toHaveAttribute('role', 'tab') expect(secondTab).toHaveAttribute('aria-selected', 'false') }) it('does not set tab role or aria-selected on links when arrowNav is false', () => { const { getByText } = render( Home About , ) const homeLink = getByText('Home') const aboutLink = getByText('About') expect(homeLink).not.toHaveAttribute('role') expect(homeLink).not.toHaveAttribute('aria-selected') expect(aboutLink).not.toHaveAttribute('role') expect(aboutLink).not.toHaveAttribute('aria-selected') }) it('works with href links when arrowNav is false (no WCAG violations)', async () => { const { container } = render( Home About , ) const results = await axe(container) expect(results).toHaveNoViolations() }) }) })