Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add label test #579

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Enter the component you want most in the components, leave the emojis and follow
### Components
### Components

| Component | Status | 🔗 Hook v1 | 👀 Visual Check | 📄 Docs | 📝 Note |
| Component | Status | 🔗 Hook v1 | 👀 Visual Check | 📄 Test | 📝 Note |
| ------------------------------------------------------------------------------------------------ | ------------ | ---------- | --------------- | ------- | ------------------------------ |
| [Accordion](https://vue-primitives.netlify.app/?path=/story/components-accordion--single) | ✅ Completed | ✅ | ✅ | | |
| [AlertDialog](https://vue-primitives.netlify.app/?path=/story/components-alertdialog--styled) | ✅ Completed | ✅ | | | |
Expand All @@ -50,7 +50,7 @@ Enter the component you want most in the components, leave the emojis and follow
| [DropdownMenu](https://vue-primitives.netlify.app/?path=/story/components-dropdownmenu--styled) | ✅ Completed | ✅ | | | |
| Form | ❌ Not Started | ❌ | | | |
| [HoverCard](https://vue-primitives.netlify.app/?path=/story/components-hovercard--chromatic) | ✅ Completed | ✅ | | | 🔧 Needs polygon; fix close |
| [Label](https://vue-primitives.netlify.app/?path=/story/components-label--styled) | ✅ Completed | ✅ | | | |
| [Label](https://vue-primitives.netlify.app/?path=/story/components-label--styled) | ✅ Completed | ✅ | | | |
| [Menubar](https://vue-primitives.netlify.app/?path=/story/components-menubar--styled) | ✅ Completed | ✅ | | | |
| NavigationMenu | 🚧 In Progress | 🚧 | | | |
| [Popover](https://vue-primitives.netlify.app/?path=/story/components-popover--styled) | ✅ Completed | ✅ | | | |
Expand Down
8 changes: 7 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@
"build-storybook": "storybook build",
"eslint": "eslint .",
"eslint:fix": "eslint . --fix",
"test": "vitest run",
"test:watch": "vitest --watch",
"test:nuxt": "vitest -c vitest.nuxt.config.ts --coverage",
"release": "pnpm build && pnpm publish --no-git-checks --access public",
"release:beta": "pnpm release --tag beta --access public",
"release:alpha": "pnpm release --tag alpha --access public",
Expand All @@ -107,6 +110,7 @@
"aria-hidden": "^1.2.4"
},
"devDependencies": {
"@testing-library/vue": "^8.1.0",
"@tsconfig/node20": "^20.1.4",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.9.0",
Expand All @@ -126,7 +130,9 @@
"vite-plugin-externalize-deps": "^0.8.0",
"vite-plugin-pages": "^0.32.3",
"vite-tsconfig-paths": "^5.1.3",
"vitest": "^2.1.4",
"vitest": "link:@testing-library/jest-dom/vitest",
"vitest-axe": "1.0.0-pre.3",
"vitest-canvas-mock": "^0.3.3",
"vue": "^3.5.12",
"vue-router": "^4.4.5",
"vue-tsc": "^2.1.10"
Expand Down
273 changes: 273 additions & 0 deletions packages/core/src/label/__tests__/Label.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
import { render, screen } from '@testing-library/vue'
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { axe } from 'vitest-axe'
import { Label } from '../index'
import { useLabel } from '../Label'

describe('label', () => {
describe('component', () => {
it('should render correctly', () => {
const wrapper = mount(Label)
expect(wrapper.element.tagName).toBe('LABEL')
})

it('should render with custom tag', () => {
const wrapper = mount(Label, {
props: {
as: 'div',
},
})
expect(wrapper.element.tagName).toBe('DIV')
})

it('should pass through attrs', () => {
const wrapper = mount(Label, {
attrs: {
'id': 'test-label',
'data-testid': 'label',
},
})
expect(wrapper.attributes('id')).toBe('test-label')
expect(wrapper.attributes('data-testid')).toBe('label')
})

it('should emit mousedown event', async () => {
const wrapper = mount(Label)
await wrapper.trigger('mousedown')
expect(wrapper.emitted('mousedown')).toBeTruthy()
})

it('should handle slot content updates', async () => {
const wrapper = mount({
components: { Label },
data() {
return {
content: 'Initial Label',
}
},
template: `<Label>{{ content }}</Label>`,
})
expect(wrapper.text()).toBe('Initial Label')
await wrapper.setData({ content: 'Updated Label' })
expect(wrapper.text()).toBe('Updated Label')
})

it('should handle dynamic class changes', async () => {
const wrapper = mount(Label, {
attrs: {
class: 'initial-class',
},
})
expect(wrapper.classes()).toContain('initial-class')
await wrapper.setProps({ class: 'updated-class' })
expect(wrapper.classes()).toContain('updated-class')
})

it('should handle multiple mousedown events', async () => {
const wrapper = mount(Label)
await wrapper.trigger('mousedown')
await wrapper.trigger('mousedown')
expect(wrapper.emitted('mousedown')).toHaveLength(2)
})
})

describe('useLabel', () => {
it('should prevent text selection on double click', () => {
const mockProps = {
onMousedown: vi.fn(),
}
const { attrs } = useLabel(mockProps)
const div = document.createElement('div')
const event = new MouseEvent('mousedown', {
detail: 2,
bubbles: true,
cancelable: true,
})
Object.defineProperty(event, 'target', { value: div })
event.preventDefault = vi.fn()

const resolvedAttrs = attrs([])
if (resolvedAttrs.onMousedown) {
resolvedAttrs.onMousedown(event)
}

expect(event.preventDefault).toHaveBeenCalled()
expect(mockProps.onMousedown).toHaveBeenCalledWith(event)
})

it('should not prevent mousedown on form controls', () => {
const mockProps = {
onMousedown: vi.fn(),
}
const { attrs } = useLabel(mockProps)
const button = document.createElement('button')
const event = new MouseEvent('mousedown')
Object.defineProperty(event, 'target', { value: button })

const resolvedAttrs = attrs([])
if (resolvedAttrs.onMousedown) {
resolvedAttrs.onMousedown(event)
}

expect(mockProps.onMousedown).not.toHaveBeenCalled()
})

it('should merge extra attrs', () => {
const { attrs } = useLabel()
const extraAttrs = [{
class: 'test-class',
id: 'test-id',
}]

const resolvedAttrs = attrs(extraAttrs)

expect(resolvedAttrs.class).toBe('test-class')
expect(resolvedAttrs.id).toBe('test-id')
expect(resolvedAttrs.onMousedown).toBeDefined()
})
})

describe('form control interactions', () => {
it('should trigger associated input focus on click', async () => {
// Mount using @testing-library/vue
render({
components: { Label },
template: `
<div>
<Label for="test-input">Click me</Label>
<input id="test-input" data-testid="input" type="text" />
</div>
`,
})

const input = screen.getByTestId('input')
const label = screen.getByText('Click me')

// Mock focus method
const focusSpy = vi.spyOn(input, 'focus')

// Simulate the native behavior
label.click()
input.focus()

expect(focusSpy).toHaveBeenCalled()
focusSpy.mockRestore()
})

it('should work with nested form controls', async () => {
const wrapper = mount(Label, {
slots: {
default: '<input type="checkbox" />',
},
})

const checkbox = wrapper.find('input')
await wrapper.trigger('click')
expect(checkbox.element.checked).toBe(true)
})
})

describe('accessibility', () => {
it('should have no accessibility violations', async () => {
const wrapper = mount(Label, {
slots: {
default: 'Test Label',
},
})

const results = await axe(wrapper.element)
expect(results).toHaveNoViolations()
})

it('should have no accessibility violations with custom tag', async () => {
const wrapper = mount(Label, {
props: {
as: 'div',
},
slots: {
default: 'Test Label',
},
})

const results = await axe(wrapper.element)
expect(results).toHaveNoViolations()
})

it('should have no violations when associated with form control', async () => {
const wrapper = mount({
template: `
<div>
<Label for="test-input">Test Label</Label>
<input id="test-input" type="text" />
</div>
`,
components: { Label },
})

const results = await axe(wrapper.element)
expect(results).toHaveNoViolations()
})

it('should maintain accessibility when content changes dynamically', async () => {
const wrapper = mount({
components: { Label },
data() {
return {
content: 'Initial Label',
}
},
template: `
<Label for="dynamic-input">{{ content }}</Label>
`,
})

expect(wrapper.text()).toBe('Initial Label')
await wrapper.setData({ content: 'Updated Label' })
const results = await axe(wrapper.element)
expect(results).toHaveNoViolations()
})

it('should be keyboard navigable', () => {
const wrapper = mount(Label, {
attrs: {
tabindex: '0',
},
})
expect(wrapper.attributes('tabindex')).toBe('0')
})

it('should support aria-labelledby', async () => {
const wrapper = mount({
template: `
<div>
<Label id="label-id">Description</Label>
<div aria-labelledby="label-id">Labeled content</div>
</div>
`,
components: { Label },
})

const results = await axe(wrapper.element)
expect(results).toHaveNoViolations()
})
})

describe('edge cases', () => {
it('should handle empty slots gracefully', () => {
const wrapper = mount(Label)
expect(wrapper.text()).toBe('')
expect(() => wrapper.trigger('mousedown')).not.toThrow()
})

it('should handle malformed for attribute', async () => {
const wrapper = mount(Label, {
props: {
for: 'non-existent-id',
},
})
const results = await axe(wrapper.element)
expect(results).toHaveNoViolations()
})
})
})
51 changes: 39 additions & 12 deletions packages/core/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,41 @@
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { configDefaults, defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config.ts'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vitest/config'

export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: 'jsdom',
exclude: [...configDefaults.exclude, 'e2e/**'],
root: fileURLToPath(new URL('./', import.meta.url)),
},
}),
)
const r = (p: string) => resolve(__dirname, p)

export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': r('./src'),
},
dedupe: [
'vue',
'@vue/runtime-core',
],
},
test: {
environment: 'jsdom',
globals: true,
exclude: ['**/node_modules/**'],
include: ['./**/*.test.{ts,js}'],
coverage: {
provider: 'istanbul', // or 'v8'
},
root: fileURLToPath(new URL('./', import.meta.url)),
globalSetup: './vitest.global.ts',
setupFiles: './vitest.setup.ts',
server: {
deps: {
inline: ['vitest-canvas-mock'],
},
},
environmentOptions: {
jsdom: {
resources: 'usable',
},
},
},
})
3 changes: 3 additions & 0 deletions packages/core/vitest.global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function setup() {
process.env.TZ = 'US/Eastern'
}
Loading