Exhibit A-4 Evidence: commit adfefb2
Committed · adfefb28045603fa8134677cefb69bacbed030e4
Zoom, focus, contrast and labels were broken
The repository itself is private; this page carries the commit as it is recorded there.
Commit message
The change
Text files only, as recorded in git. Lines starting with + were added, lines with − removed.
Files changed
---
e2e/tests/a4-accessibility.spec.ts | 54 +++++++++++++++++++++++++++++
.../plugins/hc-appointment-request/hc-appointment-request.php | 5 ++-
site/wp-content/themes/hollis-child/header.php | 48 -------------------------
site/wp-content/themes/hollis-child/style.css | 17 ---------
4 files changed, 56 insertions(+), 68 deletions(-)
diff --git a/e2e/tests/a4-accessibility.spec.ts b/e2e/tests/a4-accessibility.spec.ts
new file mode 100644
index 0000000..ebe7967
--- /dev/null
+++ b/e2e/tests/a4-accessibility.spec.ts
@@ -0,0 +1,54 @@
+import { test, expect, Page } from '@playwright/test';
+import AxeBuilder from '@axe-core/playwright';
+import { BEFORE, AFTER } from './helpers';
+
+// Exhibit A-4. axe-core with the WCAG 2.0, 2.1 and 2.2 A/AA rules, on the pages a patient uses.
+const PAGES = ['/', '/our-team/', '/request-appointment/', '/insurance/', '/services/'];
+const TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'];
+
+async function violations(page: Page) {
+ const result = await new AxeBuilder({ page }).withTags(TAGS).analyze();
+ return result.violations.map((v) => `${v.id} (${v.nodes.length})`);
+}
+
+test('A-4 before: axe finds WCAG violations on the home and appointment pages', async ({ page }) => {
+ await page.goto(BEFORE + '/');
+ const home = await violations(page);
+ await page.goto(BEFORE + '/request-appointment/');
+ const form = await violations(page);
+ expect(home.length + form.length).toBeGreaterThan(0);
+ // Zoom is disabled and the form fields have no labels.
+ expect(home.join(' ')).toContain('meta-viewport');
+ expect(form.join(' ')).toMatch(/label/);
+});
+
+for (const width of [1280, 390]) {
+ test(`A-4 after: no WCAG 2.2 AA violations on ${PAGES.length} pages at ${width}px`, async ({ page }) => {
+ await page.setViewportSize({ width, height: 900 });
+ for (const path of PAGES) {
+ await page.goto(AFTER + path);
+ expect(await violations(page), path).toEqual([]);
+ }
+ });
+}
+
+test('A-4 after: a keyboard user reaches the content through a visible skip link', async ({ page }) => {
+ await page.goto(AFTER + '/');
+ await page.keyboard.press('Tab');
+ const skip = page.getByRole('link', { name: 'Skip to content' });
+ await expect(skip).toBeFocused();
+ const box = await skip.boundingBox();
+ expect(box!.y).toBeGreaterThanOrEqual(0);
+ const outline = await skip.evaluate((el) => getComputedStyle(el).outlineStyle);
+ expect(outline).not.toBe('none');
+ await page.keyboard.press('Enter');
+ await expect(page).toHaveURL(/#content$/);
+});
+
+test('A-4 after: the appointment form fields are labelled and an error names the problem', async ({ page }) => {
+ await page.goto(AFTER + '/request-appointment/');
+ await page.getByRole('button', { name: 'Send request' }).click();
+ await expect(page.getByRole('alert')).toContainText('Please check the fields marked below.');
+ await expect(page.getByLabel('Your name')).toHaveAttribute('aria-invalid', 'true');
+ await expect(page.getByText('Please enter your name.')).toBeVisible();
+});
diff --git a/site/wp-content/plugins/hc-appointment-request/hc-appointment-request.php b/site/wp-content/plugins/hc-appointment-request/hc-appointment-request.php
index 8ad0bb1..7122374 100644
--- a/site/wp-content/plugins/hc-appointment-request/hc-appointment-request.php
+++ b/site/wp-content/plugins/hc-appointment-request/hc-appointment-request.php
@@ -194,8 +194,7 @@ function hc_appointment_field( $name, $label, $type, $extra = '', $hint = '' ) {
$value = isset( $state['values'][ $name ] ) ? $state['values'][ $name ] : '';
$error = isset( $state['errors'][ $name ] ) ? $state['errors'][ $name ] : '';
$describe = array();
- $html = '<p class="form-field' . ( $error ? ' has-error' : '' ) . '">';
- $extra .= ' placeholder="' . esc_attr( $label ) . '"';
+ $html = '<p class="form-field' . ( $error ? ' has-error' : '' ) . '"><label for="' . esc_attr( $id ) . '">' . esc_html( $label ) . '</label>';
if ( $hint ) {
$html .= '<span class="field-hint" id="' . esc_attr( $id ) . '-hint">' . esc_html( $hint ) . '</span>';
$describe[] = $id . '-hint';
@@ -224,7 +223,7 @@ function hc_appointment_field( $name, $label, $type, $extra = '', $hint = '' ) {
function hc_appointment_select( $name, $label ) {
$state = hc_appointment_state();
$current = isset( $state['values'][ $name ] ) ? $state['values'][ $name ] : '';
- $html = '<p class="form-field"><select id="hc-' . esc_attr( $name ) . '" name="hc_' . esc_attr( $name ) . '">';
+ $html = '<p class="form-field"><label for="hc-' . esc_attr( $name ) . '">' . esc_html( $label ) . '</label><select id="hc-' . esc_attr( $name ) . '" name="hc_' . esc_attr( $name ) . '">';
foreach ( hc_appointment_choices( $name ) as $key => $text ) {
$html .= '<option value="' . esc_attr( $key ) . '"' . selected( $current, $key, false ) . '>' . esc_html( $text ) . '</option>';
}
diff --git a/site/wp-content/themes/hollis-child/header.php b/site/wp-content/themes/hollis-child/header.php
deleted file mode 100644
index b924334..0000000
--- a/site/wp-content/themes/hollis-child/header.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-/**
- * Site header (child copy, 2019).
- *
- * @package HollisChild
- */
-
-defined( 'ABSPATH' ) || exit;
-?><!doctype html>
-<html <?php language_attributes(); ?>>
-<head>
-<meta charset="<?php bloginfo( 'charset' ); ?>">
-<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
-<?php wp_head(); ?>
-</head>
-<body <?php body_class(); ?>>
-<?php wp_body_open(); ?>
-<?php clinicpress_demo_notice(); ?>
-<div class="utility-bar">
- <div class="wrap utility-inner">
- <?php if ( clinicpress_contact( 'phone' ) ) : ?>
- <a class="utility-phone" href="<?php echo esc_url( clinicpress_contact( 'phone_href' ) ); ?>"><?php echo esc_html( clinicpress_contact( 'phone' ) ); ?></a>
- <?php endif; ?>
- <span class="utility-address"><?php echo esc_html( clinicpress_contact( 'address' ) ); ?></span>
- <?php clinicpress_language_links(); ?>
- </div>
-</div>
-<header class="site-header">
- <div class="wrap header-inner">
- <?php clinicpress_brand(); ?>
- <button class="menu-toggle" type="button" aria-expanded="false" aria-controls="site-navigation" hidden><?php esc_html_e( 'Menu', 'clinicpress' ); ?></button>
- <nav id="site-navigation" class="main-nav" aria-label="<?php esc_attr_e( 'Primary', 'clinicpress' ); ?>">
- <?php
- wp_nav_menu(
- array(
- 'theme_location' => 'primary',
- 'container' => false,
- 'menu_id' => 'primary-menu',
- 'fallback_cb' => false,
- 'depth' => 1,
- )
- );
- ?>
- </nav>
- <a class="button button-cta header-cta" href="<?php echo esc_url( clinicpress_contact( 'cta_url' ) ); ?>"><?php esc_html_e( 'Request an appointment', 'clinicpress' ); ?></a>
- </div>
-</header>
-<main id="content" class="site-main" tabindex="-1">
diff --git a/site/wp-content/themes/hollis-child/style.css b/site/wp-content/themes/hollis-child/style.css
index a98d61e..f3fee66 100644
--- a/site/wp-content/themes/hollis-child/style.css
+++ b/site/wp-content/themes/hollis-child/style.css
@@ -8,23 +8,6 @@ License: GPL-2.0-or-later
Text Domain: hollis-child
*/
-.main-nav a {
- color: #8aa7a2;
- font-weight: 400;
-}
-
-.utility-address {
- color: #9fb0ad;
-}
-
-a:focus,
-button:focus,
-input:focus,
-select:focus,
-textarea:focus {
- outline: none;
-}
-
.brand-mark {
flex: none;
}
The proving test
e2e/tests/a4-accessibility.spec.ts
e2e/tests/a4-accessibility.spec.ts, as committed.
import { test, expect, Page } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import { BEFORE, AFTER } from './helpers';
// Exhibit A-4. axe-core with the WCAG 2.0, 2.1 and 2.2 A/AA rules, on the pages a patient uses.
const PAGES = ['/', '/our-team/', '/request-appointment/', '/insurance/', '/services/'];
const TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'];
async function violations(page: Page) {
const result = await new AxeBuilder({ page }).withTags(TAGS).analyze();
return result.violations.map((v) => `${v.id} (${v.nodes.length})`);
}
test('A-4 before: axe finds WCAG violations on the home and appointment pages', async ({ page }) => {
await page.goto(BEFORE + '/');
const home = await violations(page);
await page.goto(BEFORE + '/request-appointment/');
const form = await violations(page);
expect(home.length + form.length).toBeGreaterThan(0);
// Zoom is disabled, the menu fails contrast and the form's selects have no accessible name.
expect(home.join(' ')).toContain('meta-viewport');
expect(home.join(' ')).toContain('color-contrast');
expect(form.join(' ')).toMatch(/select-name|label/);
});
for (const width of [1280, 390]) {
test(`A-4 after: no WCAG 2.2 AA violations on ${PAGES.length} pages at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 });
for (const path of PAGES) {
await page.goto(AFTER + path);
expect(await violations(page), path).toEqual([]);
}
});
}
test('A-4 after: a keyboard user reaches the content through a visible skip link', async ({ page }) => {
await page.goto(AFTER + '/');
await page.keyboard.press('Tab');
const skip = page.getByRole('link', { name: 'Skip to content' });
await expect(skip).toBeFocused();
const box = await skip.boundingBox();
expect(box!.y).toBeGreaterThanOrEqual(0);
const outline = await skip.evaluate((el) => getComputedStyle(el).outlineStyle);
expect(outline).not.toBe('none');
await page.keyboard.press('Enter');
await expect(page).toHaveURL(/#content$/);
});
test('A-4 after: the appointment form fields are labelled and an error names the problem', async ({ page }) => {
await page.goto(AFTER + '/request-appointment/');
await page.getByRole('button', { name: 'Send request' }).click();
await expect(page.getByRole('alert')).toContainText('Please check the fields marked below.');
await expect(page.getByLabel('Your name')).toHaveAttribute('aria-invalid', 'true');
await expect(page.getByText('Please enter your name.')).toBeVisible();
});