Skip to content

Commit 8221712

Browse files
test: add Tab component tests and a11y coverage
- 11 component tests covering TabRoot, TabList, TabItem, TabPanel (ARIA attributes, keyboard navigation, selection, show/hide) - 2 axe a11y tests for Tab compound component - Register Tab components in a11y coverage spec Co-Authored-By: Alec Lloyd Probert <graphieros@users.noreply.github.com>
1 parent 315eda2 commit 8221712

File tree

2 files changed

+209
-0
lines changed

2 files changed

+209
-0
lines changed

test/nuxt/a11y.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ import {
234234
PackageSelectionCheckbox,
235235
PackageExternalLinks,
236236
ChartSplitSparkline,
237+
TabRoot,
238+
TabList,
239+
TabItem,
240+
TabPanel,
237241
} from '#components'
238242

239243
// Server variant components must be imported directly to test the server-side render
@@ -1000,6 +1004,40 @@ describe('component accessibility audits', () => {
10001004
})
10011005
})
10021006

1007+
describe('TabRoot + TabList + TabItem + TabPanel', () => {
1008+
function createTabsFixture(modelValue: string, idPrefix: string) {
1009+
return defineComponent({
1010+
setup() {
1011+
return () =>
1012+
h(
1013+
TabRoot,
1014+
{ modelValue, idPrefix },
1015+
() => [
1016+
h(TabList, { ariaLabel: 'Test tabs' }, () => [
1017+
h(TabItem, { value: 'first' }, () => 'First'),
1018+
h(TabItem, { value: 'second' }, () => 'Second'),
1019+
]),
1020+
h(TabPanel, { value: 'first' }, () => 'First content'),
1021+
h(TabPanel, { value: 'second' }, () => 'Second content'),
1022+
],
1023+
)
1024+
},
1025+
})
1026+
}
1027+
1028+
it('should have no accessibility violations', async () => {
1029+
const component = await mountSuspended(createTabsFixture('first', 'a11y-test'))
1030+
const results = await runAxe(component)
1031+
expect(results.violations).toEqual([])
1032+
})
1033+
1034+
it('should have no accessibility violations with second tab selected', async () => {
1035+
const component = await mountSuspended(createTabsFixture('second', 'a11y-test2'))
1036+
const results = await runAxe(component)
1037+
expect(results.violations).toEqual([])
1038+
})
1039+
})
1040+
10031041
describe('PackagePlaygrounds', () => {
10041042
it('should have no accessibility violations with single link', async () => {
10051043
const links = [

test/nuxt/components/Tab.spec.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { mountSuspended } from '@nuxt/test-utils/runtime'
3+
import { defineComponent, h, nextTick } from 'vue'
4+
import { TabRoot, TabList, TabItem, TabPanel } from '#components'
5+
6+
function createTabsWrapper(props: { modelValue: string; idPrefix?: string }) {
7+
return defineComponent({
8+
components: { TabRoot, TabList, TabItem, TabPanel },
9+
setup() {
10+
return () =>
11+
h(
12+
TabRoot,
13+
({
14+
modelValue: props.modelValue,
15+
idPrefix: props.idPrefix ?? 'test',
16+
'onUpdate:modelValue': () => {}
17+
}),
18+
() => [
19+
h(TabList, { ariaLabel: 'Test tabs' }, () => [
20+
h(TabItem, { value: 'one' }, () => 'One'),
21+
h(TabItem, { value: 'two' }, () => 'Two'),
22+
h(TabItem, { value: 'three' }, () => 'Three'),
23+
]),
24+
h(TabPanel, { value: 'one' }, () => 'Content one'),
25+
h(TabPanel, { value: 'two' }, () => 'Content two'),
26+
h(TabPanel, { value: 'three' }, () => 'Content three'),
27+
],
28+
)
29+
},
30+
})
31+
}
32+
33+
async function mountTabs({ modelValue = 'one', idPrefix = 'test' } = {}) {
34+
const Wrapper = createTabsWrapper({ modelValue, idPrefix })
35+
return mountSuspended(Wrapper, { attachTo: document.body })
36+
}
37+
38+
describe('Tab components', () => {
39+
describe('TabRoot', () => {
40+
it('renders tablist', async () => {
41+
const wrapper = await mountTabs()
42+
expect(wrapper.find('[role="tablist"]').exists()).toBe(true)
43+
wrapper.unmount()
44+
})
45+
46+
it('provides selected value to children', async () => {
47+
const wrapper = await mountTabs({ modelValue: 'two' })
48+
const tabs = wrapper.findAll('[role="tab"]')
49+
const selected = tabs.find(t => t.attributes('aria-selected') === 'true')
50+
expect(selected?.text()).toBe('Two')
51+
wrapper.unmount()
52+
})
53+
})
54+
55+
describe('TabList', () => {
56+
it('has tablist role and aria-label', async () => {
57+
const wrapper = await mountTabs()
58+
const tablist = wrapper.find('[role="tablist"]')
59+
expect(tablist.exists()).toBe(true)
60+
expect(tablist.attributes('aria-label')).toBe('Test tabs')
61+
wrapper.unmount()
62+
})
63+
64+
it('supports arrow key navigation', async () => {
65+
const wrapper = await mountTabs()
66+
const tablist = wrapper.find('[role="tablist"]')
67+
const tabs = wrapper.findAll('[role="tab"]')
68+
69+
;(tabs[0]!.element as HTMLElement).focus()
70+
expect(document.activeElement).toBe(tabs[0]!.element)
71+
72+
await tablist.trigger('keydown', { key: 'ArrowRight' })
73+
expect(document.activeElement).toBe(tabs[1]!.element)
74+
75+
// Wraps around
76+
await tablist.trigger('keydown', { key: 'ArrowRight' })
77+
await tablist.trigger('keydown', { key: 'ArrowRight' })
78+
expect(document.activeElement).toBe(tabs[0]!.element)
79+
80+
// ArrowLeft wraps backwards
81+
await tablist.trigger('keydown', { key: 'ArrowLeft' })
82+
expect(document.activeElement).toBe(tabs[2]!.element)
83+
84+
wrapper.unmount()
85+
})
86+
87+
it('supports Home and End keys', async () => {
88+
const wrapper = await mountTabs()
89+
const tablist = wrapper.find('[role="tablist"]')
90+
const tabs = wrapper.findAll('[role="tab"]')
91+
92+
;(tabs[1]!.element as HTMLElement).focus()
93+
94+
await tablist.trigger('keydown', { key: 'Home' })
95+
expect(document.activeElement).toBe(tabs[0]!.element)
96+
97+
await tablist.trigger('keydown', { key: 'End' })
98+
expect(document.activeElement).toBe(tabs[2]!.element)
99+
100+
wrapper.unmount()
101+
})
102+
})
103+
104+
describe('TabItem', () => {
105+
it('has correct ARIA attributes when selected', async () => {
106+
const wrapper = await mountTabs({ modelValue: 'one' })
107+
const tabs = wrapper.findAll('[role="tab"]')
108+
const first = tabs[0]!
109+
const second = tabs[1]!
110+
111+
expect(first.attributes('aria-selected')).toBe('true')
112+
expect(first.attributes('tabindex')).toBe('-1')
113+
expect(first.attributes('data-selected')).toBeDefined()
114+
115+
expect(second.attributes('aria-selected')).toBe('false')
116+
expect(second.attributes('tabindex')).toBe('0')
117+
expect(second.attributes('data-selected')).toBeUndefined()
118+
119+
wrapper.unmount()
120+
})
121+
122+
it('emits update:modelValue on click', async () => {
123+
const wrapper = await mountTabs({ modelValue: 'one' })
124+
const tabs = wrapper.findAll('[role="tab"]')
125+
126+
await tabs[1]!.trigger('click')
127+
const root = wrapper.findComponent(TabRoot)
128+
expect(root.emitted('update:modelValue')?.[0]).toEqual(['two'])
129+
130+
wrapper.unmount()
131+
})
132+
133+
it('generates aria-controls pointing to panel id', async () => {
134+
const wrapper = await mountTabs({ idPrefix: 'my-tabs' })
135+
const tab = wrapper.findAll('[role="tab"]')[0]!
136+
expect(tab.attributes('aria-controls')).toBe('my-tabs-panel-one')
137+
wrapper.unmount()
138+
})
139+
})
140+
141+
describe('TabPanel', () => {
142+
it('shows panel matching selected value', async () => {
143+
const wrapper = await mountTabs({ modelValue: 'one' })
144+
const panels = wrapper.findAll('[role="tabpanel"]')
145+
146+
const visible = panels.filter(p => (p.element as HTMLElement).style.display !== 'none')
147+
expect(visible).toHaveLength(1)
148+
expect(visible[0]!.text()).toBe('Content one')
149+
150+
wrapper.unmount()
151+
})
152+
153+
it('hides panels not matching selected value', async () => {
154+
const wrapper = await mountTabs({ modelValue: 'two' })
155+
const panels = wrapper.findAll('[role="tabpanel"]')
156+
157+
const visible = panels.filter(p => (p.element as HTMLElement).style.display !== 'none')
158+
expect(visible).toHaveLength(1)
159+
expect(visible[0]!.text()).toBe('Content two')
160+
161+
wrapper.unmount()
162+
})
163+
164+
it('has aria-labelledby pointing to tab id', async () => {
165+
const wrapper = await mountTabs({ idPrefix: 'demo' })
166+
const panel = wrapper.findAll('[role="tabpanel"]')[0]!
167+
expect(panel.attributes('aria-labelledby')).toBe('demo-one')
168+
wrapper.unmount()
169+
})
170+
})
171+
})

0 commit comments

Comments
 (0)