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()
})
})
})