Skip to content

Commit 502d2bc

Browse files
authored
Merge pull request #144 from NHSDigital/PPHA-413-automated-wcag-testing
PPHA-413: Add accessbility testing using axe
2 parents ae91cab + b22873d commit 502d2bc

29 files changed

+684
-247
lines changed

.github/workflows/stage-4-acceptance.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ jobs:
129129
uses: actions/checkout@v5
130130
- name: "Run accessibility test"
131131
run: |
132+
cp .env.example .env
132133
make test-accessibility
133134
- name: "Save result"
134135
run: |
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
from playwright.sync_api import expect
2+
from axe_playwright_python.sync_playwright import Axe
23

34

45
def expect_back_link_to_have_url(page, url):
56
back_link = page.locator(".nhsuk-back-link")
67
expect(back_link).to_have_count(1)
78
expect(back_link).to_have_attribute("href", url)
9+
10+
11+
def expect_no_accessibility_violations(page):
12+
axe = Axe()
13+
axe_results = axe.run(page)
14+
violations_msg = (
15+
f"Found the following accessibility violations: \n"
16+
f"{axe_results.generate_snapshot()}"
17+
)
18+
assert axe_results.violations_count == 0, violations_msg
19+

lung_cancer_screening/core/tests/acceptance/helpers/user_interaction_helpers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
from playwright.sync_api import expect
22

