Skip to content

Commit 96d1313

Browse files
feat: add label test (#579)
1 parent 5da4ddb commit 96d1313

File tree

7 files changed

+421
-19
lines changed

7 files changed

+421
-19
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Enter the component you want most in the components, leave the emojis and follow
3737
### Components
3838
### Components
3939

40-
| Component | Status | 🔗 Hook v1 | 👀 Visual Check | 📄 Docs | 📝 Note |
40+
| Component | Status | 🔗 Hook v1 | 👀 Visual Check | 📄 Test | 📝 Note |
4141
| ------------------------------------------------------------------------------------------------ | ------------ | ---------- | --------------- | ------- | ------------------------------ |
4242
| [Accordion](https://vue-primitives.netlify.app/?path=/story/components-accordion--single) | ✅ Completed ||| | |
4343
| [AlertDialog](https://vue-primitives.netlify.app/?path=/story/components-alertdialog--styled) | ✅ Completed || | | |
@@ -50,7 +50,7 @@ Enter the component you want most in the components, leave the emojis and follow
5050
| [DropdownMenu](https://vue-primitives.netlify.app/?path=/story/components-dropdownmenu--styled) | ✅ Completed || | | |
5151
| Form | ❌ Not Started || | | |
5252
| [HoverCard](https://vue-primitives.netlify.app/?path=/story/components-hovercard--chromatic) | ✅ Completed || | | 🔧 Needs polygon; fix close |
53-
| [Label](https://vue-primitives.netlify.app/?path=/story/components-label--styled) | ✅ Completed || | | |
53+
| [Label](https://vue-primitives.netlify.app/?path=/story/components-label--styled) | ✅ Completed || | | |
5454
| [Menubar](https://vue-primitives.netlify.app/?path=/story/components-menubar--styled) | ✅ Completed || | | |
5555
| NavigationMenu | 🚧 In Progress | 🚧 | | | |
5656
| [Popover](https://vue-primitives.netlify.app/?path=/story/components-popover--styled) | ✅ Completed || | | |

packages/core/package.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@
9292
"build-storybook": "storybook build",
9393
"eslint": "eslint .",
9494
"eslint:fix": "eslint . --fix",
95+
"test": "vitest run",
96+
"test:watch": "vitest --watch",
97+
"test:nuxt": "vitest -c vitest.nuxt.config.ts --coverage",
9598
"release": "pnpm build && pnpm publish --no-git-checks --access public",
9699
"release:beta": "pnpm release --tag beta --access public",
97100
"release:alpha": "pnpm release --tag alpha --access public",
@@ -107,6 +110,7 @@
107110
"aria-hidden": "^1.2.4"
108111
},
109112
"devDependencies": {
113+
"@testing-library/vue": "^8.1.0",
110114
"@tsconfig/node20": "^20.1.4",
111115
"@types/jsdom": "^21.1.7",
112116
"@types/node": "^22.9.0",
@@ -126,7 +130,9 @@
126130
"vite-plugin-externalize-deps": "^0.8.0",
127131
"vite-plugin-pages": "^0.32.3",
128132
"vite-tsconfig-paths": "^5.1.3",
129-
"vitest": "^2.1.4",
133+
"vitest": "link:@testing-library/jest-dom/vitest",
134+
"vitest-axe": "1.0.0-pre.3",
135+
"vitest-canvas-mock": "^0.3.3",
130136
"vue": "^3.5.12",
131137
"vue-router": "^4.4.5",
132138
"vue-tsc": "^2.1.10"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import { render, screen } from '@testing-library/vue'
2+
import { mount } from '@vue/test-utils'
3+
import { describe, expect, it, vi } from 'vitest'
4+
import { axe } from 'vitest-axe'
5+
import { Label } from '../index'
6+
import { useLabel } from '../Label'
7+
8+
describe('label', () => {
9+
describe('component', () => {
10+
it('should render correctly', () => {
11+
const wrapper = mount(Label)
12+
expect(wrapper.element.tagName).toBe('LABEL')
13+
})
14+
15+
it('should render with custom tag', () => {
16+
const wrapper = mount(Label, {
17+
props: {
18+
as: 'div',
19+
},
20+
})
21+
expect(wrapper.element.tagName).toBe('DIV')
22+
})
23+
24+
it('should pass through attrs', () => {
25+
const wrapper = mount(Label, {
26+
attrs: {
27+
'id': 'test-label',
28+
'data-testid': 'label',
29+
},
30+
})
31+
expect(wrapper.attributes('id')).toBe('test-label')
32+
expect(wrapper.attributes('data-testid')).toBe('label')
33+
})
34+
35+
it('should emit mousedown event', async () => {
36+
const wrapper = mount(Label)
37+
await wrapper.trigger('mousedown')
38+
expect(wrapper.emitted('mousedown')).toBeTruthy()
39+
})
40+
41+
it('should handle slot content updates', async () => {
42+
const wrapper = mount({
43+
components: { Label },
44+
data() {
45+
return {
46+
content: 'Initial Label',
47+
}
48+
},
49+
template: `<Label>{{ content }}</Label>`,
50+
})
51+
expect(wrapper.text()).toBe('Initial Label')
52+
await wrapper.setData({ content: 'Updated Label' })
53+
expect(wrapper.text()).toBe('Updated Label')
54+
})
55+
56+
it('should handle dynamic class changes', async () => {
57+
const wrapper = mount(Label, {
58+
attrs: {
59+
class: 'initial-class',
60+
},
61+
})
62+
expect(wrapper.classes()).toContain('initial-class')
63+
await wrapper.setProps({ class: 'updated-class' })
64+
expect(wrapper.classes()).toContain('updated-class')
65+
})
66+
67+
it('should handle multiple mousedown events', async () => {
68+
const wrapper = mount(Label)
69+
await wrapper.trigger('mousedown')
70+
await wrapper.trigger('mousedown')
71+
expect(wrapper.emitted('mousedown')).toHaveLength(2)
72+
})
73+
})
74+
75+
describe('useLabel', () => {
76+
it('should prevent text selection on double click', () => {
77+
const mockProps = {
78+
onMousedown: vi.fn(),
79+
}
80+
const { attrs } = useLabel(mockProps)
81+
const div = document.createElement('div')
82+
const event = new MouseEvent('mousedown', {
83+
detail: 2,
84+
bubbles: true,
85+
cancelable: true,
86+
})
87+
Object.defineProperty(event, 'target', { value: div })
88+
event.preventDefault = vi.fn()
89+
90+
const resolvedAttrs = attrs([])
91+
if (resolvedAttrs.onMousedown) {
92+
resolvedAttrs.onMousedown(event)
93+
}
94+
95+
expect(event.preventDefault).toHaveBeenCalled()
96+
expect(mockProps.onMousedown).toHaveBeenCalledWith(event)
97+
})
98+
99+
it('should not prevent mousedown on form controls', () => {
100+
const mockProps = {
101+
onMousedown: vi.fn(),
102+
}
103+
const { attrs } = useLabel(mockProps)
104+
const button = document.createElement('button')
105+
const event = new MouseEvent('mousedown')
106+
Object.defineProperty(event, 'target', { value: button })
107+
108+
const resolvedAttrs = attrs([])
109+
if (resolvedAttrs.onMousedown) {
110+
resolvedAttrs.onMousedown(event)
111+
}
112+
113+
expect(mockProps.onMousedown).not.toHaveBeenCalled()
114+
})
115+
116+
it('should merge extra attrs', () => {
117+
const { attrs } = useLabel()
118+
const extraAttrs = [{
119+
class: 'test-class',
120+
id: 'test-id',
121+
}]
122+
123+
const resolvedAttrs = attrs(extraAttrs)
124+
125+
expect(resolvedAttrs.class).toBe('test-class')
126+
expect(resolvedAttrs.id).toBe('test-id')
127+
expect(resolvedAttrs.onMousedown).toBeDefined()
128+
})
129+
})
130+
131+
describe('form control interactions', () => {
132+
it('should trigger associated input focus on click', async () => {
133+
// Mount using @testing-library/vue
134+
render({
135+
components: { Label },
136+
template: `
137+
<div>
138+
<Label for="test-input">Click me</Label>
139+
<input id="test-input" data-testid="input" type="text" />
140+
</div>
141+
`,
142+
})
143+
144+
const input = screen.getByTestId('input')
145+
const label = screen.getByText('Click me')
146+
147+
// Mock focus method
148+
const focusSpy = vi.spyOn(input, 'focus')
149+
150+
// Simulate the native behavior
151+
label.click()
152+
input.focus()
153+
154+
expect(focusSpy).toHaveBeenCalled()
155+
focusSpy.mockRestore()
156+
})
157+
158+
it('should work with nested form controls', async () => {
159+
const wrapper = mount(Label, {
160+
slots: {
161+
default: '<input type="checkbox" />',
162+
},
163+
})
164+
165+
const checkbox = wrapper.find('input')
166+
await wrapper.trigger('click')
167+
expect(checkbox.element.checked).toBe(true)
168+
})
169+
})
170+
171+
describe('accessibility', () => {
172+
it('should have no accessibility violations', async () => {
173+
const wrapper = mount(Label, {
174+
slots: {
175+
default: 'Test Label',
176+
},
177+
})
178+
179+
const results = await axe(wrapper.element)
180+
expect(results).toHaveNoViolations()
181+
})
182+
183+
it('should have no accessibility violations with custom tag', async () => {
184+
const wrapper = mount(Label, {
185+
props: {
186+
as: 'div',
187+
},
188+
slots: {
189+
default: 'Test Label',
190+
},
191+
})
192+
193+
const results = await axe(wrapper.element)
194+
expect(results).toHaveNoViolations()
195+
})
196+
197+
it('should have no violations when associated with form control', async () => {
198+
const wrapper = mount({
199+
template: `
200+
<div>
201+
<Label for="test-input">Test Label</Label>
202+
<input id="test-input" type="text" />
203+
</div>
204+
`,
205+
components: { Label },
206+
})
207+
208+
const results = await axe(wrapper.element)
209+
expect(results).toHaveNoViolations()
210+
})
211+
212+
it('should maintain accessibility when content changes dynamically', async () => {
213+
const wrapper = mount({
214+
components: { Label },
215+
data() {
216+
return {
217+
content: 'Initial Label',
218+
}
219+
},
220+
template: `
221+
<Label for="dynamic-input">{{ content }}</Label>
222+
`,
223+
})
224+
225+
expect(wrapper.text()).toBe('Initial Label')
226+
await wrapper.setData({ content: 'Updated Label' })
227+
const results = await axe(wrapper.element)
228+
expect(results).toHaveNoViolations()
229+
})
230+
231+
it('should be keyboard navigable', () => {
232+
const wrapper = mount(Label, {
233+
attrs: {
234+
tabindex: '0',
235+
},
236+
})
237+
expect(wrapper.attributes('tabindex')).toBe('0')
238+
})
239+
240+
it('should support aria-labelledby', async () => {
241+
const wrapper = mount({
242+
template: `
243+
<div>
244+
<Label id="label-id">Description</Label>
245+
<div aria-labelledby="label-id">Labeled content</div>
246+
</div>
247+
`,
248+
components: { Label },
249+
})
250+
251+
const results = await axe(wrapper.element)
252+
expect(results).toHaveNoViolations()
253+
})
254+
})
255+
256+
describe('edge cases', () => {
257+
it('should handle empty slots gracefully', () => {
258+
const wrapper = mount(Label)
259+
expect(wrapper.text()).toBe('')
260+
expect(() => wrapper.trigger('mousedown')).not.toThrow()
261+
})
262+
263+
it('should handle malformed for attribute', async () => {
264+
const wrapper = mount(Label, {
265+
props: {
266+
for: 'non-existent-id',
267+
},
268+
})
269+
const results = await axe(wrapper.element)
270+
expect(results).toHaveNoViolations()
271+
})
272+
})
273+
})

packages/core/vitest.config.ts

+39-12
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,41 @@
1+
import { resolve } from 'node:path'
12
import { fileURLToPath } from 'node:url'
2-
import { configDefaults, defineConfig, mergeConfig } from 'vitest/config'
3-
import viteConfig from './vite.config.ts'
3+
import vue from '@vitejs/plugin-vue'
4+
import { defineConfig } from 'vitest/config'
45

5-
export default mergeConfig(
6-
viteConfig,
7-
defineConfig({
8-
test: {
9-
environment: 'jsdom',
10-
exclude: [...configDefaults.exclude, 'e2e/**'],
11-
root: fileURLToPath(new URL('./', import.meta.url)),
12-
},
13-
}),
14-
)
6+
const r = (p: string) => resolve(__dirname, p)
7+
8+
export default defineConfig({
9+
plugins: [vue()],
10+
resolve: {
11+
alias: {
12+
'@': r('./src'),
13+
},
14+
dedupe: [
15+
'vue',
16+
'@vue/runtime-core',
17+
],
18+
},
19+
test: {
20+
environment: 'jsdom',
21+
globals: true,
22+
exclude: ['**/node_modules/**'],
23+
include: ['./**/*.test.{ts,js}'],
24+
coverage: {
25+
provider: 'istanbul', // or 'v8'
26+
},
27+
root: fileURLToPath(new URL('./', import.meta.url)),
28+
globalSetup: './vitest.global.ts',
29+
setupFiles: './vitest.setup.ts',
30+
server: {
31+
deps: {
32+
inline: ['vitest-canvas-mock'],
33+
},
34+
},
35+
environmentOptions: {
36+
jsdom: {
37+
resources: 'usable',
38+
},
39+
},
40+
},
41+
})

packages/core/vitest.global.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function setup() {
2+
process.env.TZ = 'US/Eastern'
3+
}

0 commit comments

Comments
 (0)