EthicsComputerCourses
Join training +91 90532 10052

Accessibility Testing

Software Testing · Lesson 27 of 30 Advanced 7 min read Updated

Many people use software differently from the way its developers do: with a screen reader that reads the page aloud, with only a keyboard, with zoomed text, with colour blindness, or with a shaky hand on a small touch screen. Accessibility testing checks that these users can perceive, understand and operate your product. It also tends to improve the product for everyone: clear labels, good contrast and big tap targets help a shopkeeper in bright sunlight as much as a user with low vision.

In this lesson you will learn the WCAG standard in brief, a manual testing checklist, and simple automated checks you can run, including a contrast calculator.

WCAG in brief

The Web Content Accessibility Guidelines (WCAG) from the W3C are the international reference. The current version is WCAG 2.2, a W3C Recommendation first published in October 2023 (an updated edition is dated December 2024). WCAG groups its guidance under four principles, often remembered as POUR:

PrincipleMeaningExamples of checks
PerceivableUsers can see or hear the content in some wayText alternatives for images, captions, enough colour contrast
OperableUsers can work every controlFull keyboard access, no keyboard traps, enough time, large enough targets
UnderstandableContent and behaviour are clearLabels and instructions, clear error messages, predictable navigation
RobustAssistive technologies can interpret itCorrect roles, names and states for controls

Each guideline has testable success criteria at three levels: A (minimum), AA (the usual target for websites and in many laws and policies) and AAA (highest). For example, criterion 1.4.3 requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text at level AA. WCAG 2.2 added criteria such as Focus Not Obscured, Dragging Movements, Target Size (Minimum), Redundant Entry and Accessible Authentication, and removed the old 4.1.1 Parsing criterion.

In India, the Rights of Persons with Disabilities Act, 2016 makes accessibility a legal topic as well as a quality one; check your organisation's policy for the exact standard it must meet.

Manual checks every tester can do

Automated tools find only part of the problems. These manual checks need no special skills:

  • Keyboard only. Put the mouse away. Can you reach every link, button and field with Tab, operate them with Enter or Space, and always see where the focus is? Can you get out of every menu and dialog?
  • Zoom to 200% and enlarge the phone's font size. Does text overlap or disappear?
  • Screen reader. Try NVDA (free, Windows), Narrator (Windows), VoiceOver (macOS and iOS) or TalkBack (Android). Are images, buttons and form fields announced with sensible names?
  • Colour alone. Is any information shown only by colour, such as "fields in red are wrong"? Add text or icons.
  • Forms and errors. Does every field have a visible label? Are error messages specific and announced?
  • Time limits and motion. Can users get more time for an OTP? Can moving content be paused?

Automated checks you can run

Many accessibility problems are visible in the HTML itself: images without alt text, inputs without labels, buttons without a name, and headings that skip levels. This small checker uses Python's built-in HTML parser to find them:

PythonRun it yourself
from html.parser import HTMLParser

page = """
<html lang="en">
<body>
  <h1>Kirana Kart</h1>
  <img src="logo.png" alt="Kirana Kart logo">
  <img src="offer-banner.png">
  <h3>Today's offers</h3>
  <label for="email">Email</label>
  <input id="email" type="email">
  <input id="phone" type="tel" placeholder="Phone number">
  <button><img src="cart.svg"></button>
  <button>Pay ₹1,299</button>
</body>
</html>
"""

class A11yChecker(HTMLParser):
    def __init__(self):
        super().__init__()
        self.problems = []
        self.labels = set()
        self.inputs = []
        self.last_heading = 0
        self.in_button = False
        self.button_has_name = False

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        line = self.getpos()[0]
        if tag == "img" and "alt" not in a:
            if self.in_button:
                return
            self.problems.append(f"line {line}: <img> has no alt text")
        if tag == "label" and "for" in a:
            self.labels.add(a["for"])
        if tag == "input":
            self.inputs.append((line, a.get("id")))
        if tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
            level = int(tag[1])
            if self.last_heading and level > self.last_heading + 1:
                self.problems.append(f"line {line}: heading jumps from h{self.last_heading} to h{level}")
            self.last_heading = level
        if tag == "button":
            self.in_button, self.button_has_name = True, False
            self.button_line = line

    def handle_data(self, data):
        if self.in_button and data.strip():
            self.button_has_name = True

    def handle_endtag(self, tag):
        if tag == "button":
            if not self.button_has_name:
                self.problems.append(f"line {self.button_line}: <button> has no accessible name")
            self.in_button = False

checker = A11yChecker()
checker.feed(page)
for line, input_id in checker.inputs:
    if input_id not in checker.labels:
        checker.problems.append(f"line {line}: <input id={input_id}> has no <label>")

for problem in sorted(checker.problems, key=lambda p: int(p.split()[1].rstrip(":"))):
    print(problem)