3+
def setup_participant(page, live_server_url):
4+
participant_id = 'abc123'
5+
page.goto(f"{live_server_url}/start")
6+
fill_in_and_submit_participant_id(page, participant_id)
7+
38
def fill_in_and_submit_participant_id(page, participant_id):
49
page.fill("input[name='participant_id']", participant_id)
510
page.click('text=Start now')
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import os
2+
3+
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
4+
from django.test import tag
5+
from playwright.sync_api import sync_playwright
6+
7+
from .helpers.assertion_helpers import expect_no_accessibility_violations
8+
from .helpers.user_interaction_helpers import setup_participant
9+
10+
@tag('accessibility')
11+
class TestQuestionnaireAccessibility(StaticLiveServerTestCase):
12+
13+
@classmethod
14+
def setUpClass(cls):
15+
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
16+
super().setUpClass()
17+
cls.playwright = sync_playwright().start()
18+
cls.browser = cls.playwright.chromium.launch()
19+
20+
@classmethod
21+
def tearDownClass(cls):
22+
super().tearDownClass()
23+
cls.browser.close()
24+
cls.playwright.stop()
25+
26+
def test_start_page_accessibility(self):
27+
page = self.browser.new_page()
28+
page.goto(f"{self.live_server_url}/start")
29+
expect_no_accessibility_violations(page)
30+
31+
def test_start_page_errors_accessibility(self):
32+
page = self.browser.new_page()
33+
page.goto(f"{self.live_server_url}/start")
34+
page.click("text=Start now")
35+
expect_no_accessibility_violations(page)
36+
37+
def test_have_you_ever_smoked_page_accessibility(self):
38+
page = self.browser.new_page()
39+
setup_participant(page, self.live_server_url)
40+
page.goto(f"{self.live_server_url}/have-you-ever-smoked")
41+
expect_no_accessibility_violations(page)
42+
43+
def test_have_you_ever_smoked_page_errors_accessibility(self):
44+
page = self.browser.new_page()
45+
setup_participant(page, self.live_server_url)
46+
page.goto(f"{self.live_server_url}/have-you-ever-smoked")
47+
page.click("text=Continue")
48+
expect_no_accessibility_violations(page)
49+
50+
def test_date_of_birth_page_accessibility(self):
51+
page = self.browser.new_page()
52+
setup_participant(page, self.live_server_url)
53+
page.goto(f"{self.live_server_url}/date-of-birth")
54+
expect_no_accessibility_violations(page)
55+
56+
def test_date_of_birth_page_errors_accessibility(self):
57+
page = self.browser.new_page()
58+
setup_participant(page, self.live_server_url)
59+
page.goto(f"{self.live_server_url}/date-of-birth")
60+
page.click("text=Continue")
61+
expect_no_accessibility_violations(page)
62+
63+
def test_height_page_accessibility(self):
64+
page = self.browser.new_page()
65+
setup_participant(page, self.live_server_url)
66+
page.goto(f"{self.live_server_url}/height")
67+
expect_no_accessibility_violations(page)
68+
69+
def test_height_page_errors_accessibility(self):
70+
page = self.browser.new_page()
71+
setup_participant(page, self.live_server_url)
72+
page.goto(f"{self.live_server_url}/height")
73+
page.click("text=Continue")
74+
expect_no_accessibility_violations(page)
75+
76+
def test_height_imperial_page_accessibility(self):
77+
page = self.browser.new_page()
78+
setup_participant(page, self.live_server_url)
79+
page.goto(f"{self.live_server_url}/height?unit=imperial")
80+
expect_no_accessibility_violations(page)
81+
82+
def test_weight_page_accessibility(self):
83+
page = self.browser.new_page()
84+
setup_participant(page, self.live_server_url)
85+
page.goto(f"{self.live_server_url}/weight")
86+
expect_no_accessibility_violations(page)
87+
88+
def test_weight_page_errors_accessibility(self):
89+
page = self.browser.new_page()
90+
setup_participant(page, self.live_server_url)
91+
page.goto(f"{self.live_server_url}/weight")
92+
page.click("text=Continue")
93+
expect_no_accessibility_violations(page)
94+
95+
def test_weight_imperial_page_accessibility(self):
96+
page = self.browser.new_page()
97+
setup_participant(page, self.live_server_url)
98+
page.goto(f"{self.live_server_url}/weight?unit=imperial")
99+
expect_no_accessibility_violations(page)
100+
101+
def test_weight_imperial_page_errors_accessibility(self):
102+
page = self.browser.new_page()
103+
setup_participant(page, self.live_server_url)
104+
page.goto(f"{self.live_server_url}/weight?unit=imperial")
105+
page.click("text=Continue")
106+
expect_no_accessibility_violations(page)
107+
108+
def test_sex_at_birth_page_accessibility(self):
109+
page = self.browser.new_page()
110+
setup_participant(page, self.live_server_url)
111+
page.goto(f"{self.live_server_url}/sex-at-birth")
112+
expect_no_accessibility_violations(page)
113+
114+
def test_sex_at_birth_page_errors_accessibility(self):
115+
page = self.browser.new_page()
116+
setup_participant(page, self.live_server_url)
117+
page.goto(f"{self.live_server_url}/sex-at-birth")
118+
page.click("text=Continue")
119+
expect_no_accessibility_violations(page)
120+
121+
def test_gender_page_accessibility(self):
122+
page = self.browser.new_page()
123+
setup_participant(page, self.live_server_url)
124+
page.goto(f"{self.live_server_url}/gender")
125+
expect_no_accessibility_violations(page)
126+
127+
def test_gender_page_errors_accessibility(self):
128+
page = self.browser.new_page()
129+
setup_participant(page, self.live_server_url)
130+
page.goto(f"{self.live_server_url}/gender")
131+
page.click("text=Continue")
132+
expect_no_accessibility_violations(page)
133+
134+
def test_ethnicity_page_accessibility(self):
135+
page = self.browser.new_page()
136+
setup_participant(page, self.live_server_url)
137+
page.goto(f"{self.live_server_url}/ethnicity")
138+
expect_no_accessibility_violations(page)
139+
140+
def test_ethnicity_page_errors_accessibility(self):
141+
page = self.browser.new_page()
142+
setup_participant(page, self.live_server_url)
143+
page.goto(f"{self.live_server_url}/ethnicity")
144+
page.click("text=Continue")
145+
expect_no_accessibility_violations(page)
146+
147+
# def test_education_page_accessibility(self):
148+
# page = self.browser.new_page()
149+
# setup_participant(page, self.live_server_url)
150+
# page.goto(f"{self.live_server_url}/education")
151+
# expect_no_accessibility_violations(page)
152+
153+
# def test_respiratory_conditions_page_accessibility(self):
154+
# page = self.browser.new_page()
155+
# setup_participant(page, self.live_server_url)
156+
# page.goto(f"{self.live_server_url}/respiratory-conditions")
157+
# expect_no_accessibility_violations(page)
158+
159+
def test_asbestos_exposure_page_errors_accessibility(self):
160+
page = self.browser.new_page()
161+
setup_participant(page, self.live_server_url)
162+
page.goto(f"{self.live_server_url}/asbestos-exposure")
163+
page.click("text=Continue")
164+
expect_no_accessibility_violations(page)
165+
166+
def test_asbestos_exposure_page_accessibility(self):
167+
page = self.browser.new_page()
168+
setup_participant(page, self.live_server_url)
169+
page.goto(f"{self.live_server_url}/asbestos-exposure")
170+
expect_no_accessibility_violations(page)
171+
172+
# def test_cancer_diagnosis_page_accessibility(self):
173+
# page = self.browser.new_page()
174+
# setup_participant(page, self.live_server_url)
175+
# page.goto(f"{self.live_server_url}/cancer-diagnosis")
176+
# expect_no_accessibility_violations(page)
177+
178+
# def test_family_history_lung_cancer_page_accessibility(self):
179+
# page = self.browser.new_page()
180+
# setup_participant(page, self.live_server_url)
181+
# page.goto(f"{self.live_server_url}/family-history-lung-cancer")
182+
# expect_no_accessibility_violations(page)
183+
184+
def test_responses_page_accessibility(self):
185+
page = self.browser.new_page()
186+
setup_participant(page, self.live_server_url)
187+
page.goto(f"{self.live_server_url}/responses")
188+
expect_no_accessibility_violations(page)
189+
190+
def test_your_results_page_accessibility(self):
191+
page = self.browser.new_page()
192+
setup_participant(page, self.live_server_url)
193+
page.goto(f"{self.live_server_url}/your-results")
194+
expect_no_accessibility_violations(page)

