diff --git a/package.json b/package.json index f68f340cc0..3a927097b0 100644 --- a/package.json +++ b/package.json @@ -171,6 +171,7 @@ "nuxt": "^4.1.2", "release-it": "^19.0.5", "vitest": "^3.2.4", + "vitest-axe": "^0.1.0", "vitest-environment-nuxt": "^1.0.1", "vue-tsc": "^3.1.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 145e36070d..06b97aed97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -216,6 +216,9 @@ importers: vitest: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.7)(happy-dom@19.0.2)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0) + vitest-axe: + specifier: ^0.1.0 + version: 0.1.0(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.0.7)(happy-dom@19.0.2)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0)) vitest-environment-nuxt: specifier: ^1.0.1 version: 1.0.1(@vue/test-utils@2.4.6)(happy-dom@19.0.2)(magicast@0.3.5)(playwright-core@1.55.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.0.7)(happy-dom@19.0.2)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0)) @@ -2884,6 +2887,10 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} @@ -2922,6 +2929,10 @@ packages: peerDependencies: postcss: ^8.1.0 + axe-core@4.10.3: + resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} + engines: {node: '>=4'} + axios@1.10.0: resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} @@ -3629,6 +3640,9 @@ packages: resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} engines: {node: '>=0.3.1'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -4477,6 +4491,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + indent-string@5.0.0: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} @@ -5999,6 +6017,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -6441,6 +6463,10 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-indent@4.0.0: resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} engines: {node: '>=12'} @@ -7057,6 +7083,11 @@ packages: yaml: optional: true + vitest-axe@0.1.0: + resolution: {integrity: sha512-jvtXxeQPg8R/2ANTY8QicA5pvvdRP4F0FsVUAHANJ46YCDASie/cuhlSzu0DGcLmZvGBSBNsNuK3HqfaeknyvA==} + peerDependencies: + vitest: '>=0.16.0' + vitest-environment-nuxt@1.0.1: resolution: {integrity: sha512-eBCwtIQriXW5/M49FjqNKfnlJYlG2LWMSNFsRVKomc8CaMqmhQPBS5LZ9DlgYL9T8xIVsiA6RZn2lk7vxov3Ow==} @@ -10100,6 +10131,8 @@ snapshots: dependencies: tslib: 2.8.1 + aria-query@5.3.2: {} + array-ify@1.0.0: {} assertion-error@2.0.1: {} @@ -10138,6 +10171,8 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 + axe-core@4.10.3: {} + axios@1.10.0: dependencies: follow-redirects: 1.15.9 @@ -10831,6 +10866,8 @@ snapshots: diff@8.0.2: {} + dom-accessibility-api@0.5.16: {} + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -11880,6 +11917,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + indent-string@5.0.0: {} index-to-position@1.1.0: {} @@ -12275,8 +12314,7 @@ snapshots: p-locate: 6.0.0 optional: true - lodash-es@4.17.21: - optional: true + lodash-es@4.17.21: {} lodash.capitalize@4.2.1: {} @@ -13885,6 +13923,11 @@ snapshots: dependencies: picomatch: 2.3.1 + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -14500,6 +14543,10 @@ snapshots: strip-final-newline@4.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-indent@4.0.0: dependencies: min-indent: 1.0.1 @@ -15148,6 +15195,16 @@ snapshots: terser: 5.43.1 yaml: 2.8.0 + vitest-axe@0.1.0(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.0.7)(happy-dom@19.0.2)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0)): + dependencies: + aria-query: 5.3.2 + axe-core: 4.10.3 + chalk: 5.6.2 + dom-accessibility-api: 0.5.16 + lodash-es: 4.17.21 + redent: 3.0.0 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.7)(happy-dom@19.0.2)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0) + vitest-environment-nuxt@1.0.1(@vue/test-utils@2.4.6)(happy-dom@19.0.2)(magicast@0.3.5)(playwright-core@1.55.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.0.7)(happy-dom@19.0.2)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0)): dependencies: '@nuxt/test-utils': 3.19.2(@vue/test-utils@2.4.6)(happy-dom@19.0.2)(magicast@0.3.5)(playwright-core@1.55.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.0.7)(happy-dom@19.0.2)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0)) diff --git a/test/components/Accordion.spec.ts b/test/components/Accordion.spec.ts index d760308477..47565f74a2 100644 --- a/test/components/Accordion.spec.ts +++ b/test/components/Accordion.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import Accordion from '../../src/runtime/components/Accordion.vue' import type { AccordionProps, AccordionSlots } from '../../src/runtime/components/Accordion.vue' import ComponentRender from '../component-render' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('Accordion', () => { const items = [{ @@ -62,4 +64,12 @@ describe('Accordion', () => { const html = await ComponentRender(nameOrHtml, options, Accordion) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(Accordion, { + props: { items, modelValue: '1' } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/Alert.spec.ts b/test/components/Alert.spec.ts index 9f851f8e1b..4574c0a946 100644 --- a/test/components/Alert.spec.ts +++ b/test/components/Alert.spec.ts @@ -3,6 +3,8 @@ import Alert from '../../src/runtime/components/Alert.vue' import type { AlertProps, AlertSlots } from '../../src/runtime/components/Alert.vue' import ComponentRender from '../component-render' import theme from '#build/ui/alert' +import { mountSuspended } from '@nuxt/test-utils/runtime' +import { axe } from 'vitest-axe' describe('Alert', () => { const variants = Object.keys(theme.variants.variant) as any @@ -34,4 +36,22 @@ describe('Alert', () => { const html = await ComponentRender(nameOrHtml, options, Alert) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(Alert, { + props: { + title: 'Alert', + icon: 'i-lucide-lightbulb', + description: 'This is a description', + actions: [{ label: 'Action' }], + close: true, + avatar: { + src: 'https://github.com/benjamincanac.png', + alt: 'Benjamin Canac' + } + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/AuthForm.spec.ts b/test/components/AuthForm.spec.ts index b77a7697ff..7a2b39f97e 100644 --- a/test/components/AuthForm.spec.ts +++ b/test/components/AuthForm.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import AuthForm from '../../src/runtime/components/AuthForm.vue' import type { AuthFormProps, AuthFormSlots } from '../../src/runtime/components/AuthForm.vue' import ComponentRender from '../component-render' +import { mountSuspended } from '@nuxt/test-utils/runtime' +import { axe } from 'vitest-axe' describe('AuthForm', () => { const fields = [{ @@ -44,4 +46,26 @@ describe('AuthForm', () => { const html = await ComponentRender(nameOrHtml, options, AuthForm) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(AuthForm, { + props: { + fields, + title: 'Title', + description: 'Description', + icon: 'i-lucide-user', + providers: [{ label: 'Google', icon: 'i-simple-icons-google' }], + separator: 'or', + submit: { label: 'Submit' } + + } + }) + + expect(await axe(wrapper.element, { + rules: { + // The passwordVisibility button has an invalid aria-controls value - it need to point to the id of the password input not its name. + 'aria-valid-attr-value': { enabled: false } + } + })).toHaveNoViolations() + }) }) diff --git a/test/components/Avatar.spec.ts b/test/components/Avatar.spec.ts index 26dd9c8cb1..ab3a369f6c 100644 --- a/test/components/Avatar.spec.ts +++ b/test/components/Avatar.spec.ts @@ -3,6 +3,8 @@ import Avatar from '../../src/runtime/components/Avatar.vue' import type { AvatarProps, AvatarSlots } from '../../src/runtime/components/Avatar.vue' import ComponentRender from '../component-render' import theme from '#build/ui/avatar' +import { mountSuspended } from '@nuxt/test-utils/runtime' +import { axe } from 'vitest-axe' describe('Avatar', () => { const sizes = Object.keys(theme.variants.size) as any @@ -26,4 +28,15 @@ describe('Avatar', () => { const html = await ComponentRender(nameOrHtml, options, Avatar) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(Avatar, { + props: { + alt: 'Benjamin Canac', + src: 'https://github.com/benjamincanac.png' + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/AvatarGroup.spec.ts b/test/components/AvatarGroup.spec.ts index b849346a51..3336169b6f 100644 --- a/test/components/AvatarGroup.spec.ts +++ b/test/components/AvatarGroup.spec.ts @@ -5,6 +5,8 @@ import AvatarGroup from '../../src/runtime/components/AvatarGroup.vue' import type { AvatarGroupProps, AvatarGroupSlots } from '../../src/runtime/components/AvatarGroup.vue' import ComponentRender from '../component-render' import theme from '#build/ui/avatar-group' +import { mountSuspended } from '@nuxt/test-utils/runtime' +import { axe } from 'vitest-axe' const AvatarGroupWrapper = defineComponent({ components: { @@ -34,4 +36,14 @@ describe('AvatarGroup', () => { const html = await ComponentRender(nameOrHtml, options, AvatarGroupWrapper) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(AvatarGroupWrapper, { + props: { + max: 2 + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/Badge.spec.ts b/test/components/Badge.spec.ts index 2e6e36e627..b4f8941696 100644 --- a/test/components/Badge.spec.ts +++ b/test/components/Badge.spec.ts @@ -3,6 +3,8 @@ import Badge from '../../src/runtime/components/Badge.vue' import type { BadgeProps, BadgeSlots } from '../../src/runtime/components/Badge.vue' import ComponentRender from '../component-render' import theme from '#build/ui/badge' +import { mountSuspended } from '@nuxt/test-utils/runtime' +import { axe } from 'vitest-axe' describe('Badge', () => { const sizes = Object.keys(theme.variants.size) as any @@ -34,4 +36,18 @@ describe('Badge', () => { const html = await ComponentRender(nameOrHtml, options, Badge) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(Badge, { + props: { + label: 'Badge', + icon: 'i-lucide-rocket', + leadingIcon: 'i-lucide-arrow-left', + trailingIcon: 'i-lucide-arrow-right', + avatar: { src: 'https://github.com/benjamincanac.png' } + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/Banner.spec.ts b/test/components/Banner.spec.ts index e36a283f99..1224e52dbe 100644 --- a/test/components/Banner.spec.ts +++ b/test/components/Banner.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import Banner from '../../src/runtime/components/Banner.vue' import type { BannerProps, BannerSlots } from '../../src/runtime/components/Banner.vue' import ComponentRender from '../component-render' +import { mountSuspended } from '@nuxt/test-utils/runtime' +import { axe } from 'vitest-axe' describe('Banner', () => { const props = { id: 'banner' } @@ -29,4 +31,18 @@ describe('Banner', () => { const html = await ComponentRender(nameOrHtml, options, Banner) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(Banner, { + props: { + id: 'banner', + title: 'Title', + icon: 'i-lucide-rocket', + actions: [{ label: 'Learn more', trailingIcon: 'i-lucide-arrow-right' }], + close: true + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/BlogPost.spec.ts b/test/components/BlogPost.spec.ts index 7c3646f505..bcaaacb397 100644 --- a/test/components/BlogPost.spec.ts +++ b/test/components/BlogPost.spec.ts @@ -3,6 +3,8 @@ import BlogPost from '../../src/runtime/components/BlogPost.vue' import type { BlogPostProps, BlogPostSlots } from '../../src/runtime/components/BlogPost.vue' import ComponentRender from '../component-render' import theme from '#build/ui/blog-post' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('BlogPost', () => { const variants = Object.keys(theme.variants.variant) as any @@ -60,4 +62,15 @@ describe('BlogPost', () => { const html = await ComponentRender(nameOrHtml, options, BlogPost) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(BlogPost, { + props: { + ...props, + authors: [author1, author2] + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/BlogPosts.spec.ts b/test/components/BlogPosts.spec.ts index 2fea60c365..59255dc7e2 100644 --- a/test/components/BlogPosts.spec.ts +++ b/test/components/BlogPosts.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import BlogPosts from '../../src/runtime/components/BlogPosts.vue' import type { BlogPostsProps, BlogPostsSlots } from '../../src/runtime/components/BlogPosts.vue' import ComponentRender from '../component-render' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('BlogPosts', () => { const posts = [{ @@ -35,4 +37,12 @@ describe('BlogPosts', () => { const html = await ComponentRender(nameOrHtml, options, BlogPosts) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(BlogPosts, { + props + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/Breadcrumb.spec.ts b/test/components/Breadcrumb.spec.ts index bc2f8522a8..0d4d625ff8 100644 --- a/test/components/Breadcrumb.spec.ts +++ b/test/components/Breadcrumb.spec.ts @@ -2,12 +2,15 @@ import { describe, it, expect } from 'vitest' import Breadcrumb from '../../src/runtime/components/Breadcrumb.vue' import type { BreadcrumbProps, BreadcrumbSlots } from '../../src/runtime/components/Breadcrumb.vue' import ComponentRender from '../component-render' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('Breadcrumb', () => { const items = [{ label: 'Home', avatar: { - src: 'https://github.com/benjamincanac.png' + src: 'https://github.com/benjamincanac.png', + alt: 'Benjamin Canac' }, to: '/' }, { @@ -42,4 +45,12 @@ describe('Breadcrumb', () => { const html = await ComponentRender(nameOrHtml, options, Breadcrumb) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(Breadcrumb, { + props + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/Button.spec.ts b/test/components/Button.spec.ts index 9501754380..f912992c8d 100644 --- a/test/components/Button.spec.ts +++ b/test/components/Button.spec.ts @@ -6,6 +6,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/button' import { mountSuspended } from '@nuxt/test-utils/runtime' import { flushPromises } from '@vue/test-utils' +import { axe } from 'vitest-axe' import { UForm @@ -111,4 +112,20 @@ describe('Button', () => { resolve?.(null) }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(Button, { + props: { + label: 'Button', + avatar: { + src: 'https://github.com/benjamincanac.png', + alt: 'Benjamin Canac' + }, + trailingIcon: 'i-lucide-arrow-right' + + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/Calendar.spec.ts b/test/components/Calendar.spec.ts index 699336871c..9f928758b9 100644 --- a/test/components/Calendar.spec.ts +++ b/test/components/Calendar.spec.ts @@ -5,6 +5,7 @@ import type { CalendarProps, CalendarSlots } from '../../src/runtime/components/ import ComponentRender from '../component-render' import theme from '#build/ui/calendar' import { CalendarDate } from '@internationalized/date' +import { axe } from 'vitest-axe' describe('Calendar', () => { const sizes = Object.keys(theme.variants.size) as any @@ -67,4 +68,17 @@ describe('Calendar', () => { expect(wrapper.emitted()).toMatchObject({ 'update:modelValue': [[date]] }) }) }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(Calendar, { + props: { + modelValue: new CalendarDate(2025, 1, 1), + range: true, + multiple: true, + numberOfMonths: 2 + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/Card.spec.ts b/test/components/Card.spec.ts index dfafab3d1e..7418e23938 100644 --- a/test/components/Card.spec.ts +++ b/test/components/Card.spec.ts @@ -3,6 +3,8 @@ import Card from '../../src/runtime/components/Card.vue' import type { CardProps, CardSlots } from '../../src/runtime/components/Card.vue' import ComponentRender from '../component-render' import theme from '#build/ui/card' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('Card', () => { const variants = Object.keys(theme.variants.variant) as any @@ -21,4 +23,10 @@ describe('Card', () => { const html = await ComponentRender(nameOrHtml, options, Card) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(Card) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/Carousel.spec.ts b/test/components/Carousel.spec.ts index 659cef6152..7cc7b3977e 100644 --- a/test/components/Carousel.spec.ts +++ b/test/components/Carousel.spec.ts @@ -3,24 +3,26 @@ import { describe, it, expect } from 'vitest' import Carousel from '../../src/runtime/components/Carousel.vue' import type { CarouselProps, CarouselSlots } from '../../src/runtime/components/Carousel.vue' import ComponentRender from '../component-render' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' const CarouselWrapper = defineComponent({ components: { UCarousel: Carousel as any }, template: ` - + ` }) describe('Carousel', () => { const items = [ - { src: 'https://picsum.photos/600/600?random=1' }, - { src: 'https://picsum.photos/600/600?random=2' }, - { src: 'https://picsum.photos/600/600?random=3' }, - { src: 'https://picsum.photos/600/600?random=4' }, - { src: 'https://picsum.photos/600/600?random=5' }, - { src: 'https://picsum.photos/600/600?random=6' } + { src: 'https://picsum.photos/600/600?random=1', alt: 'Image 1' }, + { src: 'https://picsum.photos/600/600?random=2', alt: 'Image 2' }, + { src: 'https://picsum.photos/600/600?random=3', alt: 'Image 3' }, + { src: 'https://picsum.photos/600/600?random=4', alt: 'Image 4' }, + { src: 'https://picsum.photos/600/600?random=5', alt: 'Image 5' }, + { src: 'https://picsum.photos/600/600?random=6', alt: 'Image 6' } ] const props = { items } @@ -42,4 +44,16 @@ describe('Carousel', () => { const html = await ComponentRender(nameOrHtml, options, CarouselWrapper) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(CarouselWrapper, { + props: { + items, + arrows: true, + dots: true + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/ChangelogVersion.spec.ts b/test/components/ChangelogVersion.spec.ts index bd2abd9d1b..22880b87bc 100644 --- a/test/components/ChangelogVersion.spec.ts +++ b/test/components/ChangelogVersion.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import ChangelogVersion from '../../src/runtime/components/ChangelogVersion.vue' import type { ChangelogVersionProps, ChangelogVersionSlots } from '../../src/runtime/components/ChangelogVersion.vue' import ComponentRender from '../component-render' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('ChangelogVersion', () => { it.each([ @@ -37,4 +39,21 @@ describe('ChangelogVersion', () => { const html = await ComponentRender(nameOrHtml, options, ChangelogVersion) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(ChangelogVersion, { + props: { + title: 'Version 1.0.0', + description: 'Initial release', + date: '2025-01-01', + badge: 'Badge', + authors: [{ name: 'Benjamin Canac', description: 'benjamincanac', avatar: { src: 'https://github.com/benjamincanac.png', alt: 'Benjamin Canac' } }], + indicator: true, + to: '/changelog', + image: 'https://picsum.photos/640/360' + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/ChangelogVersions.spec.ts b/test/components/ChangelogVersions.spec.ts index 7e00198192..f9907eb6d4 100644 --- a/test/components/ChangelogVersions.spec.ts +++ b/test/components/ChangelogVersions.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import ChangelogVersions from '../../src/runtime/components/ChangelogVersions.vue' import type { ChangelogVersionsProps, ChangelogVersionsSlots } from '../../src/runtime/components/ChangelogVersions.vue' import ComponentRender from '../component-render' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('ChangelogVersions', () => { const versions = [{ @@ -38,4 +40,12 @@ describe('ChangelogVersions', () => { const html = await ComponentRender(nameOrHtml, options, ChangelogVersions) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(ChangelogVersions, { + props + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/ChatMessage.spec.ts b/test/components/ChatMessage.spec.ts index eda09e22bd..b9dadeb12f 100644 --- a/test/components/ChatMessage.spec.ts +++ b/test/components/ChatMessage.spec.ts @@ -3,13 +3,15 @@ import ChatMessage from '../../src/runtime/components/ChatMessage.vue' import type { ChatMessageProps, ChatMessageSlots } from '../../src/runtime/components/ChatMessage.vue' import ComponentRender from '../component-render' import theme from '#build/ui/chat-message' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('ChatMessage', () => { const variants = Object.keys(theme.variants.variant) as any const props = { id: '6045235a-a435-46b8-989d-2df38ca2eb47', role: 'user' as const, - parts: [{ type: 'text', text: 'Hello, how are you?' }] + parts: [{ type: 'text' as const, text: 'Hello, how are you?' }] } it.each([ @@ -31,4 +33,12 @@ describe('ChatMessage', () => { const html = await ComponentRender(nameOrHtml, options, ChatMessage) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(ChatMessage, { + props + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/ChatMessages.spec.ts b/test/components/ChatMessages.spec.ts index 4243ca11f6..f7d6f693e2 100644 --- a/test/components/ChatMessages.spec.ts +++ b/test/components/ChatMessages.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import ChatMessages from '../../src/runtime/components/ChatMessages.vue' import type { ChatMessagesProps, ChatMessagesSlots } from '../../src/runtime/components/ChatMessages.vue' import ComponentRender from '../component-render' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('ChatMessages', () => { const statuses = ['ready', 'submitted', 'streaming', 'error'] as any @@ -10,11 +12,11 @@ describe('ChatMessages', () => { messages: [{ id: '6045235a-a435-46b8-989d-2df38ca2eb47', role: 'user' as const, - parts: [{ type: 'text', text: 'Hello, how are you?' }] + parts: [{ type: 'text' as const, text: 'Hello, how are you?' }] }, { id: '6045235a-a435-46b8-989d-2df38ca2eb47', role: 'assistant' as const, - parts: [{ type: 'text', text: 'I am fine, thank you!' }] + parts: [{ type: 'text' as const, text: 'I am fine, thank you!' }] }] } @@ -36,4 +38,12 @@ describe('ChatMessages', () => { const html = await ComponentRender(nameOrHtml, options, ChatMessages) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(ChatMessages, { + props + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/ChatPalette.spec.ts b/test/components/ChatPalette.spec.ts index 3119b636b0..4f448ed27c 100644 --- a/test/components/ChatPalette.spec.ts +++ b/test/components/ChatPalette.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import ChatPalette from '../../src/runtime/components/ChatPalette.vue' import type { ChatPaletteProps, ChatPaletteSlots } from '../../src/runtime/components/ChatPalette.vue' import ComponentRender from '../component-render' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('ChatPalette', () => { it.each([ @@ -16,4 +18,10 @@ describe('ChatPalette', () => { const html = await ComponentRender(nameOrHtml, options, ChatPalette) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(ChatPalette) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/ChatPrompt.spec.ts b/test/components/ChatPrompt.spec.ts index 3e97009948..cd7fa08efc 100644 --- a/test/components/ChatPrompt.spec.ts +++ b/test/components/ChatPrompt.spec.ts @@ -3,6 +3,8 @@ import ChatPrompt from '../../src/runtime/components/ChatPrompt.vue' import type { ChatPromptProps, ChatPromptSlots } from '../../src/runtime/components/ChatPrompt.vue' import ComponentRender from '../component-render' import theme from '#build/ui/chat-prompt' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('ChatPrompt', () => { const variants = Object.keys(theme.variants.variant) as any @@ -22,4 +24,14 @@ describe('ChatPrompt', () => { const html = await ComponentRender(nameOrHtml, options, ChatPrompt) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(ChatPrompt, { + props: { + placeholder: 'Placeholder' + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/ChatPromptSubmit.spec.ts b/test/components/ChatPromptSubmit.spec.ts index c8085a6c1f..a17afdef66 100644 --- a/test/components/ChatPromptSubmit.spec.ts +++ b/test/components/ChatPromptSubmit.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import ChatPromptSubmit from '../../src/runtime/components/ChatPromptSubmit.vue' import type { ChatPromptSubmitProps } from '../../src/runtime/components/ChatPromptSubmit.vue' import ComponentRender from '../component-render' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('ChatPromptSubmit', () => { const statuses = ['ready', 'submitted', 'streaming', 'error'] as any @@ -15,4 +17,14 @@ describe('ChatPromptSubmit', () => { const html = await ComponentRender(nameOrHtml, options, ChatPromptSubmit) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(ChatPromptSubmit, { + props: { + status: 'ready' + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/Checkbox.spec.ts b/test/components/Checkbox.spec.ts index 1ca750448f..988e80c605 100644 --- a/test/components/Checkbox.spec.ts +++ b/test/components/Checkbox.spec.ts @@ -6,6 +6,8 @@ import theme from '#build/ui/checkbox' import { renderForm } from '../utils/form' import { mount, flushPromises } from '@vue/test-utils' import type { FormInputEvents } from '../../src/module' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('Checkbox', () => { const sizes = Object.keys(theme.variants.size) as any @@ -42,6 +44,16 @@ describe('Checkbox', () => { expect(html).toMatchSnapshot() }) + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(Checkbox, { + props: { + label: 'Test checkbox' + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) + describe('emits', () => { test('update:modelValue event', async () => { const wrapper = mount(Checkbox) diff --git a/test/components/CheckboxGroup.spec.ts b/test/components/CheckboxGroup.spec.ts index 61987e7dfe..f47b15c50b 100644 --- a/test/components/CheckboxGroup.spec.ts +++ b/test/components/CheckboxGroup.spec.ts @@ -7,6 +7,8 @@ import themeCheckbox from '#build/ui/checkbox' import { flushPromises, mount } from '@vue/test-utils' import { renderForm } from '../utils/form' import type { FormInputEvents } from '../../src/module' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('CheckboxGroup', () => { const sizes = Object.keys(theme.variants.size) as any @@ -49,6 +51,21 @@ describe('CheckboxGroup', () => { expect(html).toMatchSnapshot() }) + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(CheckboxGroup, { + props: { + items: [ + { value: '1', label: 'Option 1' }, + { value: '2', label: 'Option 2' } + ], + legend: 'Legend' + + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) + describe('emits', () => { test('update:modelValue event', async () => { const wrapper = mount(CheckboxGroup, { props: { items: ['Option 1', 'Option 2'] } }) diff --git a/test/components/Chip.spec.ts b/test/components/Chip.spec.ts index 60bbf0fe0d..c81972384d 100644 --- a/test/components/Chip.spec.ts +++ b/test/components/Chip.spec.ts @@ -3,6 +3,8 @@ import Chip from '../../src/runtime/components/Chip.vue' import type { ChipProps, ChipSlots } from '../../src/runtime/components/Chip.vue' import ComponentRender from '../component-render' import theme from '#build/ui/chip' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('Chip', () => { const sizes = Object.keys(theme.variants.size) as any @@ -26,4 +28,14 @@ describe('Chip', () => { const html = await ComponentRender(nameOrHtml, options, Chip) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(Chip, { + props: { + text: 'Text' + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/Collapsible.spec.ts b/test/components/Collapsible.spec.ts index 0083699ad1..6a617f770d 100644 --- a/test/components/Collapsible.spec.ts +++ b/test/components/Collapsible.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import Collapsible from '../../src/runtime/components/Collapsible.vue' import type { CollapsibleProps, CollapsibleSlots } from '../../src/runtime/components/Collapsible.vue' import ComponentRender from '../component-render' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('Collapsible', () => { const props = { open: true } @@ -21,4 +23,14 @@ describe('Collapsible', () => { const html = await ComponentRender(nameOrHtml, options, Collapsible) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(Collapsible, { + props: { + open: true + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/ColorPicker.spec.ts b/test/components/ColorPicker.spec.ts index eddf7276b5..97e083eb4a 100644 --- a/test/components/ColorPicker.spec.ts +++ b/test/components/ColorPicker.spec.ts @@ -4,6 +4,7 @@ import ColorPicker from '../../src/runtime/components/ColorPicker.vue' import type { ColorPickerProps } from '../../src/runtime/components/ColorPicker.vue' import ComponentRender from '../component-render' import theme from '#build/ui/color-picker' +import { axe } from 'vitest-axe' describe('ColorPicker', () => { const sizes = Object.keys(theme.variants.size) as any @@ -28,6 +29,12 @@ describe('ColorPicker', () => { expect(html).toMatchSnapshot() }) + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(ColorPicker) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) + describe('emits', () => { test('update:modelValue event', async () => { const wrapper = await mountSuspended(ColorPicker) diff --git a/test/components/CommandPalette.spec.ts b/test/components/CommandPalette.spec.ts index 97d41123af..a0dfc305c0 100644 --- a/test/components/CommandPalette.spec.ts +++ b/test/components/CommandPalette.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import CommandPalette from '../../src/runtime/components/CommandPalette.vue' import type { CommandPaletteProps, CommandPaletteSlots } from '../../src/runtime/components/CommandPalette.vue' import ComponentRender from '../component-render' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('CommandPalette', () => { const groups = [{ @@ -36,17 +38,17 @@ describe('CommandPalette', () => { items: [{ label: 'bug', chip: { - color: 'error' + color: 'error' as const } }, { label: 'feature', chip: { - color: 'success' + color: 'success' as const } }, { label: 'enhancement', chip: { - color: 'info' + color: 'info' as const } }] }, { @@ -55,7 +57,8 @@ describe('CommandPalette', () => { items: [{ label: 'benjamincanac', avatar: { - src: 'https://github.com/benjamincanac.png' + src: 'https://github.com/benjamincanac.png', + alt: 'Benjamin Canac' }, to: 'https://github.com/benjamincanac', target: '_blank' @@ -95,4 +98,26 @@ describe('CommandPalette', () => { const html = await ComponentRender(nameOrHtml, options, CommandPalette) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(CommandPalette, { + props: { + groups, + close: true, + modelValue: [groups[2]?.items[0], groups[1]?.items[0]], + multiple: true + } + }) + + expect(await axe(wrapper.element, { + rules: { + // ARIA input fields must have an accessible name (aria-input-field-name)" + // Fix any of the following: + // aria-label attribute does not exist or is empty + // aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty + // Element has no title attribute + 'aria-input-field-name': { enabled: false } + } + })).toHaveNoViolations() + }) }) diff --git a/test/components/Container.spec.ts b/test/components/Container.spec.ts index 7eb0bfb36c..cf047e73e1 100644 --- a/test/components/Container.spec.ts +++ b/test/components/Container.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import Container from '../../src/runtime/components/Container.vue' import type { ContainerProps, ContainerSlots } from '../../src/runtime/components/Container.vue' import ComponentRender from '../component-render' +import { mountSuspended } from '@nuxt/test-utils/runtime' +import { axe } from 'vitest-axe' describe('Container', () => { it.each([ @@ -14,4 +16,10 @@ describe('Container', () => { const html = await ComponentRender(nameOrHtml, options, Container) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(Container) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/ContextMenu.spec.ts b/test/components/ContextMenu.spec.ts index b19aa6f2ba..e5a3a8dd36 100644 --- a/test/components/ContextMenu.spec.ts +++ b/test/components/ContextMenu.spec.ts @@ -5,6 +5,7 @@ import type { ContextMenuProps, ContextMenuSlots } from '../../src/runtime/compo import theme from '#build/ui/context-menu' import { mountSuspended } from '@nuxt/test-utils/runtime' import { expectSlotProps } from '../utils/types' +import { axe } from 'vitest-axe' const ContextMenuWrapper = defineComponent({ components: { @@ -105,6 +106,16 @@ describe('ContextMenu', () => { expect(wrapper.html()).toMatchSnapshot() }) + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(ContextMenuWrapper, { + props + }) + + await wrapper.find('span').trigger('click.right') + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) + test('should have the correct types', () => { // normal expectSlotProps('item', () => ContextMenu({ diff --git a/test/components/DashboardGroup.spec.ts b/test/components/DashboardGroup.spec.ts index a3ad02c3d2..fe20a55916 100644 --- a/test/components/DashboardGroup.spec.ts +++ b/test/components/DashboardGroup.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import DashboardGroup from '../../src/runtime/components/DashboardGroup.vue' import type { DashboardGroupProps, DashboardGroupSlots } from '../../src/runtime/components/DashboardGroup.vue' import ComponentRender from '../component-render' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('DashboardGroup', () => { it.each([ @@ -14,4 +16,10 @@ describe('DashboardGroup', () => { const html = await ComponentRender(nameOrHtml, options, DashboardGroup) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(DashboardGroup) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/DashboardNavbar.spec.ts b/test/components/DashboardNavbar.spec.ts index 10fe34f6c3..79a0c44ce6 100644 --- a/test/components/DashboardNavbar.spec.ts +++ b/test/components/DashboardNavbar.spec.ts @@ -4,6 +4,7 @@ import DashboardGroup from '../../src/runtime/components/DashboardGroup.vue' import DashboardNavbar from '../../src/runtime/components/DashboardNavbar.vue' import type { DashboardNavbarProps, DashboardNavbarSlots } from '../../src/runtime/components/DashboardNavbar.vue' import { mountSuspended } from '@nuxt/test-utils/runtime' +import { axe } from 'vitest-axe' const DashboardWrapper = defineComponent({ components: { @@ -43,4 +44,14 @@ describe('DashboardNavbar', () => { const wrapper = await mountSuspended(DashboardWrapper, options) expect(wrapper.html()).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(DashboardWrapper, { + props: { + title: 'Dashboard' + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/DashboardPanel.spec.ts b/test/components/DashboardPanel.spec.ts index dbadc3837d..df4e5c4746 100644 --- a/test/components/DashboardPanel.spec.ts +++ b/test/components/DashboardPanel.spec.ts @@ -4,6 +4,7 @@ import DashboardGroup from '../../src/runtime/components/DashboardGroup.vue' import DashboardPanel from '../../src/runtime/components/DashboardPanel.vue' import type { DashboardPanelProps, DashboardPanelSlots } from '../../src/runtime/components/DashboardPanel.vue' import { mountSuspended } from '@nuxt/test-utils/runtime' +import { axe } from 'vitest-axe' const DashboardWrapper = defineComponent({ components: { @@ -40,4 +41,15 @@ describe('DashboardPanel', () => { const wrapper = await mountSuspended(DashboardWrapper, options) expect(wrapper.html()).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(DashboardWrapper, { + props: { + id: 'test', + resizable: true + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/DashboardResizeHandle.spec.ts b/test/components/DashboardResizeHandle.spec.ts index 1afec32366..5cf13e9e80 100644 --- a/test/components/DashboardResizeHandle.spec.ts +++ b/test/components/DashboardResizeHandle.spec.ts @@ -4,6 +4,7 @@ import DashboardGroup from '../../src/runtime/components/DashboardGroup.vue' import DashboardResizeHandle from '../../src/runtime/components/DashboardResizeHandle.vue' import type { DashboardResizeHandleProps, DashboardResizeHandleSlots } from '../../src/runtime/components/DashboardResizeHandle.vue' import { mountSuspended } from '@nuxt/test-utils/runtime' +import { axe } from 'vitest-axe' const DashboardWrapper = defineComponent({ components: { @@ -31,4 +32,10 @@ describe('DashboardResizeHandle', () => { const wrapper = await mountSuspended(DashboardWrapper, options) expect(wrapper.html()).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(DashboardWrapper) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/DashboardSearch.spec.ts b/test/components/DashboardSearch.spec.ts index 9e8e595193..3a390a549a 100644 --- a/test/components/DashboardSearch.spec.ts +++ b/test/components/DashboardSearch.spec.ts @@ -4,6 +4,7 @@ import DashboardGroup from '../../src/runtime/components/DashboardGroup.vue' import DashboardSearch from '../../src/runtime/components/DashboardSearch.vue' import type { DashboardSearchProps } from '../../src/runtime/components/DashboardSearch.vue' import { mountSuspended } from '@nuxt/test-utils/runtime' +import { axe } from 'vitest-axe' const DashboardWrapper = defineComponent({ components: { @@ -47,4 +48,22 @@ describe('DashboardSearch', () => { const wrapper = await mountSuspended(DashboardWrapper, options) expect(wrapper.html()).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(DashboardWrapper, { + props + }) + + expect(await axe(wrapper.element, { + // "ARIA input fields must have an accessible name (aria-input-field-name)" + // + // Fix any of the following: + // aria-label attribute does not exist or is empty + // aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty + // Element has no title attribute + rules: { + 'aria-input-field-name': { enabled: false } + } + })).toHaveNoViolations() + }) }) diff --git a/test/components/DashboardSearchButton.spec.ts b/test/components/DashboardSearchButton.spec.ts index b3e7dc93e6..ba4fb5dae0 100644 --- a/test/components/DashboardSearchButton.spec.ts +++ b/test/components/DashboardSearchButton.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import DashboardSearchButton from '../../src/runtime/components/DashboardSearchButton.vue' import type { DashboardSearchButtonProps } from '../../src/runtime/components/DashboardSearchButton.vue' import ComponentRender from '../component-render' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('DashboardSearchButton', () => { it.each([ @@ -15,4 +17,16 @@ describe('DashboardSearchButton', () => { const html = await ComponentRender(nameOrHtml, options, DashboardSearchButton) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(DashboardSearchButton, { + props: { + label: 'Open', + icon: 'i-lucide-house', + kbds: ['alt', 'o'] + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/DashboardSidebar.spec.ts b/test/components/DashboardSidebar.spec.ts index 31a4450ef4..0d46f87218 100644 --- a/test/components/DashboardSidebar.spec.ts +++ b/test/components/DashboardSidebar.spec.ts @@ -4,6 +4,7 @@ import DashboardGroup from '../../src/runtime/components/DashboardGroup.vue' import DashboardSidebar from '../../src/runtime/components/DashboardSidebar.vue' import type { DashboardSidebarProps, DashboardSidebarSlots } from '../../src/runtime/components/DashboardSidebar.vue' import { mountSuspended } from '@nuxt/test-utils/runtime' +import { axe } from 'vitest-axe' const DashboardWrapper = defineComponent({ components: { @@ -50,4 +51,16 @@ describe('DashboardSidebar', () => { const wrapper = await mountSuspended(DashboardWrapper, options) expect(wrapper.html()).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(DashboardWrapper, { + props: { + id: 'test', + resizable: true, + collapsible: true + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/DashboardSidebarCollapse.spec.ts b/test/components/DashboardSidebarCollapse.spec.ts index ea9169af5d..5c0ae3dc23 100644 --- a/test/components/DashboardSidebarCollapse.spec.ts +++ b/test/components/DashboardSidebarCollapse.spec.ts @@ -4,6 +4,7 @@ import DashboardGroup from '../../src/runtime/components/DashboardGroup.vue' import DashboardSidebarCollapse from '../../src/runtime/components/DashboardSidebarCollapse.vue' import type { DashboardSidebarCollapseProps } from '../../src/runtime/components/DashboardSidebarCollapse.vue' import { mountSuspended } from '@nuxt/test-utils/runtime' +import { axe } from 'vitest-axe' const DashboardWrapper = defineComponent({ components: { @@ -27,4 +28,10 @@ describe('DashboardSidebarCollapse', () => { const wrapper = await mountSuspended(DashboardWrapper, options) expect(wrapper.html()).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(DashboardWrapper) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/DashboardSidebarToggle.spec.ts b/test/components/DashboardSidebarToggle.spec.ts index 8b37c657bb..a1d9814f3a 100644 --- a/test/components/DashboardSidebarToggle.spec.ts +++ b/test/components/DashboardSidebarToggle.spec.ts @@ -4,6 +4,7 @@ import DashboardGroup from '../../src/runtime/components/DashboardGroup.vue' import DashboardSidebarToggle from '../../src/runtime/components/DashboardSidebarToggle.vue' import type { DashboardSidebarToggleProps } from '../../src/runtime/components/DashboardSidebarToggle.vue' import { mountSuspended } from '@nuxt/test-utils/runtime' +import { axe } from 'vitest-axe' const DashboardWrapper = defineComponent({ components: { @@ -27,4 +28,10 @@ describe('DashboardSidebarToggle', () => { const wrapper = await mountSuspended(DashboardWrapper, options) expect(wrapper.html()).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(DashboardWrapper) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/DashboardToolbar.spec.ts b/test/components/DashboardToolbar.spec.ts index 7b9c7da716..d6587cdeb0 100644 --- a/test/components/DashboardToolbar.spec.ts +++ b/test/components/DashboardToolbar.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import DashboardToolbar from '../../src/runtime/components/DashboardToolbar.vue' import type { DashboardToolbarProps, DashboardToolbarSlots } from '../../src/runtime/components/DashboardToolbar.vue' import ComponentRender from '../component-render' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('DashboardToolbar', () => { it.each([ @@ -17,4 +19,10 @@ describe('DashboardToolbar', () => { const html = await ComponentRender(nameOrHtml, options, DashboardToolbar) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(DashboardToolbar) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/Drawer.spec.ts b/test/components/Drawer.spec.ts index 970fb5a997..003a2cf3d0 100644 --- a/test/components/Drawer.spec.ts +++ b/test/components/Drawer.spec.ts @@ -3,6 +3,8 @@ import Drawer from '../../src/runtime/components/Drawer.vue' import type { DrawerProps, DrawerSlots } from '../../src/runtime/components/Drawer.vue' import ComponentRender from '../component-render' import theme from '#build/ui/drawer' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('Drawer', () => { const directions = Object.keys(theme.variants.direction) as any @@ -31,4 +33,17 @@ describe('Drawer', () => { const html = await ComponentRender(nameOrHtml, options, Drawer) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(Drawer, { + props: { + open: true, + portal: false, + title: 'Title', + description: 'Description' + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/DropdownMenu.spec.ts b/test/components/DropdownMenu.spec.ts index 59b5807ccc..9eb4f3b16a 100644 --- a/test/components/DropdownMenu.spec.ts +++ b/test/components/DropdownMenu.spec.ts @@ -4,6 +4,8 @@ import type { DropdownMenuProps, DropdownMenuSlots } from '../../src/runtime/com import ComponentRender from '../component-render' import theme from '#build/ui/dropdown-menu' import { expectSlotProps } from '../utils/types' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('DropdownMenu', () => { const sizes = Object.keys(theme.variants.size) as any @@ -12,7 +14,8 @@ describe('DropdownMenu', () => { [{ label: 'My account', avatar: { - src: 'https://github.com/benjamincanac.png' + src: 'https://github.com/benjamincanac.png', + alt: 'Benjamín Canac' }, type: 'label' }], @@ -112,6 +115,22 @@ describe('DropdownMenu', () => { expect(html).toMatchSnapshot() }) + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(DropdownMenu, { + props + }) + + expect(await axe(wrapper.element, { + rules: { + // "Certain ARIA roles must contain particular children (aria-required-children)" + // + // Fix any of the following: + // Element has children which are not allowed: img[tabindex] + 'aria-required-children': { enabled: false } + } + })).toHaveNoViolations() + }) + test('should have the correct types', () => { // normal expectSlotProps('item', () => DropdownMenu({ diff --git a/test/components/Error.spec.ts b/test/components/Error.spec.ts index 7d232e5ec7..b239ca5ad3 100644 --- a/test/components/Error.spec.ts +++ b/test/components/Error.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import Error from '../../src/runtime/components/Error.vue' import type { ErrorProps, ErrorSlots } from '../../src/runtime/components/Error.vue' import ComponentRender from '../component-render' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('Error', () => { const error = { @@ -30,4 +32,16 @@ describe('Error', () => { const html = await ComponentRender(nameOrHtml, options, Error) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(Error, { + props: { + error, + redirect: '/blog', + clear: { label: 'Home' } + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/FieldGroup.spec.ts b/test/components/FieldGroup.spec.ts index 303eff3252..feff946563 100644 --- a/test/components/FieldGroup.spec.ts +++ b/test/components/FieldGroup.spec.ts @@ -4,6 +4,8 @@ import type { FieldGroupProps, FieldGroupSlots } from '../../src/runtime/compone import ComponentRender from '../component-render' import { UInput, UButton } from '#components' import buttonTheme from '#build/ui/button' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' describe('FieldGroup', () => { const sizes = Object.keys(buttonTheme.variants.size) as any @@ -45,4 +47,16 @@ describe('FieldGroup', () => { const html = await ComponentRender(nameOrHtml, options, FieldGroup) expect(html).toMatchSnapshot() }) + + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(FieldGroup, { + slots: { + default: { + template: ` Click me! ` + } + } + }) + + expect(await axe(wrapper.element)).toHaveNoViolations() + }) }) diff --git a/test/components/FileUpload.spec.ts b/test/components/FileUpload.spec.ts index e5c7cca86a..e86e2c9f2b 100644 --- a/test/components/FileUpload.spec.ts +++ b/test/components/FileUpload.spec.ts @@ -6,6 +6,8 @@ import ComponentRender from '../component-render' import { renderForm } from '../utils/form' import type { FormInputEvents } from '../../src/module' import theme from '#build/ui/file-upload' +import { axe } from 'vitest-axe' +import { mountSuspended } from '@nuxt/test-utils/runtime' // Mock URL.createObjectURL to return deterministic blob URLs URL.createObjectURL = vi.fn((file: File | Blob) => { @@ -89,6 +91,31 @@ describe('FileUpload', () => { expect(html).toMatchSnapshot() }) + it('passes accessibility tests', async () => { + const wrapper = await mountSuspended(FileUpload, { + props: { + label: 'Upload files', + description: 'Select files to upload', + required: true + } + }) + + expect(await axe(wrapper.element, { + rules: { + // "Form elements must have labels (label)" + // Fix any of the following: + // Element does not have an implicit (wrapped)