ACCEsibility with automated testing

A frontal picture of David

David Hernández

Who am I?

Developer/ CEO / Founder @ Digital Tack

Mail: david@digitaltack.com

 

 

 

A QR code to David's linkedin profile: https://www.linkedin.com/in/davidhernandezruiz1/

Before we start

Let's do a POLL

What are we going to see?

What is accessibility?

The accessibility logo

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?

An image showing how different types of dissabilities might be permanent, temporary or situational

 

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

Graph showing the most common web accessibility mistakes in the top one million webs, from the WebAIM One Million project.

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