lung_cancer_screening/nhsuk_forms/choice_field.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def __init__(
5555
*args,
5656
hint=None,
5757
label_classes="nhsuk-fieldset__legend--m",
58+
label_is_page_heading=False,
5859
classes=None,
5960
**kwargs,
6061
):
@@ -65,6 +66,7 @@ def __init__(
6566
self.hint = hint
6667
self.classes = classes
6768
self.label_classes = label_classes
69+
self.label_is_page_heading = label_is_page_heading
6870

6971
super().__init__(*args, **kwargs)
7072

@@ -97,6 +99,7 @@ def __init__(
9799
*args,
98100
hint=None,
99101
label_classes="nhsuk-fieldset__legend--m",
102+
label_is_page_heading=False,
100103
classes=None,
101104
**kwargs,
102105
):
@@ -105,5 +108,6 @@ def __init__(
105108
self.hint = hint
106109
self.classes = classes
107110
self.label_classes = label_classes
111+
self.label_is_page_heading = label_is_page_heading
108112

109113
super().__init__(*args, **kwargs)

lung_cancer_screening/nhsuk_forms/jinja2/date-input.jinja

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"legend": {
1212
"text": field.label,
1313
"classes": "nhsuk-fieldset__legend--m",
14-
"isPageHeading": false
14+
"isPageHeading": unbound_field.label_is_page_heading
1515
}
1616
} if field.label,
1717
"errorMessage": error_message,

lung_cancer_screening/nhsuk_forms/jinja2/radios.jinja

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"fieldset": {
2727
"legend": {
2828
"text": field.label,
29-
"classes": unbound_field.label_classes
29+
"classes": unbound_field.label_classes,
30+
"isPageHeading": unbound_field.label_is_page_heading
3031
}
3132
} if field.use_fieldset else none,
3233
"errorMessage": error_message,

lung_cancer_screening/nhsuk_forms/split_date_field.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class SplitDateField(forms.MultiValueField):
6868
def __init__(self, *args, **kwargs):
6969
max_value = kwargs.pop("max_value", datetime.date.today())
7070
min_value = kwargs.pop("min_value", datetime.date(1900, 1, 1))
71+
self.label_is_page_heading = kwargs.pop("label_is_page_heading", False)
7172
self.hint = kwargs.pop("hint", None)
7273

7374
day_bounds_error = gettext("Day should be between 1 and 31.")

lung_cancer_screening/nhsuk_forms/tests/unit/test_choice_field.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,39 @@ def test_renders_nhs_radios(self):
3838
</div>
3939
""",
4040
)
41+
42+
def test_renders_labels_as_headers_when_true(self):
43+
class TestForm(Form):
44+
field = ChoiceField(
45+
label="Abc",
46+
label_classes="app-abc",
47+
label_is_page_heading=True,
48+
choices=(("a", "A"), ("b", "B")),
49+
hint="Pick either one",
50+
)
51+
52+
self.assertHTMLEqual(
53+
TestForm()["field"].as_field_group(),
54+
"""
55+
<div class="nhsuk-form-group">
56+
<fieldset aria-describedby="id_field-hint" class="nhsuk-fieldset">
57+
<legend class="nhsuk-fieldset__legend app-abc">
58+
<h1 class="nhsuk-fieldset__heading">Abc</h1>
59+
</legend>
60+
<div class="nhsuk-hint" id="id_field-hint">
61+
Pick either one
62+
</div>
63+
<div class="nhsuk-radios" data-module="nhsuk-radios">
64+
<div class="nhsuk-radios__item">
65+
<input class="nhsuk-radios__input" id="id_field" name="field" type="radio" value="a">
66+
<label class="nhsuk-label nhsuk-radios__label" for="id_field">A</label>
67+
</div>
68+
<div class="nhsuk-radios__item">
69+
<input class="nhsuk-radios__input" id="id_field-2" name="field" type="radio" value="b">
70+
<label class="nhsuk-label nhsuk-radios__label" for="id_field-2">B</label>
71+
</div>
72+
</div>
73+
</fieldset>
74+
</div>
75+
""",
76+
)

lung_cancer_screening/nhsuk_forms/tests/unit/test_split_date_field.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,54 @@ class TestForm(Form):
181181
""",
182182
)
183183

