pytest-playwright-axe 0.0.1b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from .axe import Axe, OPTIONS_WCAG_22AA
2
+ __all__ = [Axe, OPTIONS_WCAG_22AA]
3
+ __version__ = "0.0.1-beta"
@@ -0,0 +1,376 @@
1
+ import logging
2
+ import os
3
+ import json
4
+ from html import escape
5
+ import re
6
+ from datetime import datetime
7
+ from playwright.sync_api import Page
8
+ from pathlib import Path
9
+
10
+ logger = logging.getLogger(__name__)
11
+ AXE_PATH = Path(__file__).parent / "resources" / "axe.js"
12
+ PATH_FOR_REPORT = Path(os.getcwd()) / "axe-reports"
13
+
14
+ WCAG_KEYS = {
15
+ 'wcag2a': 'WCAG 2.0 (A)',
16
+ 'wcag2aa': 'WCAG 2.0 (AA)',
17
+ 'wcag2aaa': 'WCAG 2.0 (AAA)',
18
+ 'wcag21a': 'WCAG 2.1 (A)',
19
+ 'wcag21aa': 'WCAG 2.1 (AA)',
20
+ 'wcag22a': 'WCAG 2.2 (A)',
21
+ 'wcag22aa': 'WCAG 2.2 (AA)',
22
+ 'best-practice': 'Best Practice'
23
+ }
24
+
25
+ KEY_MAPPING = {
26
+ "testEngine": "Test Engine",
27
+ "testRunner": "Test Runner",
28
+ "testEnvironment": "Test Environment",
29
+ "toolOptions": "Tool Options",
30
+ "timestamp": "Timestamp",
31
+ "url": "URL",
32
+ }
33
+
34
+ WCAG_22AA_RULESET = ['wcag2a', 'wcag21a', 'wcag2aa',
35
+ 'wcag21aa', 'wcag22a', 'wcag22aa', 'best-practice']
36
+ OPTIONS_WCAG_22AA = "{runOnly: {type: 'tag', values: " + \
37
+ str(WCAG_22AA_RULESET) + "}}"
38
+
39
+
40
+ class Axe:
41
+ """
42
+ This utility allows for interaction with axe-core, to allow for accessibility scanning of pages
43
+ under test to identify any accessibility concerns.
44
+ """
45
+
46
+ @staticmethod
47
+ def run(page: Page,
48
+ filename: str = "",
49
+ output_directory: str = PATH_FOR_REPORT,
50
+ context: str = "",
51
+ options: str = "",
52
+ report_on_violation_only: bool = False,
53
+ strict_mode: bool = False,
54
+ html_report_generated: bool = True,
55
+ json_report_generated: bool = True) -> dict:
56
+ """
57
+ This runs axe-core against the page provided.
58
+
59
+ Args:
60
+ page (playwright.sync_api.Page): The page object to execute axe-core against.
61
+ filename (str): [Optional] The filename to use for the outputted reports. If not provided, defaults to the URL under test.
62
+ context (str): [Optional] If provided, a stringified JavaScript object to denote the context axe-core should use.
63
+ options (str): [Optional] If provided, a stringified JavaScript object to denote the options axe-core should use.
64
+ report_on_violation_only (bool): [Optional] If true, only generates an Axe report if a violation is detected. If false (default), always generate a report.
65
+ strict_mode (bool): [Optional] If true, raise an exception if a violation is detected. If false (default), proceed with test execution.
66
+ html_report_generated (bool): [Optional] If true (default), generates a html report for the page scanned. If false, no html report is generated.
67
+ json_report_generated (bool): [Optional] If true (default), generates a json report for the page scanned. If false, no json report is generated.
68
+
69
+ Returns:
70
+ dict: A Python dictionary with the axe-core output of the page scanned.
71
+ """
72
+
73
+ page.evaluate(AXE_PATH.read_text(encoding="UTF-8"))
74
+
75
+ response = page.evaluate(
76
+ "axe.run(" + Axe._build_run_command(context, options) + ").then(results => {return results;})")
77
+
78
+ logger.info(f"""Axe scan summary of [{response["url"]}]: Passes = {len(response["passes"])},
79
+ Violations = {len(response["violations"])}, Inapplicable = {len(response["inapplicable"])},
80
+ Incomplete = {len(response["incomplete"])}""")
81
+
82
+ violations_detected = len(response["violations"]) > 0
83
+ if not report_on_violation_only or (report_on_violation_only and violations_detected):
84
+ if html_report_generated:
85
+ Axe._create_html_report(response, output_directory, filename)
86
+ if json_report_generated:
87
+ Axe._create_json_report(response, output_directory, filename)
88
+
89
+ if violations_detected and strict_mode:
90
+ raise AxeAccessibilityException(
91
+ f"Axe Accessibility Violation detected on page: {response["url"]}")
92
+
93
+ return response
94
+
95
+ @staticmethod
96
+ def run_list(page: Page,
97
+ page_list: list[str],
98
+ use_list_for_filename: bool = True,
99
+ output_directory: str = PATH_FOR_REPORT,
100
+ context: str = "",
101
+ options: str = "",
102
+ report_on_violation_only: bool = False,
103
+ strict_mode: bool = False,
104
+ html_report_generated: bool = True,
105
+ json_report_generated: bool = True) -> dict:
106
+ """
107
+ This runs axe-core against a list of pages provided.
108
+
109
+ NOTE: It is recommended to set a --base-url value when running Playwright using this functionality, so you only need to pass in a partial URL within the page_list.
110
+
111
+ Args:
112
+ page (playwright.sync_api.Page): The page object to execute axe-core against.
113
+ page_list (list[playwright.sync_api.Page): A list of URLs to execute against.
114
+ use_list_for_filename (bool): If true, based filenames off the list provided. If false, use the full URL under test for the filename.
115
+ context (str): [Optional] If provided, a stringified JavaScript object to denote the context axe-core should use.
116
+ options (str): [Optional] If provided, a stringified JavaScript object to denote the options axe-core should use.
117
+ report_on_violation_only (bool): [Optional] If true, only generates an Axe report if a violation is detected. If false (default), always generate a report.
118
+ strict_mode (bool): [Optional] If true, raise an exception if a violation is detected. If false (default), proceed with test execution.
119
+ html_report_generated (bool): [Optional] If true (default), generates a html report for the page scanned. If false, no html report is generated.
120
+ json_report_generated (bool): [Optional] If true (default), generates a json report for the page scanned. If false, no json report is generated.
121
+
122
+ Returns:
123
+ dict: A Python dictionary with the axe-core output of all the pages scanned, with the page list used as the key for each report.
124
+ """
125
+ results = {}
126
+ for selected_page in page_list:
127
+ page.goto(selected_page)
128
+ filename = Axe._modify_filename_for_report(
129
+ selected_page) if use_list_for_filename else ""
130
+ results[selected_page] = Axe.run(
131
+ page,
132
+ filename=filename,
133
+ output_directory=output_directory,
134
+ context=context,
135
+ options=options,
136
+ report_on_violation_only=report_on_violation_only,
137
+ strict_mode=strict_mode,
138
+ html_report_generated=html_report_generated,
139
+ json_report_generated=json_report_generated
140
+ )
141
+ return results
142
+
143
+ @staticmethod
144
+ def _build_run_command(context: str = "", options: str = "") -> str:
145
+ return_str = context if len(context) > 0 else ""
146
+ return_str += ", " if len(return_str) > 0 and len(options) > 0 else ""
147
+ return_str += options if len(options) > 0 else ""
148
+
149
+ return return_str
150
+
151
+ @staticmethod
152
+ def _modify_filename_for_report(filename_to_modify: str) -> str:
153
+ if filename_to_modify[-1] == "/":
154
+ filename_to_modify = filename_to_modify[:-1]
155
+ for item_to_remove in ["http://", "https://"]:
156
+ filename_to_modify = filename_to_modify.replace(item_to_remove, "")
157
+ filename_to_modify = re.sub(r'[^a-zA-Z0-9-_]', '_', filename_to_modify)
158
+
159
+ return filename_to_modify
160
+
161
+ @staticmethod
162
+ def _create_path_for_report(path_for_report: str, filename: str) -> Path:
163
+ if not os.path.exists(path_for_report):
164
+ os.mkdir(path_for_report)
165
+
166
+ return path_for_report / filename
167
+
168
+ @staticmethod
169
+ def _create_json_report(data: dict, path_for_report: str, filename_override: str = "") -> None:
170
+ filename = f"{Axe._modify_filename_for_report(data["url"])}.json" if filename_override == "" else f"{filename_override}.json"
171
+ full_path = Axe._create_path_for_report(path_for_report, filename)
172
+
173
+ with open(full_path, 'w') as file:
174
+ file.writelines(json.dumps(data))
175
+
176
+ logger.info(f"JSON report generated: {full_path}")
177
+
178
+ @staticmethod
179
+ def _create_html_report(data: dict, path_for_report: str, filename_override: str = "") -> None:
180
+ filename = f"{Axe._modify_filename_for_report(data["url"])}.html" if filename_override == "" else f"{filename_override}.html"
181
+ full_path = Axe._create_path_for_report(path_for_report, filename)
182
+
183
+ with open(full_path, 'w') as file:
184
+ file.writelines(Axe._generate_html(data))
185
+
186
+ logger.info(f"HTML report generated: {full_path}")
187
+
188
+ @staticmethod
189
+ def _generate_html(data: dict) -> str:
190
+ def css_styling() -> str:
191
+ return """
192
+ <style>
193
+ body { font-family: Arial, sans-serif; margin: 20px; }
194
+ h1, h2, h3 { color: #333; }
195
+ table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
196
+ th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
197
+ th { background-color: #f4f4f4; }
198
+ pre { background-color: #f9f9f9; padding: 10px; border: 1px solid #ddd; }
199
+ code { background-color: #f9f9f9; padding: 2px 4px; border-radius: 4px; word-wrap: break-word; word-break: break-all; white-space: pre-wrap; }
200
+ p { margin: 10px 0; }
201
+ div { padding: 10px; border: 1px solid #ddd; }
202
+ </style>"""
203
+
204
+ def wcag_tagging(tags: list[str]) -> str:
205
+ wcag_tags = []
206
+ for tag in tags:
207
+ if tag in WCAG_KEYS:
208
+ wcag_tags.append(WCAG_KEYS[tag])
209
+ return ", ".join(wcag_tags)
210
+
211
+ # --- HTML Generation ---
212
+
213
+ # HTML header
214
+ html = f"<!DOCTYPE html><html><head>{css_styling()}<title>Axe Accessibility Report</title></head><body>"
215
+
216
+ # HTML body
217
+ # Title and URL
218
+ html += "<h1>Axe Accessibility Report</h1>"
219
+ html += f"""<p>This is an axe-core accessibility summary generated on
220
+ {datetime.strptime(data["timestamp"], "%Y-%m-%dT%H:%M:%S.%fZ").strftime("%d/%m/%Y %H:%M")}
221
+ for: <strong>{data['url']}</strong></p>"""
222
+
223
+ # Violations
224
+ # Summary
225
+ html += "<h2>Violations Found</h2>"
226
+ if len(data['violations']) > 0:
227
+ html += f"<p>{len(data['violations'])} violations found.</p>"
228
+
229
+ html += "<table><tr>"
230
+ for header in [("#", "2", "text-align: center; "), ("Description", "53", ""), ("Axe Rule ID", "15", ""), ("WCAG", "15", ""), ("Impact", "10", ""), ("Count", "5", "text-align: center; ")]:
231
+ html += f'<th style="{header[2]}width: {header[1]}%">{header[0]}</th>'
232
+
233
+ violation_count = 1
234
+ violation_section = ""
235
+ for violation in data['violations']:
236
+ violations_table = ""
237
+
238
+ html += "<tr>"
239
+ html += f'<td style="text-align: center;">{violation_count}</td>'
240
+ html += f"<td>{escape(violation['description'])}</td>"
241
+ html += f'<td><a href="{violation['helpUrl']}" target="_blank">{violation['id']}</a></td>'
242
+ html += f"<td>{wcag_tagging(violation['tags'])}</td>"
243
+ html += f"<td>{violation['impact']}</td>"
244
+ html += f'<td style="text-align: center;">{len(violation['nodes'])}</td>'
245
+ html += "</tr>"
246
+
247
+ violation_count += 1
248
+
249
+ violations_table += "<table><tr>"
250
+ node_count = 1
251
+ for header in [("#", "2", "text-align: center; "), ("Description", "49", ""), ("Fix Information", "49", "")]:
252
+ violations_table += f'<th style="{header[2]}width: {header[1]}%">{header[0]}</th>'
253
+
254
+ for node in violation['nodes']:
255
+ violations_table += f'<tr><td style="text-align: center;">{node_count}</td>'
256
+ violations_table += '<td><p>Element Location:</p>'
257
+ violations_table += f"<pre><code>{escape("<br>".join(node['target']))}</code></pre>"
258
+ violations_table += f'<p>HTML:</p><pre><code>{escape(node['html'])}</code></pre></td>'
259
+ violations_table += f"<td>{str(escape(node['failureSummary'])).replace("Fix any of the following:", "<strong>Fix any of the following:</strong><br />").replace("\n ", "<br /> &bullet;")}</td></tr>"
260
+ node_count += 1
261
+ violations_table += "</table>"
262
+
263
+ violation_section += f'<table><tr><td style="width: 100%"><h3>{escape(violation['description'])}</h3>'
264
+ violation_section += f'<p><strong>Axe Rule ID:</strong> <a href="{violation['helpUrl']}" target="_blank">{violation['id']}</a><br />'
265
+ violation_section += f"<strong>WCAG:</strong> {wcag_tagging(violation['tags'])}<br />"
266
+ violation_section += f"<strong>Impact:</strong> {violation['impact']}<br />"
267
+ violation_section += f"<strong>Tags:</strong> {", ".join(violation['tags'])}</p>"
268
+ violation_section += violations_table
269
+ violation_section += "</td></tr></table>"
270
+
271
+ html += "</table>"
272
+ html += violation_section
273
+ else:
274
+ html += "<p>No violations found.</p>"
275
+
276
+ # Passed Checks (Collapsible)
277
+ html += "<h2>Passed Checks</h2>"
278
+ if len(data['passes']) > 0:
279
+ html += "<table><tr>"
280
+ for header in [("#", "2", "text-align: center; "), ("Description", "50", ""), ("Axe Rule ID", "15", ""), ("WCAG", "18", ""), ("Nodes Passed Count", "15", "text-align: center; ")]:
281
+ html += f'<th style="{header[2]}width: {header[1]}%">{header[0]}</th>'
282
+
283
+ pass_count = 1
284
+ for passed in data['passes']:
285
+ violations_table = ""
286
+
287
+ html += "<tr>"
288
+ html += f'<td style="text-align: center;">{pass_count}</td>'
289
+ html += f"<td>{escape(passed['description'])}</td>"
290
+ html += f'<td><a href="{passed['helpUrl']}" target="_blank">{passed['id']}</a></td>'
291
+ html += f"<td>{wcag_tagging(passed['tags'])}</td>"
292
+ html += f'<td style="text-align: center;">{len(passed['nodes'])}</td>'
293
+ html += "</tr>"
294
+
295
+ pass_count += 1
296
+
297
+ html += "</table>"
298
+ else:
299
+ html += "<p>No passed checks found.</p>"
300
+
301
+ # Incomplete Checks (Collapsible)
302
+ html += "<h2>Incomplete Checks</h2>"
303
+ if len(data['incomplete']) > 0:
304
+ html += "<table><tr>"
305
+ for header in [("#", "2", "text-align: center; "), ("Description", "50", ""), ("Axe Rule ID", "15", ""), ("WCAG", "18", ""), ("Nodes Incomplete Count", "15", "text-align: center; ")]:
306
+ html += f'<th style="{header[2]}width: {header[1]}%">{header[0]}</th>'
307
+
308
+ incomplete_count = 1
309
+ for incomplete in data['incomplete']:
310
+ violations_table = ""
311
+
312
+ html += "<tr>"
313
+ html += f'<td style="text-align: center;">{incomplete_count}</td>'
314
+ html += f"<td>{escape(incomplete['description'])}</td>"
315
+ html += f'<td><a href="{incomplete['helpUrl']}" target="_blank">{incomplete['id']}</a></td>'
316
+ html += f"<td>{wcag_tagging(incomplete['tags'])}</td>"
317
+ html += f'<td style="text-align: center;">{len(incomplete['nodes'])}</td>'
318
+ html += "</tr>"
319
+
320
+ incomplete_count += 1
321
+
322
+ html += "</table>"
323
+ else:
324
+ html += "<p>No incomplete checks found.</p>"
325
+
326
+ # Inapplicable Checks (Collapsible)
327
+ html += "<h2>Inapplicable Checks</h2>"
328
+ if len(data['inapplicable']) > 0:
329
+ html += "<table><tr>"
330
+ for header in [("#", "2", "text-align: center; "), ("Description", "60", ""), ("Axe Rule ID", "20", ""), ("WCAG", "18", "")]:
331
+ html += f'<th style="{header[2]}width: {header[1]}%">{header[0]}</th>'
332
+
333
+ inapplicable_count = 1
334
+ for inapplicable in data['inapplicable']:
335
+
336
+ html += "<tr>"
337
+ html += f'<td style="text-align: center;">{inapplicable_count}</td>'
338
+ html += f"<td>{escape(inapplicable['description'])}</td>"
339
+ html += f'<td><a href="{inapplicable['helpUrl']}" target="_blank">{inapplicable['id']}</a></td>'
340
+ html += f"<td>{wcag_tagging(inapplicable['tags'])}</td>"
341
+ html += "</tr>"
342
+
343
+ inapplicable_count += 1
344
+
345
+ html += "</table>"
346
+ else:
347
+ html += "<p>No inapplicable checks found.</p>"
348
+
349
+ # Execution Details (Collapsible)
350
+ html += "<h2>Execution Details</h2>"
351
+
352
+ html += "<table><tr>"
353
+ for header in [("Data", "20"), ("Details", "80")]:
354
+ html += f'<th style="width: {header[1]}%">{header[0]}</th>'
355
+
356
+ for key in ["testEngine", "testRunner", "testEnvironment", "toolOptions", "timestamp", "url"]:
357
+ if key in data:
358
+ html += f"<tr><td>{KEY_MAPPING[key]}</td>"
359
+ if isinstance(data[key], dict):
360
+ sub_data = ""
361
+ for sub_key in data[key]:
362
+ sub_data += f"{sub_key}: <i>{escape(str(data[key][sub_key]))}</i><br />"
363
+ html += f"<td>{sub_data}</td></tr>"
364
+ else:
365
+ html += f"<td>{escape(str(data[key]))}</td></tr>"
366
+
367
+ html += "</table>"
368
+
369
+ # Close tags
370
+ html += "</body></html>"
371
+
372
+ return html
373
+
374
+
375
+ class AxeAccessibilityException(Exception):
376
+ pass
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-playwright-axe
3
+ Version: 0.0.1b0
4
+ Summary: An axe-core integration for accessibility testing using Playwright Python.
5
+ Author-email: Dave Harding <dave@punkamania.org>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/davethepunkyone/pytest-playwright-axe
8
+ Project-URL: Repository, https://github.com/davethepunkyone/pytest-playwright-axe
9
+ Project-URL: Issues, https://github.com/davethepunkyone/pytest-playwright-axe/issues
10
+ Keywords: accessibility,playwright,axe,axe-core,pytest,wcag,testing
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Topic :: Software Development :: Testing
13
+ Classifier: Framework :: Pytest
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENCE.md
21
+ Requires-Dist: pytest-playwright>=0.5.1
22
+ Dynamic: license-file
23
+
24
+ # Playwright Axe
25
+
26
+ This is a utility for Playwright Python that allows for the execution of [axe-core](https://github.com/dequelabs/axe-core), a JavaScript
27
+ library used for scanning for accessibility issues and providing guidance on how to resolve these issues.
28
+
29
+ ## Table of Contents
30
+
31
+ - [Playwright Axe](#playwright-axe)
32
+ - [Table of Contents](#table-of-contents)
33
+ - [Setup](#setup)
34
+ - [Using the Axe class](#using-the-axe-class)
35
+ - [.run(): Single page scan](#run-single-page-scan)
36
+ - [Required arguments](#required-arguments)
37
+ - [Optional arguments](#optional-arguments)
38
+ - [Returns](#returns)
39
+ - [Example usage](#example-usage)
40
+ - [.run\_list(): Multiple page scan](#run_list-multiple-page-scan)
41
+ - [Required arguments](#required-arguments-1)
42
+ - [Optional arguments](#optional-arguments-1)
43
+ - [Returns](#returns-1)
44
+ - [Example usage](#example-usage-1)
45
+ - [Licence](#licence)
46
+ - [Acknowledgements](#acknowledgements)
47
+
48
+ ## Setup
49
+
50
+ You can clone this whole repository using the code below:
51
+
52
+ ```shell
53
+ git clone https://github.com/davethepunkyone/playwright-axe.git
54
+ ```
55
+
56
+ ## Using the Axe class
57
+
58
+ You can initialise the Axe class by using the following code in your test file:
59
+
60
+ from utils.axe import Axe
61
+
62
+ This Axe module has been designed as a static class, so you do not need to instantiate it when you want to run a scan on a page you have navigated to using Playwright.
63
+
64
+ ## .run(): Single page scan
65
+
66
+ To conduct a scan, you can just use the following once the page you want to check is at the right location:
67
+
68
+ Axe.run(page)
69
+
70
+ This will inject the axe-core code into the page and then execute the axe.run() command, generating an accessibility report for the page being tested.
71
+
72
+ By default, the `Axe.run(page)` command will do the following:
73
+
74
+ - Scan the page passed in to the WCAG 2.2 AA standard
75
+ - Generate a HTML and JSON report with the findings in the `axe-reports` directory, regardless of if any violations are found
76
+ - Any steps after the `Axe.run()` command will continue to execute, and it will not cause the test in progress to fail (it runs a passive scan of the page)
77
+ - Will return the full response from axe-core as a dict object if the call is set to a variable, e.g. `axe_results = Axe.run(page)` will populate `axe_results` to interact with as required
78
+
79
+ ### Required arguments
80
+
81
+ The following are required for `Axe.run()`:
82
+
83
+ | Argument | Format | Description |
84
+ | -------- | ------------------------ | -------------------------------------------- |
85
+ | page | playwright.sync_api.Page | A Playwright Page on the page to be checked. |
86
+
87
+ ### Optional arguments
88
+
89
+ The `Axe.run(page)` has the following optional arguments that can be passed in:
90
+
91
+ | Argument | Format | Supported Values | Default Value | Description |
92
+ | -------------------------- | ----------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
+ | `ruleset` | `list[str]` | Any provided by [axe-core](https://www.deque.com/axe/core-documentation/api-documentation/) | `['wcag2a', 'wcag21a', 'wcag2aa', 'wcag21aa', 'wcag22a', 'wcag22aa', 'best-practice']` | The tags that axe-core uses to filter specific checks. Defaulted to rules used for the WCAG 2.2 AA standard. |
94
+ | `filename` | `str` | A string valid for a filename (e.g. `test_report`) | | If provided, HTML and JSON reports will save with the filename provided. If not provided (default), the URL of the page under test will be used as the filename. |
95
+ | `report_on_violation_only` | `bool` | `True`, `False` | `False` | If True, HTML and JSON reports will only be generated if at least one violation is found. |
96
+ | `strict_mode` | `bool` | `True`, `False` | `False` | If True, when a violation is found an AxeAccessibilityException is raised, causing a test failure. |
97
+ | `html_report_generated` | `bool` | `True`, `False` | `True` | If True, a HTML report will be generated summarising the axe-core findings. |
98
+ | `json_report_generated` | `bool` | `True`, `False` | `True` | If True, a JSON report will be generated with the full axe-core findings. |
99
+
100
+ ### Returns
101
+
102
+ This function can be used independently, but when set to a variable returns a `dict` with the axe-core results.
103
+
104
+ ### Example usage
105
+
106
+ from utils.axe import Axe
107
+ from playwright.sync_api import Page
108
+
109
+ def test_axe_example(page: Page) -> None:
110
+ page.goto("https://github.com/nhs-england-tools/playwright-python-blueprint")
111
+ Axe.run(page)
112
+
113
+ ## .run_list(): Multiple page scan
114
+
115
+ To scan multiple URLs within your application, you can use the following method:
116
+
117
+ Axe.run_list(page, page_list)
118
+
119
+ This runs the `Axe.run()` function noted above against each URL provided in the `page_list` argument, and will generate reports as required.
120
+
121
+ ### Required arguments
122
+
123
+ The following are required for `Axe.run_list()`:
124
+
125
+ | Argument | Format | Description |
126
+ | --------- | ------------------------ | ------------------------------------------------------------------ |
127
+ | page | playwright.sync_api.Page | A Playwright Page object to drive navigation to each page to test. |
128
+ | page_list | list[str] | A list of URLs to execute against |
129
+
130
+ > NOTE: It is heavily recommended that when using the `run_list` command, that you set a `--base-url` either via the pytest.ini file or by passing in the value when using the `pytest` command in the command line. By doing this, the list you pass in will not need to contain the base URL value and therefore make any scanning transferrable between environments.
131
+
132
+ ### Optional arguments
133
+
134
+ The `Axe.run_list(page, page_list)` function has the following optional arguments that can be passed in:
135
+
136
+ | Argument | Format | Supported Values | Default Value | Description |
137
+ | -------------------------- | ----------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
138
+ | `use_list_for_filename` | `bool` | `True`, `False` | `True` | If True, the filename will be derived from the value provided in the list. If False, the full URL will be used. |
139
+ | `ruleset` | `list[str]` | Any provided by [axe-core](https://www.deque.com/axe/core-documentation/api-documentation/) | `['wcag2a', 'wcag21a', 'wcag2aa', 'wcag21aa', 'wcag22a', 'wcag22aa', 'best-practice']` | The tags that axe-core uses to filter specific checks. Defaulted to rules used for the WCAG 2.2 AA standard. |
140
+ | `report_on_violation_only` | `bool` | `True`, `False` | `False` | If True, HTML and JSON reports will only be generated if at least one violation is found. |
141
+ | `strict_mode` | `bool` | `True`, `False` | `False` | If True, when a violation is found an AxeAccessibilityException is raised, causing a test failure. |
142
+ | `html_report_generated` | `bool` | `True`, `False` | `True` | If True, a HTML report will be generated summarising the axe-core findings. |
143
+ | `json_report_generated` | `bool` | `True`, `False` | `True` | If True, a JSON report will be generated with the full axe-core findings. |
144
+
145
+ ### Returns
146
+
147
+ This function can be used independently, but when set to a variable returns a `dict` with the axe-core results for all pages scanned (using the URL value in the list provided as the key).
148
+
149
+ ### Example usage
150
+
151
+ When using the following command: `pytest --base-url https://www.github.com`:
152
+
153
+ from utils.axe import Axe
154
+ from playwright.sync_api import Page
155
+
156
+ def test_accessibility(page: Page) -> None:
157
+ # A list of URLs to loop through
158
+ urls_to_check = [
159
+ "nhs-england-tools/playwright-python-blueprint",
160
+ "nhs-england-tools/playwright-python-blueprint/wiki"
161
+ ]
162
+
163
+ Axe.run_list(page, urls_to_check)
164
+
165
+
166
+ ## Licence
167
+
168
+ Unless stated otherwise, the codebase is released under the [MIT License](LICENCE.md). This covers both the codebase and any sample code in the documentation.
169
+
170
+ ## Acknowledgements
171
+
172
+ This package was created based on work initially designed for the
173
+ [NHS England Playwright Python Blueprint](https://github.com/nhs-england-tools/playwright-python-blueprint).
@@ -0,0 +1,7 @@
1
+ pytest_playwright_axe/__init__.py,sha256=qKmVgHi4zuSkJ4Jn_O1umfvNoi-AlOjpdaFj1IC69eY,105
2
+ pytest_playwright_axe/axe.py,sha256=m3QobCq4AX6aqh5ibxnnchpx4At0vX6AvqGL6iSlcPA,18214
3
+ pytest_playwright_axe-0.0.1b0.dist-info/licenses/LICENCE.md,sha256=01A33sc4wm4kXRR9t-vhyO_pBFJf6YrxNJENpL6Ky7I,1093
4
+ pytest_playwright_axe-0.0.1b0.dist-info/METADATA,sha256=gqrKpw1-3weam66qTLK2Yu7HdmzSeh85E4Gf-l6H25c,12507
5
+ pytest_playwright_axe-0.0.1b0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
6
+ pytest_playwright_axe-0.0.1b0.dist-info/top_level.txt,sha256=Ah5OmwisfHaedWQ-PUV3HvC8ISjwKvGQxK7JNDo5pbw,22
7
+ pytest_playwright_axe-0.0.1b0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (78.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ # MIT Licence
2
+
3
+ Copyright (c) 2025 Dave Harding.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ pytest_playwright_axe