Output
line 6: <img> has no alt text
line 7: heading jumps from h1 to h3
line 10: <input id=phone> has no <label>
line 11: <button> has no accessible name

Line numbers count from the start of the page text. The phone field has a placeholder, but a placeholder disappears when you type and is not a reliable label. The cart button contains only an icon, so a screen reader would announce just "button". Fixes: alt="Diwali offer: 20% off sweets", an <h2> instead of <h3>, a <label> for the phone field, and aria-label="Cart" or visible text on the icon button.

Checking colour contrast

Contrast is calculated from the relative luminance (perceived brightness) of the text and background colours: ratio = (lighter + 0.05) / (darker + 0.05). It ranges from 1:1 (same colour) to 21:1 (black on white).

JavaScriptRun it yourself
function luminance(hex) {
  const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
  const lin = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
  return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
}

function contrast(fg, bg) {
  const [hi, lo] = [luminance(fg), luminance(bg)].sort((a, b) => b - a);
  return (hi + 0.05) / (lo + 0.05);
}

const pairs = [
  ["#000000", "#ffffff", "black on white"],
  ["#767676", "#ffffff", "mid grey on white"],
  ["#999999", "#ffffff", "light grey on white"],
  ["#ffffff", "#0f766e", "white on teal button"],
  ["#f4c430", "#ffffff", "saffron text on white"],
];

for (const [fg, bg, label] of pairs) {
  const ratio = contrast(fg, bg);
  const normal = ratio >= 4.5 ? "pass" : "FAIL";
  const large = ratio >= 3 ? "pass" : "FAIL";
  console.log(`${ratio.toFixed(2).padStart(5)}:1  normal text ${normal}, large text ${large}  (${label})`);
}
Output
21.00:1  normal text pass, large text pass  (black on white)
 4.54:1  normal text pass, large text pass  (mid grey on white)
 2.85:1  normal text FAIL, large text FAIL  (light grey on white)
 5.47:1  normal text pass, large text pass  (white on teal button)
 1.64:1  normal text FAIL, large text FAIL  (saffron text on white)

The light grey placeholder-style text that designers love fails, and so does bright saffron text on white, even though both look fine to many people on a good monitor.

Tools

  • axe-core is a widely used open-source accessibility rules engine. It runs in browser extensions (axe DevTools) and inside automated tests.
  • Lighthouse in Chrome DevTools includes an accessibility audit.
  • WAVE and browser accessibility inspectors show the names and roles that assistive technology sees.

With Playwright Test and the @axe-core/playwright package, an automated scan becomes part of your suite:

JavaScript
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";

test("login page has no detectable WCAG A/AA violations", async ({ page }) => {
  await page.goto("http://localhost:8000/login.html");
  const results = await new AxeBuilder({ page })
    .withTags(["wcag2a", "wcag2aa"])
    .analyze();
  expect(results.violations).toEqual([]);
});

Automated scans catch only a portion of WCAG problems. Whether alt text is meaningful, whether the focus order makes sense and whether an error message is understandable still needs a person.

Common mistakes

  • Relying only on an automated score. A perfect score does not mean the page is usable with a keyboard or a screen reader.
  • Placeholders instead of labels. They vanish on typing and are often low contrast.
  • Removing the focus outline for looks, which leaves keyboard users lost.
  • Testing accessibility only at the end. Fixing a design is far cheaper than rebuilding finished screens.

Key points

  • Accessibility testing checks that people with disabilities can perceive, operate and understand the product.
  • WCAG 2.2 is the current standard; level AA is the usual target, with POUR as its four principles.
  • Keyboard-only, zoom, screen reader and colour checks are manual tests any tester can do.
  • Contrast must be at least 4.5:1 for normal text and 3:1 for large text at level AA.
  • axe-core, Lighthouse and WAVE automate part of the work, but human judgement is still needed.

Exercise

A teal brand colour #14b8a6 is proposed for white button text (#ffffff on #14b8a6). Use the contrast program to check it. If it fails, try darker teals (#0d9488, #0f766e) and report the lightest one that passes for normal text.

Show solution
JavaScript
for (const bg of ["#14b8a6", "#0d9488", "#0f766e"]) {
  const ratio = contrast("#ffffff", bg);
  console.log(bg, ratio.toFixed(2), ratio >= 4.5 ? "pass" : "FAIL");
}
Output
#14b8a6 2.49 FAIL
#0d9488 3.74 FAIL
#0f766e 5.47 pass

Add these lines after the two functions in the contrast program. The two lighter teals fail for normal text (#0d9488 would pass only for large text, at 3:1 or more), so #0f766e is the lightest of the three that can be used behind white button labels.

Quick Quiz

1. Which minimum contrast ratio does WCAG level AA require for normal-size text?

2. What does the P in POUR stand for?

3. Which is the best way to label a phone number field?

4. An automated scan reports zero violations. What should you conclude?

Learn Software Testing with a mentor

Free lessons get you started. Our Software Testing training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call