Skip to content
Draft
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
163 changes: 163 additions & 0 deletions app/components/CacheAnalysis.a11y.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* @vitest-environment jsdom
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import CacheAnalysis from './CacheAnalysis.vue'

describe('CacheAnalysis - Accessibility', () => {
const mockProps = {
cacheHeaders: {
'cache-control': 'public, max-age=3600, s-maxage=7200',
'cache-status': '"Netlify Edge"; hit',
'x-nf-request-id': 'test-id',
'age': '100',
'etag': '"abc123"',
'vary': 'Accept-Encoding',
},
enableDiffOnHover: false,
}

beforeEach(() => {
vi.clearAllTimers()
})

it('has proper ARIA roles for interactive elements', () => {
const wrapper = mount(CacheAnalysis, {
props: mockProps,
})

// Check that all dt elements with tabindex have role="button"
const interactiveDts = wrapper.findAll('dt[tabindex="0"]')
interactiveDts.forEach((dt) => {
expect(dt.attributes('role')).toBe('button')
})
})

it('has proper aria-labels for all interactive elements', () => {
const wrapper = mount(CacheAnalysis, {
props: mockProps,
})

// Check that all dt elements with role="button" have aria-label
const buttons = wrapper.findAll('dt[role="button"]')
buttons.forEach((button) => {
expect(button.attributes('aria-label')).toBeTruthy()
expect(button.attributes('aria-label')).not.toBe('')
})
})

it('hides decorative emojis from screen readers', () => {
const wrapper = mount(CacheAnalysis, {
props: mockProps,
})

// Find all spans with aria-hidden="true"
const hiddenElements = wrapper.findAll('[aria-hidden="true"]')

// Should have at least the emoji decorations (arrows, emojis in headings, etc.)
expect(hiddenElements.length).toBeGreaterThan(0)

// Check that checkmarks and X marks are hidden - they're inside dd elements
const booleanValues = wrapper.findAll('dd span[aria-hidden="true"]')
expect(booleanValues.length).toBeGreaterThan(0)
})

it('provides screen reader text for boolean values', () => {
const wrapper = mount(CacheAnalysis, {
props: mockProps,
})

// Find screen reader only text for boolean values
const srOnlyTexts = wrapper.findAll('.sr-only')

// Should have at least some screen reader text
expect(srOnlyTexts.length).toBeGreaterThan(0)
})

it('has proper keyboard navigation support', () => {
const wrapper = mount(CacheAnalysis, {
props: mockProps,
})

// Check that interactive elements can be tabbed to
const tabbableElements = wrapper.findAll('[tabindex="0"]')
expect(tabbableElements.length).toBeGreaterThan(0)

// Each should be a button or have proper role
tabbableElements.forEach((element) => {
const role = element.attributes('role')
expect(role).toBeTruthy()
})
})

it('has tooltip information accessible via aria-label', () => {
const wrapper = mount(CacheAnalysis, {
props: mockProps,
})

// Tooltip triggers should have aria-labels
const tooltipTriggers = wrapper.findAll('.tooltip-trigger')
tooltipTriggers.forEach((trigger) => {
expect(trigger.attributes('aria-label')).toBeTruthy()
expect(trigger.attributes('role')).toBe('button')
})
})

it('maintains semantic HTML structure with dl/dt/dd', () => {
const wrapper = mount(CacheAnalysis, {
props: mockProps,
})

// Should have proper description list structure
const dl = wrapper.find('dl')
expect(dl.exists()).toBe(true)

const dts = wrapper.findAll('dt')
const dds = wrapper.findAll('dd')

expect(dts.length).toBeGreaterThan(0)
expect(dds.length).toBeGreaterThan(0)
})

it('provides meaningful focus indicators', () => {
const wrapper = mount(CacheAnalysis, {
props: mockProps,
})

// Check that data-key elements have proper classes for focus states
const dataKeys = wrapper.findAll('.data-key')
dataKeys.forEach((key) => {
// Element should be focusable
expect(key.attributes('tabindex')).toBe('0')
expect(key.attributes('role')).toBe('button')
})
})

it('uses semantic headings for cache sections', () => {
const wrapper = mount(CacheAnalysis, {
props: mockProps,
})

// Should have h4 elements for sections
const headings = wrapper.findAll('h4')
expect(headings.length).toBeGreaterThan(0)

// Each heading should have meaningful content
headings.forEach((heading) => {
expect(heading.text().trim()).not.toBe('')
})
})

it('screen reader text is visually hidden but accessible', () => {
const wrapper = mount(CacheAnalysis, {
props: mockProps,
})

const srOnly = wrapper.find('.sr-only')
if (srOnly.exists()) {
// Check that sr-only class exists and would be applied
expect(srOnly.classes()).toContain('sr-only')
}
})
})
Loading