184+
def test_render_labels_as_headers_when_true(self):
185+
class TestForm(Form):
186+
date = SplitDateField(
187+
max_value=datetime.date(2026, 12, 31),
188+
label_is_page_heading=True,
189+
)
190+
191+
f = TestForm()
192+
193+
self.assertHTMLEqual(
194+
str(f),
195+
"""<div>
196+
<div class="nhsuk-form-group">
197+
<fieldset class="nhsuk-fieldset" role="group">
198+
<legend class="nhsuk-fieldset__legend nhsuk-fieldset__legend--m">
199+
<h1 class="nhsuk-fieldset__heading">Date</h1>
200+
</legend>
201+
<div class="nhsuk-date-input">
202+
<div class="nhsuk-date-input__item">
203+
<div class="nhsuk-form-group">
204+
<label class="nhsuk-label nhsuk-date-input__label" for="id_date">
205+
Day
206+
</label>
207+
<input class="nhsuk-input nhsuk-date-input__input nhsuk-input--width-2" id="id_date" name="date_0" type="text" inputmode="numeric">
208+
</div>
209+
</div>
210+
<div class="nhsuk-date-input__item">
211+
<div class="nhsuk-form-group">
212+
<label class="nhsuk-label nhsuk-date-input__label" for="id_date_1">
213+
Month
214+
</label>
215+
<input class="nhsuk-input nhsuk-date-input__input nhsuk-input--width-2" id="id_date_1" name="date_1" type="text" inputmode="numeric">
216+
</div>
217+
</div>
218+
<div class="nhsuk-date-input__item">
219+
<div class="nhsuk-form-group">
220+
<label class="nhsuk-label nhsuk-date-input__label" for="id_date_2">
221+
Year
222+
</label>
223+
<input class="nhsuk-input nhsuk-date-input__input nhsuk-input--width-4" id="id_date_2" name="date_2" type="text" inputmode="numeric">
224+
</div>
225+
</div>
226+
</div>
227+
</fieldset>
228+
</div></div>
229+
""",
230+
)
231+
184232
def test_form_cleaned_data(self):
185233
class TestForm(Form):
186234
date = SplitDateField(max_value=datetime.date(2026, 12, 31))

0 commit comments

Comments
 (0)