ACCEsibility with automated testing
David Hernández
Who am I?
Developer/ CEO / Founder @ Digital Tack
Mail: david@digitaltack.com
Before we start
Let's do a POLL
What are we going to see?
What is accessibility?
Accessibility is the design of products, devices, services, vehicles, or environments so as to be usable by people with disabilities.
What is disability?
Disability is the experience of any condition that makes it more difficult for a person to do certain activities or have equitable access within a given society.
Why it matters
An estimated 1.3 billion people – about 16% of the global population – currently experience significant disability.
Who is benefited by a ramp in the street?
The 2024 report on the accessibility of the top 1,000,000 home pages
The WebAIM Million
- An average of 56.8 errors per page
- A 13.6% increase since the 2023
- 95.9% of home pages had detected WCAG 2 failures
Who is going to test the accessibility now?
European Accessibility Act
European Accessibility Act
From 2016, mandatory for all the websites of the public sector.
From the 28th of June of 2025, mandatory for all websites, apps and products from companies with more than 10 employees or more than 2 million € of annual balance.
Navigating the web
How do we navigate?
How do we test it?
Testing library
Playwright
How does it look on a test?
import { describe, expect, it } from 'vitest'
import { render, screen } from '@testing-library/vue'
import Button from '@/infrastructure/components/Button.vue'
import { ButtonColor } from '@/infrastructure/types/Button'
describe('A button', () => {
it('is rendered', () => {
const options = {
slots: {
default: 'Add to cart'
}
}
render(Button, options)
const button = screen.getByText('Add to cart')
expect(button).toBeTruthy()
})
})
This is not the only way to navigate
Aria DevTools
But how does it look on a test?
import { describe, expect, it } from 'vitest'
import { render, screen } from '@testing-library/vue'
import Button from '@/infrastructure/components/Button.vue'
import { ButtonColor } from '@/infrastructure/types/Button'
describe('A button', () => {
it('is rendered', () => {
const options = {
slots: {
default: 'Add to cart'
}
}
render(Button, options)
const button = screen.getByRole('button')
expect(button).toBeTruthy()
})
})
The importance of Semantic HTML
Semantic HTML is the use of HTML markup to reinforce the semantics, or meaning, of the information in web pages and web applications rather than merely to define its presentation or look.
HTML Roles
Many semantic elements in HTML have a role; for example, <input type="radio"> has the "radio" role.
Non-semantic elements in HTML do not have a role
The role attribute can provide semantics
Common mistakes and how to test them
Low Contrast Text
A contrast ratio of 3:1 is the minimum level recommended
The contrast ratio of 4.5:1 was chosen for level AA
The contrast ratio of 7:1 was chosen for level AAA
How we test it in the browser?
How we test it?
test.describe('contrast checkers', () => {
test('can check the contrast of an element', async ({ page }) => {
const contrastChecker = new ColorContrastChecker()
await page.goto('http://localhost:8000')
const title = page.getByText('Products')
await expect(title).toBeVisible()
const style = await title.evaluate((title) => {
return window.getComputedStyle(title)
})
const bgColor = rgbaToHex(style['backgroundColor'])
const color = rgbaToHex(style['color'])
const fontSize = parseInt(style['fontSize'])
expect(contrastChecker.isLevelAA(bgColor, color, fontSize)).toBeTruthy()
})
})
Missing Alternative Text
How we test it?
describe('An image', () => {
it('has an alternative text', () => {
const fakeImage: TImage = {
sizes: {
small: 'test.jpg',
},
alt: 'Alt text'
}
const props = {
image: fakeImage,
size: ImageSize.small
}
render(Image, { props })
const image = screen.getByRole('img')
expect(image.getAttribute('alt')).toBe(fakeImage.alt)
})
})
Missing Form Labels
<label for="input">Input Label</label>
<input id="input" />
<label>Input Label<input /></label>
<input aria-label="Input Label" />
<span id="label">Input Label</span>
<input aria-labelledby="label" />
How to test it?
describe('A text input', () => {
it('has a label', () => {
const options = {
props: {
name: 'search',
label: 'Search',
placeholder: 'Search for a product'
}
}
render(TextInput, options)
const input = screen.getByLabelText(options.props.label)
expect(input).toBeTruthy()
})
})
Empty Links
<a href="/"><i class="fa fa-home" /></a>
How we test it?
describe('The menu', () => {
it('has a link to the home page', () => {
const options = {
global: {
plugins: [router]
}
}
render(Menu, options)
const link = screen.getByRole('link', { name: 'Home' })
expect(link).toBeTruthy()
})
})
Empty Buttons
<button><i class="fa fa-magnifying-glass" /></button>
How we test it?
describe('A button', () => {
it('has is not empty', () => {
const options = {
slots: {
default: 'Add to cart'
}
}
render(Button, options)
const button = screen.getByRole('button', { name: 'Add to cart' })
expect(button).toBeTruthy()
})
})
We can do it better
describe('A button', () => {
it('has a label', () => {
const options = {
slots: {
default: 'Add to cart'
},
props: {
ariaLabel: 'Add pineapple to cart'
}
}
render(Button, options)
const button = screen.getByRole('button', { label: options.props.ariaLabel })
expect(button).toBeTruthy()
})
})
We can do it better
Missing document language
How can we test it?
import { expect, test } from "@playwright/test";
test.describe('site language', () => {
test('is configured', async ({ page }) => {
await page.goto('http://localhost:8000')
const title = page.getByText('Products')
await expect(title).toBeVisible()
const content = await page.content()
expect(content).toContain('<html lang="en">')
})
})
Summary
Just change your selectors / locators
describe('A search form', () => {
it('can search', () => {
render(SearchForm)
const input = screen.getByLabelText('Search product')
const button = screen.getByText('Search')
input.focus()
fireEvent.change(input, { target: { value: 'Pineapple' }})
fireEvent.keyPress(input, { key: 'Enter', code: 13, charCode: 13 })
const pineappleButton = await screen.getByText('Add Pineapple to cart')
expect(pineappleButton.isVisible()).toBeTruthy()
const melonButton = await screen.getByText('Add Melon to cart')
expect(melonButton.isVisible()).toBeFalsy()
})
})
questions?
Links of interest
Examples
Accessibility with automated testing
By david_hernandez
Accessibility with automated testing
This presentation by David Hernández introduces the concept of web scraping and explains how it works. It covers topics such as making HTTP requests, interpreting HTML/XML, and using XPath and CSS selectors. Examples in Python and JavaScript are provided, along with a demonstration of basic web scraping.
- 54