Skip to content

Commit

Permalink
add generate function for testing axe
Browse files Browse the repository at this point in the history
  • Loading branch information
tonyfast committed Jul 10, 2024
1 parent f25f59a commit 7718356
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 29 deletions.
27 changes: 27 additions & 0 deletions nbconvert_a11y/axe/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# testing axe in python

the `test_axe` module provides utilities for running `axe-core` on html document in python. it is designed for asynchronous interactive use and synchronous testing with the `pytest_axe` extension.

`axe-core` is a javascript module that must be run inside of a live browser. currently, `playwright` is the only browser automation supporter, but `selenium` could be added.

<!-- can we add it to robots from jupyter after that? -->

## axe exceptions in python

each release contains a version of axe and a set of exceptions generated for that specific version.
the types are useful in accounting for expected accessibility that can be fixed later.

## playwright testing

it turns out the best ergonomics with working with axe from playwright are to append methods to the `Page` class. it has the following methods when `test_axe` is active:

`Page.axe`
: run axe and return the test results as a python dictionary

`Page.test_axe`
: run axe and return the test results as a python exception group

`Page.aom`
: an alias to the soon to be deprecated `page.accessibility.snapshot()`

we have to main synchronous and asynchronous methods for playwright
1 change: 1 addition & 0 deletions nbconvert_a11y/axe/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .async_axe import *
from ..browsers import Browser
from .axe_exceptions import *
from .ipy import load_ipython_extension, unload_ipython_extension
7 changes: 6 additions & 1 deletion nbconvert_a11y/axe/async_axe.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from json import dumps, loads
from pathlib import Path
from subprocess import PIPE, check_output
from playwright.async_api import Page
from playwright.async_api import Page, Locator, async_playwright

from nbconvert_a11y.axe.types import AxeOptions

Expand Down Expand Up @@ -58,6 +58,11 @@ async def validate_html(html: str) -> dict:
_, stderr = await process.communicate(html.encode())
return loads(stderr)

async def validate_axe(html: str, **config):
async with async_playwright() as pw:
browser = await pw.chromium.launch()
page = await browser.new_page()
return await pw_axe(page, **config)

async def pw_validate_html(page):
return await validate_html(await page.outer_html())
Expand Down
115 changes: 115 additions & 0 deletions nbconvert_a11y/axe/ipy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import atexit
from dataclasses import dataclass, field
from pathlib import Path
from traceback import print_exception
from playwright.async_api import Browser, Playwright, async_playwright
from asyncio import gather, get_event_loop

from nbconvert_a11y.axe.types import AxeConfigure, AxeOptions
from nbconvert_a11y.pytest_w3c import ValidatorViolation
from nbconvert_a11y.axe.async_axe import validate_html


def run(*args, **kwargs):
from asyncio import run

return run(*args, **kwargs)


@dataclass
class Results: ...


@dataclass
class Playwright:
axe_options: AxeOptions = field(default_factory=AxeOptions)
axe_config: AxeConfigure = field(default_factory=AxeConfigure)
pw: Playwright = None
browser: Browser = None

def enter(self):
if self.pw is None:
self.pw = async_playwright()

self.ctx = run(self.pw.__aenter__())
atexit.register(self.exit)
if self.browser is None:
self.browser = run(self.ctx.chromium.launch())
return self

def exit(self):
run(self.ctx.__aexit__())

def new_page(self, content):
page = run(self.browser.new_page())
if isinstance(content, Path):
run(page.goto(content))
elif isinstance(content, str):
if content.lstrip().startswith("<"):
run(page.set_content(content))
elif "://" in content:
run(page.goto(content))
return page

async def test_axe_page(self, page):
from . import pw_test_axe

return await pw_test_axe(page, '"body"', **self.axe_options.dict())

async def test_vnu_page(self, page):
from . import pw_validate_html

return ValidatorViolation.from_violations(await pw_validate_html(page))

def test_axe_html(self, html):
return run(self.test_axe_page(self.new_page(html)))

def test_vnu_html(self, html):
return ValidatorViolation.from_violations(run(validate_html(html)))

def axe_magic_cell(self, line, cell):
from IPython.display import HTML, display

page = self.new_page(cell)
display(HTML(cell))
axe = self.test_axe_page(page)
if isinstance(axe, Exception):
print_exception(axe)

def html_magic_cell(self, line, cell):
axe = vnu = True
from IPython.display import HTML, display

display(HTML(cell))
page = self.new_page(cell)
for x in run(gather(self.test_axe_page(page), self.test_vnu_page(page))):
print(x)

if isinstance(x, (Exception, ExceptionGroup)):
print_exception(x)

def vnu_magic_cell(self, line, cell):
vnu = self.test_vnu_html(cell)
if isinstance(vnu, Exception):
print_exception(vnu)

def line_magic(self, line, cell=None):
pass


def load_ipython_extension(shell):
from traitlets import Instance

