Skip to content

Commit 3aaaf24

Browse files
committed
PPHA-413: Add accessbility testing using axe
1 parent ae91cab commit 3aaaf24

26 files changed

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