From d549bdba45c5ff83bacd0a249f71d15e6d406690 Mon Sep 17 00:00:00 2001 From: Olena Date: Sat, 19 Jul 2025 18:59:57 +0300 Subject: [PATCH 1/4] Modernize Axe class implementation --- axe_selenium_python/axe.py | 74 ++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/axe_selenium_python/axe.py b/axe_selenium_python/axe.py index 9ec5616..a643fb6 100755 --- a/axe_selenium_python/axe.py +++ b/axe_selenium_python/axe.py @@ -2,28 +2,52 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +from __future__ import annotations + import json -import os +import logging +from pathlib import Path -_DEFAULT_SCRIPT = os.path.join( - os.path.dirname(__file__), "node_modules", "axe-core", "axe.min.js" -) +logger = logging.getLogger(__name__) +_DEFAULT_SCRIPT = Path(__file__).parent / "node_modules" / "axe-core" / "axe.min.js" -class Axe: - def __init__(self, selenium, script_url=_DEFAULT_SCRIPT): - self.script_url = script_url - self.selenium = selenium - def inject(self): +class Axe: + def __init__(self, selenium_driver, script_url: str | Path = _DEFAULT_SCRIPT) -> None: """ - Recursively inject aXe into all iframes and the top level document. - - :param script_url: location of the axe-core script. - :type script_url: string + Initialize Axe instance. + + Args: + selenium_driver: Selenium WebDriver instance + script_url: Path to axe-core JavaScript file + + Raises: + FileNotFoundError: If script file doesn't exist """ - with open(self.script_url, encoding="utf8") as f: - self.selenium.execute_script(f.read()) + self.script_url = Path(script_url) + self.selenium = selenium_driver + self._is_injected = False + + if not self.script_url.exists(): + msg = f"Axe script not found at: {self.script_url}" + raise FileNotFoundError(msg) + + def inject(self) -> None: + """ + Inject axe-core script into the current page and all iframes. + + Raises: + RuntimeError: If injection fails + """ + try: + script_content = self.script_url.read_text(encoding="utf-8") + self.selenium.execute_script(script_content) + self._is_injected = True + logger.info("Axe-core successfully injected into page") + except Exception as e: + msg = f"Failed to inject axe-core script: {e}" + raise RuntimeError(msg) from e def run(self, context=None, options=None): """ @@ -96,19 +120,15 @@ def write_results(self, data, name=None): """ Write JSON to file with the specified name. - :param name: Path to the file to be written to. If no path is passed - a new JSON file "results.json" will be created in the - current working directory. - :param output: JSON object. + param name: Path to the file to be written to. If no path is passed + a new JSON file "results.json" will be created in the + current working directory. + :param data: JSON object. """ - if name: - filepath = os.path.abspath(name) + filepath = Path(name).resolve() else: - filepath = os.path.join(os.path.getcwd(), "results.json") + filepath = Path.cwd() / "results.json" - with open(filepath, "w", encoding="utf8") as f: - try: - f.write(json.dumps(data, indent=4)) - except NameError: - f.write(json.dumps(data, indent=4)) + with open(filepath, "w", encoding="utf-8") as f: + json.dump(data, f, indent=4) From a0aa27e1e5681c309fff55ec1efaa1148794280a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 16:13:09 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- axe_selenium_python/axe.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/axe_selenium_python/axe.py b/axe_selenium_python/axe.py index a643fb6..a8f47fd 100755 --- a/axe_selenium_python/axe.py +++ b/axe_selenium_python/axe.py @@ -14,21 +14,23 @@ class Axe: - def __init__(self, selenium_driver, script_url: str | Path = _DEFAULT_SCRIPT) -> None: + def __init__( + self, selenium_driver, script_url: str | Path = _DEFAULT_SCRIPT + ) -> None: """ Initialize Axe instance. - + Args: selenium_driver: Selenium WebDriver instance script_url: Path to axe-core JavaScript file - + Raises: FileNotFoundError: If script file doesn't exist """ self.script_url = Path(script_url) self.selenium = selenium_driver self._is_injected = False - + if not self.script_url.exists(): msg = f"Axe script not found at: {self.script_url}" raise FileNotFoundError(msg) @@ -36,12 +38,12 @@ def __init__(self, selenium_driver, script_url: str | Path = _DEFAULT_SCRIPT) -> def inject(self) -> None: """ Inject axe-core script into the current page and all iframes. - + Raises: RuntimeError: If injection fails """ try: - script_content = self.script_url.read_text(encoding="utf-8") + script_content = self.script_url.read_text(encoding="utf-8") self.selenium.execute_script(script_content) self._is_injected = True logger.info("Axe-core successfully injected into page") From b7a55f27a1d5211e3daac04adc944d95172a41ff Mon Sep 17 00:00:00 2001 From: Olena Date: Sun, 20 Jul 2025 12:30:00 +0300 Subject: [PATCH 3/4] Fix ruff error because of pre-commit failing --- axe_selenium_python/axe.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/axe_selenium_python/axe.py b/axe_selenium_python/axe.py index a8f47fd..9e40ad4 100755 --- a/axe_selenium_python/axe.py +++ b/axe_selenium_python/axe.py @@ -127,10 +127,7 @@ def write_results(self, data, name=None): current working directory. :param data: JSON object. """ - if name: - filepath = Path(name).resolve() - else: - filepath = Path.cwd() / "results.json" + filepath = Path(name).resolve() if name else Path.cwd() / "results.json" with open(filepath, "w", encoding="utf-8") as f: json.dump(data, f, indent=4) From 07b09a69381afbd4ba587e107facbff072e3989b Mon Sep 17 00:00:00 2001 From: Olena Date: Sun, 20 Jul 2025 15:37:11 +0300 Subject: [PATCH 4/4] Update the script --- axe_selenium_python/axe.py | 131 ++++++++++++++++++++++--------------- 1 file changed, 78 insertions(+), 53 deletions(-) diff --git a/axe_selenium_python/axe.py b/axe_selenium_python/axe.py index 9e40ad4..74c92f1 100755 --- a/axe_selenium_python/axe.py +++ b/axe_selenium_python/axe.py @@ -55,68 +55,88 @@ def run(self, context=None, options=None): """ Run axe against the current page. - :param context: which page part(s) to analyze and/or what to exclude. - :param options: dictionary of aXe options. + Args: + context: which page part(s) to analyze and/or what to exclude. + options: dictionary of aXe options. + + Raises: + RuntimeError: If axe is not injected or analysis fails """ - template = ( - "var callback = arguments[arguments.length - 1];" - + "axe.run(%s).then(results => callback(results))" - ) - args = "" - # If context parameter is passed, add to args + if not self._is_injected: + msg = "Axe not injected. Call inject() first." + raise RuntimeError(msg) + + args = [] if context is not None: - args += "%r" % context - # Add comma delimiter only if both parameters are passed - if context is not None and options is not None: - args += "," - # If options parameter is passed, add to args + args.append(json.dumps(context)) if options is not None: - args += "%s" % options + if context is None: + args.append("document") + args.append(json.dumps(options)) + + js_args = ", ".join(args) if args else "" + + command = f""" + var callback = arguments[arguments.length - 1]; + axe.run({js_args}) + .then(results => callback(results)) + .catch(error => callback({{error: error.message}})); + """ + + try: + response = self.selenium.execute_async_script(command) + + if "error" in response: + msg = f"Axe analysis failed: {response['error']}" + raise RuntimeError(msg) - command = template % args - response = self.selenium.execute_async_script(command) - return response + return response + except Exception as e: + msg = f"Failed to execute axe analysis: {e}" + raise RuntimeError(msg) from e def report(self, violations): """ Return readable report of accessibility violations found. - :param violations: Dictionary of violations. - :type violations: dict - :return report: Readable report of violations. - :rtype: string + Args: + violations: List of violations from axe results. + + Returns: + Formatted string report. """ - string = "" - string += "Found " + str(len(violations)) + " accessibility violations:" - for violation in violations: - string += ( - "\n\n\nRule Violated:\n" - + violation["id"] - + " - " - + violation["description"] - + "\n\tURL: " - + violation["helpUrl"] - + "\n\tImpact Level: " - + violation["impact"] - + "\n\tTags:" - ) - for tag in violation["tags"]: - string += " " + tag - string += "\n\tElements Affected:" - i = 1 - for node in violation["nodes"]: - for target in node["target"]: - string += "\n\t" + str(i) + ") Target: " + target - i += 1 - for item in node["all"]: - string += "\n\t\t" + item["message"] - for item in node["any"]: - string += "\n\t\t" + item["message"] - for item in node["none"]: - string += "\n\t\t" + item["message"] - string += "\n\n\n" - return string + if not violations: + return "No accessibility violations found!" + + lines = [f"Found {len(violations)} accessibility violations:"] + + for i, violation in enumerate(violations, 1): + lines.append(f"\n{i}. Rule: {violation['id']} - {violation['description']}") + lines.append(f" Impact Level: {violation['impact']}") + lines.append(f" Help URL: {violation['helpUrl']}") + lines.append(f" Tags: {', '.join(violation['tags'])}") + lines.append(" Elements Affected:") + + for node_idx, node in enumerate(violation["nodes"], 1): + lines.extend( + f" {node_idx}) Target: {target}" for target in node["target"] + ) + + for message_list in [ + node.get("all", []), + node.get("any", []), + node.get("none", []), + ]: + lines.extend( + f" - {item['message']}" + for item in message_list + if item.get("message") + ) + + lines.append("") + + return "\n".join(lines) def write_results(self, data, name=None): """ @@ -129,5 +149,10 @@ def write_results(self, data, name=None): """ filepath = Path(name).resolve() if name else Path.cwd() / "results.json" - with open(filepath, "w", encoding="utf-8") as f: - json.dump(data, f, indent=4) + try: + with open(filepath, "w", encoding="utf-8") as f: + json.dump(data, f, indent=4) + logger.info("Results saved to: %s", filepath) + except Exception as e: + msg = f"Failed to save results to {filepath}: {e}" + raise OSError(msg) from e