Ctrl K
ComunidadGuías…

frontend/tests/accessibility/template-regressions.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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"')
    }
  })
})