frontend/tests/accessibility/template-regressions.test.ts
Rama main · réplica de lectura del repositorio original.
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
function readAppFile(path: string): string {
return readFileSync(resolve(process.cwd(), 'app', path), 'utf8')
}
describe('accessibility template regressions', () => {
it('keeps the custom import modal title and description connected to the dialog', () => {
const source = readAppFile('components/ImportModal.vue')
expect(source).toContain('<DialogTitle')
expect(source).toContain('<DialogDescription')
expect(source).toContain('{{ modalTitle }}')
expect(source).toContain('{{ modalDescription }}')
})
it('provides title and description props to every custom-content modal', () => {
const files = [
'components/PasswordChangeDialog.vue',
'pages/assets/[id]/index.vue',
'components/AttributeImpactModal.vue',
'pages/categories/index.vue',
'pages/conditions/index.vue',
'pages/locations.vue',
'pages/users.vue'
]
for (const file of files) {
const source = readAppFile(file)
const modalTags = source.match(/<UModal[\s\S]*?>/g) || []
expect(modalTags.length, file).toBeGreaterThan(0)
for (const tag of modalTags) {
expect(tag, file).toMatch(/\s:?title=/)
expect(tag, file).toMatch(/\s:?description=/)
}
}
})
it('names icon and data-type choices and exposes their selected state', () => {
const iconPicker = readAppFile('components/IconPicker.vue')
expect(iconPicker).toContain(':aria-label=')
expect(iconPicker).toContain(':aria-pressed=')
for (const file of ['pages/attributes/new.vue', 'pages/attributes/[id]/edit.vue']) {
const source = readAppFile(file)
expect(source, file).toContain('role="radiogroup"')
expect(source, file).toContain('role="radio"')
expect(source, file).toContain(':aria-checked=')
}
})
it('keeps the account menu trigger named', () => {
const source = readAppFile('app.vue')
expect(source).toContain('aria-label="Open account menu"')
expect(source).toContain('aria-label="Open navigation"')
expect(source).toContain('aria-label="Close navigation"')
expect(source).toContain('aria-label="Add asset"')
})
it('requests search focus when navigating from the dashboard', () => {
expect(readAppFile('pages/index.vue')).toContain('to="/assets?focus=search"')
const assetsSource = readAppFile('pages/assets/index.vue')
expect(assetsSource).toContain('route.query.focus !== \'search\'')
expect(assetsSource).toContain('searchContainer.value?.querySelector<HTMLInputElement>(\'input\')?.focus()')
})
it('uses an asset-name link and names each asset actions menu', () => {
const source = readAppFile('pages/assets/index.vue')
expect(source).toContain(':to="`/assets/${asset.id}`"')
expect(source).toContain(':aria-label="`Actions for ${asset.name}`"')
})
it('distinguishes an asset load failure from an empty inventory and offers retry', () => {
const source = readAppFile('pages/assets/index.vue')
expect(source).toContain('v-else-if="error || blocked"')
expect(source).toContain('Could not load assets')
expect(source).toContain('@click="refresh()"')
})
it('names the mobile asset delete control', () => {
expect(readAppFile('pages/assets/[id]/index.vue')).toContain(':aria-label="`Delete ${asset.name}`"')
})
it('uses the shared searchable icon picker everywhere icons are editable', () => {
for (const file of [
'pages/locations.vue',
'pages/collections.vue',
'components/CategoryEditor.vue',
'components/AssetHistory.vue'
]) {
expect(readAppFile(file), file).toContain('<IconPicker v-model=')
}
const iconPicker = readAppFile('components/IconPicker.vue')
expect(iconPicker).toContain('max-h-72 overflow-y-auto')
expect(iconPicker).toContain('Search Lucide icons')
})
it('allows the attribute filters to shrink without overlapping search', () => {
const source = readAppFile('pages/attributes/index.vue')
expect(source).toContain('flex min-w-0 flex-1 gap-2 overflow-x-auto')
expect(source).toContain('class="w-full shrink-0 2xl:w-64"')
})
it('reserves a fixed action column so field types align across plugin and custom rows', () => {
expect(readAppFile('pages/attributes/index.vue')).toContain('sm:grid-cols-[minmax(0,1.25fr)_minmax(150px,.7fr)_8rem]')
})
it('keeps select attribute controls readable in dark mode', () => {
expect(readAppFile('components/AttributeOptionsEditor.vue')).toContain('class="font-semibold text-mist-950 dark:text-white"')
for (const file of ['pages/attributes/new.vue', 'pages/attributes/[id]/edit.vue']) {
const source = readAppFile(file)
expect(source, file).toContain('class="block font-semibold text-mist-950 dark:text-white"')
expect(source, file).toContain('class="rounded-lg border border-default bg-default p-2 text-mist-950 dark:text-white"')
}
})
})