Skip to content

gluing to together interactive ux with automated testing¤

generate our accessible version of table and test it against the nu validator and axe.

this is important to post because it densely demonstrates an interactive computing activity for testing accessibility backed by automated testing.

the tables in this document captured by assistive technology

this work hits on a long time issue i've been wrestling with where dataframes are a challenge from screen readers.

our nbconvert_a11y table representations¤

from nbconvert_a11y.tables import get_table, Config
from nbconvert_a11y.axe import async_axe

(df := pandas.concat(dict(zip("ab", ([
    DataFrame(numpy.random.randn(50, 3)),
    DataFrame(numpy.random.randn(50, 3))
]))), axis=1))
df = df.set_index(df.index)
(table := get_table(df, Config(group_columns=True, group_index=True), caption="randomly generated data we can test."))
randomly generated data we can test.
index 1 a b
0 1 2 0 1 2
0 0.71 0.79 -1.44 -1.59 0.86 0.82
1 -0.28 -0.12 -1.23 0.61 -0.44 -0.69
2 0.04 -0.29 -0.37 -1.57 0.52 -0.40
3 0.52 -0.17 -0.30 -0.23 -0.46 0.05
4 1.41 -1.77 -0.16 -0.04 -1.61 -1.09
5 -0.31 -0.65 -0.51 1.36 0.46 -0.62
6 2.56 0.12 1.34 -0.25 2.35 -1.68
7 -0.05 -1.48 -0.45 0.34 -0.51 -0.20
8 -0.51 -0.13 0.57 0.82 -0.62 0.36
9 0.67 -0.63 -0.52 0.48 -0.38 0.87
... ...
40 1.64 0.85 -1.87 -0.45 1.47 1.08
41 0.24 0.61 0.80 0.68 0.44 0.43
42 -0.40 -0.79 -0.49 -0.78 -0.12 0.50
43 0.99 0.15 -1.58 -0.94 -1.97 -0.77
44 -0.49 -0.89 -0.55 -0.62 -0.02 0.69
45 0.35 -1.68 -1.38 -0.21 0.68 1.73
46 1.01 -0.01 0.66 -0.56 -1.63 0.78
47 -1.79 -1.89 -0.71 -0.67 -1.21 -0.42
48 1.68 -1.34 -1.53 -1.81 0.27 -2.87
49 -1.28 -0.77 -0.76 -2.02 0.68 -1.21
min -1.79 -3.10 -3.08 -2.02 -1.97 -2.87
max 3.19 1.87 2.27 1.61 2.35 2.03

vnu checker¤

test the quality of the table against the table

validator = DataFrame(await async_axe.validate_html(
    str(table)
)).messages.series()
validator.groupby("message").count()[["type"]].pipe(get_table, caption="caption a summary of the vnu validator errors for the dataframe we generated.")
caption a summary of the vnu validator errors for the dataframe we generated.
message type
Attribute “aria-colspan” is unnecessary for elements that have attribute “colspan”. 8
Attribute “aria-rowspan” is unnecessary for elements that have attribute “rowspan”. 2
Bad value “True” for attribute “aria-hidden” on element “span”. 1
Bad value “colheader” for attribute “role” on element “th”. 38
Bad value “rowheader” for attribute “role” on element “th”. 2
Consider adding a “lang” attribute to the “html” start tag to declare the language of this document. 1
Element “head” is missing a required instance of child element “title”. 1
Element “td” is missing one or more of the following attributes: “role”. 1
Start tag seen without seeing a doctype first. Expected “<!DOCTYPE html>”. 1
Table cell spans past the end of its row group established by a “tbody” element; clipped to the end of the row group. 2
The “cell” role is unnecessary for element “td”. 132
The “role” attribute must not be used on a “td” element which has a “table” ancestor with no “role” attribute, or with a “role” attribute whose value is “table”, “grid”, or “treegrid”. 132
The “role” attribute must not be used on a “th” element which has a “table” ancestor with no “role” attribute, or with a “role” attribute whose value is “table”, “grid”, or “treegrid”. 40
The “role” attribute must not be used on a “tr” element which has a “table” ancestor with no “role” attribute, or with a “role” attribute whose value is “table”, “grid”, or “treegrid”. 27
The “rowgroup” role is unnecessary for element “tbody”. 3
The “rowgroup” role is unnecessary for element “tfoot”. 1
The “rowgroup” role is unnecessary for element “thead”. 1
The “rowheader” role is unnecessary for element “th”. 2
The “row” role is unnecessary for element “tr”. 27
The “table” role is unnecessary for element “table”. 1
min 1
max 132

axe checker¤

Series((await async_axe.validate_axe(str(table))).get("violations")).series().set_index("id").groupby("impact").pipe(
    get_table, caption="a summary of the axe errors for the table. none of them are related to our table.", config=Config(group_index=True)
)
a summary of the axe errors for the table. none of them are related to our table.
impact id impact tags description help helpUrl nodes
moderate 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.8/landmark-one-main?application=axeAPI [{'any': [], 'all': [{'id': 'page-has-main', 'data': None, 'relatedNodes': [], 'impact': 'moderate', 'message': 'Document does not have a main landmark'}], 'none': [], 'impact': 'moderate', 'html': '<html><head></head><body></body></html>', 'target': ['html'], 'failureSummary': 'Fix all of the following:\n Document does not have a main landmark'}]
page-has-heading-one moderate ['cat.semantics', 'best-practice'] Ensure that the page, or at least one of its frames contains a level-one heading Page should contain a level-one heading https://dequeuniversity.com/rules/axe/4.8/page-has-heading-one?application=axeAPI [{'any': [], 'all': [{'id': 'page-has-heading-one', 'data': None, 'relatedNodes': [], 'impact': 'moderate', 'message': 'Page must have a level-one heading'}], 'none': [], 'impact': 'moderate', 'html': '<html><head></head><body></body></html>', 'target': ['html'], 'failureSummary': 'Fix all of the following:\n Page must have a level-one heading'}]
serious document-title serious ['cat.text-alternatives', 'wcag2a', 'wcag242', 'TTv5', 'TT12.a', 'EN-301-549', 'EN-9.2.4.2', 'ACT'] Ensures each HTML document contains a non-empty <title> element Documents must have <title> element to aid in navigation https://dequeuniversity.com/rules/axe/4.8/document-title?application=axeAPI [{'any': [{'id': 'doc-has-title', 'data': None, 'relatedNodes': [], 'impact': 'serious', 'message': 'Document does not have a non-empty <title> element'}], 'all': [], 'none': [], 'impact': 'serious', 'html': '<html><head></head><body></body></html>', 'target': ['html'], 'failureSummary': 'Fix any of the following:\n Document does not have a non-empty <title> element'}]
html-has-lang serious ['cat.language', 'wcag2a', 'wcag311', 'TTv5', 'TT11.a', 'EN-301-549', 'EN-9.3.1.1', 'ACT'] Ensures every HTML document has a lang attribute <html> element must have a lang attribute https://dequeuniversity.com/rules/axe/4.8/html-has-lang?application=axeAPI [{'any': [{'id': 'has-lang', 'data': {'messageKey': 'noLang'}, 'relatedNodes': [], 'impact': 'serious', 'message': 'The <html> element does not have a lang attribute'}], 'all': [], 'none': [], 'impact': 'serious', 'html': '<html><head></head><body></body></html>', 'target': ['html'], 'failureSummary': 'Fix any of the following:\n The <html> element does not have a lang attribute'}]
min None None None None None None
max None None None None None None

this might be one of the most naturally accessible documents i've written.