if not shell.has_trait("pw"):
shell.add_traits(pw=Instance(Playwright, args=()))
shell.register_magic_function(shell.pw.axe_magic_cell, "cell", "axe")
shell.register_magic_function(shell.pw.vnu_magic_cell, "cell", "vnu")
shell.register_magic_function(shell.pw.html_magic_cell, "cell", "html")
from nest_asyncio import apply

apply()
shell.pw.enter()


def unload_ipython_extension(shell):
pass
74 changes: 74 additions & 0 deletions nbconvert_a11y/data/roles.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"grid": {
"table": "grid",
"thead": "rowgroup",
"tbody": "rowgroup",
"tfoot": "rowgroup",
"tr": "row",
"th": {
"row": "rowheader",
"col": "colheader"
},
"td": "gridcell"
},
"treegrid": {
"table": "grid",
"thead": "rowgroup",
"tbody": "rowgroup",
"tfoot": "rowgroup",
"tr": "row",
"th": {
"row": "rowheader",
"col": "colheader"
},
"td": "gridcell"
},
"table": {
"table": "table",
"thead": "rowgroup",
"tbody": "rowgroup",
"tfoot": "rowgroup",
"tr": "row",
"th": {
"row": "rowheader",
"col": "colheader"
},
"td": "cell"
},
"list": {
"table": "list",
"thead": "group",
"tbody": "group",
"tfoot": "group",
"tr": "listitem",
"th": {
"row": "none",
"col": "none"
},
"td": "none"
},
"region": {
"table": "none",
"thead": "none",
"tbody": "none",
"tfoot": "none",
"tr": "region",
"th": {
"row": "none",
"col": "none"
},
"td": "none"
},
"none": {
"table": "none",
"thead": "none",
"tbody": "none",
"tfoot": "none",
"tr": "none",
"th": {
"row": "none",
"col": "none"
},
"td": "none"
}
}
25 changes: 16 additions & 9 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,23 @@ platforms = ["linux-64", "osx-64", "win-64", "osx-arm64"]
depends-on = ["pip", "install-axe", "pw-install-ff"]
inputs = [
"pyproject.toml",
"{tests,nbconvert-a11y}/*.py",
"{tests,nbconvert-a11y}/**/*.py",
"{tests,nbconvert_a11y}/*.py",
"{tests,nbconvert_a11y}/**/*.py",
]
cmd = """pip install -e. --no-deps --ignore-installed --disable-pip-version-check
&& PLAYWRIGHT_BROWSERS_PATH=.pixi/.local-browsers pytest --browser firefox"""
cmd = """PLAYWRIGHT_BROWSERS_PATH=.pixi/.local-browsers pytest --browser firefox"""


[feature.test-axe.tasks.install-axe]
outputs = ["node_modules/axe-core"]
cmd = "npm install axe-core"
inputs = ["package.json"]
outputs = ["node_modules/axe-core", "package-lock.json"]
cmd = "npm install"

[feature.test-axe.tasks.vendor-axe]
depends-on = ["install-axe"]
inputs = ["package-lock.json"]
outputs = ["nbconvert_a11y/axe/axe-core"]
cmd = "cp node_modules/axe-core nbconvert_a11y/axe/axe-core/"


[feature.test-axe.tasks.pw-install-ff]
outputs = [".pixi/.local-browsers/firefox-*"]
Expand All @@ -43,8 +50,8 @@ mkdocstrings = ">=0.25.1,<0.26"
[feature.docs.tasks.build]
inputs = [
"mkdocs.yml",
"{tests,nbconvert-a11y}/*.{ipynb,md,py}",
"{tests,nbconvert-a11y}/**/*.{ipynb,md,py}",
"{tests,nbconvert_a11y}/*.{ipynb,md,py}",
"{tests,nbconvert_a11y}/**/*.{ipynb,md,py}",
]
outputs = ["site"]
cmd = "mkdocs build -v"
Expand Down Expand Up @@ -89,7 +96,7 @@ docs = { features = ["docs"], solve-group = "default" }


[tasks.pip]
inputs = ["pyproject.toml"]
inputs = ["pyproject.toml", "nbconvert_a11y/templates"]
outputs = ["build/pip-freeze/*.txt"]
cmd = """
python -m pip install -vv --no-deps --ignore-installed --disable-pip-version-check
Expand Down
25 changes: 6 additions & 19 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

__metadata:
version: 6
cacheKey: 8

"@deathbeds/nbconvert-a11y@workspace:.":
version: 0.0.0-use.local
resolution: "@deathbeds/nbconvert-a11y@workspace:."
dependencies:
axe-core: ^4.8.2
languageName: unknown
linkType: soft

"axe-core@npm:^4.8.2":
version: 4.8.2
resolution: "axe-core@npm:4.8.2"
checksum: 8c19f507dabfcb8514e4280c7fc66e85143be303ddb57ec9f119338021228dc9b80560993938003837bda415fde7c07bba3a96560008ffa5f4145a248ed8f5fe
languageName: node
linkType: hard
axe-core@^4.8.2:
version "4.9.1"
resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.9.1.tgz"
integrity sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==

0 comments on commit 7718356

Please sign in to comment.