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(' { 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(//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(\'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(' { 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"') } }) })