Skip to content

invoking axe-core from python playwright¤

javascript is not my jam. id rather write python. this post is a result of these inconsistencies.

in my accessibility, i often need to test the accessibility of a web page. the javascript way to this is to run using playwright and playwright-axe together.

i couldn't find an evident way to do that. instead i peaked into the playwright-axe integration and realizes we could invoke axe through python by copying a little bit.

auditting an html file¤

  1. create a headless playwright browser
  2. load the page
  3. inject axe-core
  4. audit the page
  5. close the browser
    from pathlib import Path
async def audit(file, **config):
    import playwright.async_api
    async with playwright.async_api.async_playwright() as play:
        browser, page = await get_browser_page(play)
        await page.goto(Path(file).absolute().as_uri())
        await __import__("asyncio").sleep(3)
        await injectReadability(page)
        # await injectAxe(page)
        # data = await get_audit_data(page, **config)
        await __import__("asyncio").sleep(30)
        await browser.close()
    return data
async def injectReadability(page): 
    await page.evaluate(requests.get("https://raw.githubusercontent.com/mozilla/readability/master/Readability.js").text)
    status = await page.evaluate("""async () => {
      const readability = await import('https://cdn.skypack.dev/@mozilla/readability');
      return (new readability.Readability(document)).parse();
    }""")
        await audit("better.html")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [15], line 1
----> 1 await audit("better.html")

Cell In [11], line 7, in audit(file, **config)
      5 await page.goto(Path(file).absolute().as_uri())
      6 await __import__("asyncio").sleep(3)
----> 7 await injectReadability(page)
      8 # await injectAxe(page)
      9 # data = await get_audit_data(page, **config)
     10 await __import__("asyncio").sleep(30)

Cell In [12], line 2, in injectReadability(page)
      1 async def injectReadability(page): 
----> 2     await page.evaluate(requests.get("https://raw.githubusercontent.com/mozilla/readability/master/Readability.js").text)
      3     status = await page.evaluate("""async () => {
      4       const readability = await import('https://cdn.skypack.dev/@mozilla/readability');
      5       return (new readability.Readability(document)).parse();
      6     }""")

NameError: name 'requests' is not defined
async def get_browser_page(play, **options):
    from shlex import split
    browser = await play.chromium.launch(
        args=split('--enable-blink-features="AccessibilityObjectModel"'),
        headless=False,  channel="chrome-beta")
    return browser, await browser.new_page()

injectAxe mimics how playwright-axe loads the package. we used a cached requests object vendored from unpkg

async def injectAxe(page): 
    await page.evaluate(requests.get("https://unpkg.com/axe-core").text)
async def injectReadability(page): 
    await page.evaluate(requests.get("https://raw.githubusercontent.com/mozilla/readability/master/Readability.js").text)
    status = await page.evaluate("""var article = new Readability(document).parse();""")

get_audit_data extracts the axe test results and the accesssbility tree from the page.

async def get_audit_data(page, **config):
    from json import dumps
    return await __import__("asyncio").gather(
        page.evaluate(F"window.axe.run(window.document, {dumps(config)})"),
        page.accessibility.snapshot())

testing a page¤

we use this notebook as the test artifact by generating an html version of it with nbconvert

    import requests_cache, requests, pandas;  requests_cache.install_cache("a11y")
    from pathlib import Path
    THIS = Path("2022-10-27-axe-core-playwright-python.ipynb")
    if __name__ == "__main__":
        !jupyter nbconvert --to html $THIS
[NbConvertApp] WARNING | Config option `kernel_spec_manager_class` not recognized by `NbConvertApp`.
[NbConvertApp] Converting notebook 2022-10-27-axe-core-playwright-python.ipynb to html
[NbConvertApp] Writing 605344 bytes to 2022-10-27-axe-core-playwright-python.html

getting the accessibility audit objects¤

axe contains the axe test data and tree contains the accessibility tree.

    axe, tree = map(pandas.Series, await audit(THIS.with_suffix(".html"), runOnly="best-practice".split()))

axe violations¤

    pandas.DataFrame(axe.violations)
id impact tags description help helpUrl nodes
0 landmark-one-main moderate [cat.semantics, best-practice] Ensures the document has a main landmark Document should have one main landmark https://dequeuniversity.com/rules/axe/4.5/land... [{'any': [], 'all': [{'id': 'page-has-main', '...
1 region moderate [cat.keyboard, best-practice] Ensures all page content is contained by landm... All page content should be contained by landmarks https://dequeuniversity.com/rules/axe/4.5/regi... [{'any': [{'id': 'region', 'data': {'isIframe'...

accessibility tree¤

    pandas.DataFrame(tree.children)
role name level
0 text Loading [MathJax]/jax/output/CommonHTML/fonts/... NaN
1 heading invoking axe-core from python playwright 1.0
2 text javascript is not my jam. id rather write pyth... NaN
3 text in my accessibility, i often need to test the ... NaN
4 text i couldn't find an evident way to do that. ins... NaN
... ... ... ...
275 text . NaN
276 text children NaN
277 text ) NaN
278 heading conclusion 2.0
279 text we can run axe in python playwright and analyz... NaN

280 rows × 3 columns

conclusion¤

we can run axe in python playwright and analyze results in pandas.