Repository navigation
feat: a11y testing setup #3177
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: a11y testing setup #3177
Changes from all commits
35b14f2
a974076
1fd7af8
9dee776
177e755
1de7a16
75e158a
a6bafae
66f1a48
c92db4b
463e237
7cf0798
137f4d4
8fcb355
2c218d8
f331a87
e9ff65d
2a5856f
640faa9
8941dc5
e7dd7ba
f4a2e18
71930dd
72eaa23
8e6108f
b02e584
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| name: Accessibility | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| types: [opened, synchronize, reopened] | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| env: | ||
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | ||
|
|
||
| concurrency: | ||
| group: a11y-${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| browser: | ||
| name: Accessibility - browser | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| # Match the local Docker environment and the installed Playwright version. | ||
| container: | ||
| image: mcr.microsoft.com/playwright:v1.60.0-noble | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1 | ||
| with: | ||
| node-version-file: ".node-version" | ||
| cache: true | ||
|
|
||
| - name: Install dependencies | ||
| run: vp install --frozen-lockfile | ||
|
|
||
| # Already inside Docker; call Vitest directly to avoid nested Docker | ||
| # and task caching. Source aliases mean no package build is needed. | ||
| - name: Run axe, keyboard, ARIA and visual checks | ||
| working-directory: tests | ||
| run: pnpm exec vp test --config a11y/vite.config.ts --run | ||
|
|
||
| - name: Upload browser report and diagnostics | ||
| if: ${{ !cancelled() }} | ||
| uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 | ||
| with: | ||
| name: a11y-browser | ||
| path: | | ||
| tests/playwright-report/a11y/browser/ | ||
| tests/test-results/a11y/ | ||
| retention-days: 7 |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,61 @@ | ||||||
| This document covers the main areas that are most relevant to the editor regarding accessibility. These areas are derived from: | ||||||
|
|
||||||
| - The list of issues found by the Docs team in an accessibility audit (marked with the a11y tag on GitHub). | ||||||
| - Scanning through the WCAG 2.0–2.2 guidelines and identifying likely problem areas. | ||||||
| - Long-standing UX issues that relate to accessibility but haven't yet been formally documented. | ||||||
|
|
||||||
| # Legibility | ||||||
|
|
||||||
| Sight-impaired users need to be able to read text in the editor body as well as in interactive elements like menus and toolbars. The following concerns are most relevant to BlockNote for accessibility. | ||||||
|
|
||||||
| ## Contrast | ||||||
|
|
||||||
| WCAG sets guidelines for [minimum](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum) and [enhanced](https://www.w3.org/WAI/WCAG22/Understanding/contrast-enhanced) contrast that text has to have against its background to be legible. | ||||||
|
|
||||||
| ## Text Spacing & Resizing | ||||||
|
|
||||||
| Vision-impaired users have tools available, which make text on a page easier to read by applying additional CSS styles. There are WCAG guidelines that state the upper limits of how the text's [size](https://www.w3.org/WAI/WCAG21/Understanding/resize-text.html) and [spacing](https://www.w3.org/WAI/WCAG21/Understanding/text-spacing.html) may be increased, without causing content to clip. | ||||||
|
|
||||||
| # Keyboard & Focus Handling | ||||||
|
|
||||||
| Users with impaired motor control will use a keyboard or other device that relies solely on buttons to navigate a page. When it comes to BlockNote, there are a few specific cases | ||||||
| that we need to consider. | ||||||
|
|
||||||
| ## Editor Focus | ||||||
|
|
||||||
| Currently, the editor can be pretty annoying when moving focus around a page. Once focus lands on the editor, it immediately snaps to the editor's selection. This is fine for when the user wants to focus the editor, but less pretty annoying when moving around the page and having to focus the editor due to tab order. Since it has its own tab handling, trying to move focus outside it can be frustrating. | ||||||
|
|
||||||
| ## Block Selection | ||||||
|
|
||||||
| There is a distinction between selecting all content within a block and selecting the entire block, especially for things like backspace handling. This distinction is currently unclear to the user, selected blocks are not always clearly highlighted, and it's only possible to select an entire block with text content using Cmd+Click. | ||||||
|
|
||||||
| ## Interactive Block Elements | ||||||
|
|
||||||
| Some blocks contain interactive elements within them, e.g., checkboxes in check list items. These are not accessible while the editor is focused and are otherwise, but their tab order is not at all intuitive. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. State the interactive-block behavior explicitly. The sentence ends with “and are otherwise,” so it does not state whether these elements are accessible outside the editor. This ambiguity can lead to incorrect test coverage. Proposed wording
Committable suggestion
|
||||||
|
|
||||||
| ## UI Elements | ||||||
|
|
||||||
| Some work has been put into making the formatting and link toolbars accessible. Suggestion menus are also keyboard-accessible. For other UI elements, keyboard navigation is either completely broken (e.g., side menu) or not considered (e.g., file panel). | ||||||
|
|
||||||
| ## Hover Controls | ||||||
|
|
||||||
| Any controls that are exposed by hovering an element with the mouse cursor or focusing it must be accessible with the keyboard, as per this [WCAG guideline](https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html). | ||||||
|
|
||||||
| # Screen Reader Announcements | ||||||
|
|
||||||
| When screen readers announce content within the editor, they must also announce additional semantic information that would be useful when viewing and editing a document. | ||||||
|
|
||||||
| ## Screen Reader Focus | ||||||
|
|
||||||
| Screen readers have their own keyboard navigation and target handling separate from the browser. This means that while an element is focused in the browser, the user can still move the screen reader to target other elements on the page. However, moving around the editor using the keyboard moves the screen reader target with it, so generally everything should work out-of-the-box in our case. | ||||||
|
|
||||||
| ## Markup | ||||||
|
|
||||||
| The editor contains a lot of markup that is currently not announced by screen readers but is required for the user to understand the document structure. This is generally fixed using ARIA attributes and includes things like: | ||||||
|
|
||||||
| - Block type | ||||||
| - Block nesting | ||||||
| - Block children | ||||||
| - Block colors | ||||||
| - Table row/column information | ||||||
| - ... | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| This document covers what testing infrastructure must be in place to ensure that BlockNote has a good level of accessibility. This means coverage of all areas discussed in Outline of Concerns. | ||
|
|
||
| # Legibility | ||
|
|
||
| ## Contrast | ||
|
|
||
| Contrast requirements can be checked using static DOM analysis tools like [axe-core](https://github.com/dequelabs/axe-core/tree/develop). A rule like [color-contrast](https://dequeuniversity.com/rules/axe/4.13/color-contrast?application=RuleDescription) calculates contrast between the CSS text and background colors for each element. It cannot check contrast for image, gradient, or other non-single-color backgrounds, but these aren't used in BlockNote, so it doesn't matter in our case. Visual regression testing is not necessary for checking contrast. | ||
|
|
||
| ## Text Spacing & Resizing | ||
|
|
||
| The WCAG guidelines require that text spacing & sizing must be modifiable, and this is done in accessibility tools by overwriting CSS. Testing this is twofold. First, static DOM analysis using [axe-core](https://github.com/dequelabs/axe-core/tree/develop) ensures that no inline styles prevent CSS rules from applying. Visual regression testing is then used to capture screenshots and verify that content is not clipped, which is already part of our existing test infrastructure. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Do not claim that axe-core validates text-spacing and resize overrides. Axe-core checks static accessibility rules. It does not execute WCAG 1.4.4 or 1.4.12 CSS overrides, or prove that content remains usable after those overrides. Describe axe-core as partial static coverage and keep a direct browser check for the resulting layout. 🤖 Prompt for AI Agents |
||
|
|
||
| # Keyboard & Focus Handling | ||
|
|
||
| We already have keyboard handling as part of our existing tests. This is done in two ways: | ||
|
|
||
| 1. Simulating key presses using synthetic events or directly calling their related handlers in a jsdom environment. | ||
| 2. Driving the keyboard to dispatch real events in a real browser environment. | ||
|
|
||
| For accessibility purposes, we should be using a real browser environment. Simulated key presses in a jsdom environment are not guaranteed to behave the same as a real browser, while calling the handlers is not functionally different to having a unit test for that handler. We should refactor our existing jsdom tests to either unit or browser tests. | ||
|
|
||
| The scope of keyboard handling should include: | ||
|
|
||
| - Keyboard navigation through a page with interactive elements, including a BlockNote editor. | ||
| - Selections within an editor at different levels: | ||
| - Selecting content within blocks. | ||
| - Selecting entire blocks. | ||
| - Selecting the entire editor. | ||
| - Focusing all interactive elements within default blocks, including media controls. | ||
| - Focusing/triggering all interactive elements in the BlockNote UI, including those gated behind hovering/focusing another element. | ||
|
|
||
| Visual regression testing should also be used to ensure the focused elements are visually distinct, e.g., using a focus ring. | ||
|
|
||
| # Screen Reader Announcements | ||
|
|
||
| Static DOM analysis tools like [axe-core](https://github.com/dequelabs/axe-core/tree/develop) can catch a large chunk of missing screen reader announcements through the presence of things like ARIA attributes. However, they are geared more towards static websites, and we are in a fairly unique position where the user is editing content as well as viewing it, and the content itself has significantly more markup than the average website. For example, our schema allows for block indentation. It's important that the indentation level of a block is communicated by a screen reader, but it's not something that static DOM analysis tools can catch. | ||
|
|
||
| Therefore, we should also incorporate automated screen reader testing, which lets you capture the output of a screen reader as a page is being navigated. This lets us ensure that BlockNote-specific semantics are captured by a screen reader. While screen readers have their own keyboard navigation and targeting, we don't need to test this. When navigating through the editor using a keyboard, the screen reader target will move with the selection. Advanced screen reader navigation, like [VoiceOver's rotor](https://support.apple.com/en-euro/guide/voiceover/mchlp2719/mac), is reliant on proper DOM element semantics and ARIA attributes, so it's tested implicitly by static DOM analysis tools. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Do not treat screen-reader navigation as implicit axe coverage. Axe-core does not validate VoiceOver or NVDA rotor order, virtual-cursor movement, or spoken output. Require dedicated GuidePup or manual coverage for these behaviors, or mark them explicitly as out of scope. 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,53 @@ | ||||||
| This document goes over the tooling we need based onTesting Requirements and discusses how accessibility tests should be organized. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Insert the missing space in the opening sentence.
- This document goes over the tooling we need based onTesting Requirements and discusses how accessibility tests should be organized.
+ This document goes over the tooling we need based on Testing Requirements and discusses how accessibility tests should be organized.Committable suggestion
|
||||||
|
|
||||||
| # Tooling Overview | ||||||
|
|
||||||
| This section goes over all the relevant tooling for accessibility testing, both manual and automated. | ||||||
|
|
||||||
| ## Linting | ||||||
|
|
||||||
| [oxlint](https://oxc.rs/docs/guide/usage/linter.html) includes a ruleset for accessibility linting - [jsx-a11y](https://oxc.rs/docs/guide/usage/linter/rules.html?sort=source&dir=asc&scope=jsx_a11y). This is useful for catching any obvious issues like missing labels and positive tab indices before running any automated tests. | ||||||
|
|
||||||
| ## Static DOM Analysis | ||||||
|
|
||||||
| [axe-core](https://github.com/dequelabs/axe-core) is the gold standard for basic accessibility testing. It scans the DOM of a page and checks for issues similar issues to the aforementioned linter. The draw is that it has a [browser extension](https://chromewebstore.google.com/detail/axe-devtools-web-accessib/lhdoppojpmngadmnindnejefpokejbdd), and more importantly, has a [Playwright integration](https://playwright.dev/docs/accessibility-testing) which makes it much easier to slot in to our existing testing infrastructure, and scan different UI menus/toolbars with it. | ||||||
|
|
||||||
| ## Visual Regression Testing | ||||||
|
|
||||||
| We can already do visual regression testing using Playwright. | ||||||
|
|
||||||
| ## Screen Reader Automated Testing | ||||||
|
|
||||||
| This is something that has only come around in recent years, but it's pretty self explanatory. You can programmatically control a screen reader and transcribe its output into snapshots that you compare other test runs against. | ||||||
|
|
||||||
| In our case, we mostly just care about the transcriptions as the screen reader will follow the selection when navigating the editor using a keyboard. | ||||||
|
|
||||||
| The utility of these tests really is in looking at a transcription, comparing it to the editor state and seeing if there's any useful information that isn't being conveyed, so we can make further improvements. The "north star" is that the transcription of a screen reader provides all the necessary information for someone to be able to recreate the document, as well as keyboard inputs made by the user, without any errors. | ||||||
|
|
||||||
| [GuidePup](https://www.guidepup.dev/) is a library for automated screen reader testing that fits quite well into our existing stack as it integrates with Playwright. It supports [VoiceOver](https://support.apple.com/en-euro/guide/voiceover/welcome/mac) on macOS, [NVDA](https://www.nvaccess.org/) on Windows, and a virtual screen reader for use outside a browser environment. Other popular solutions include [BrowserStack](https://www.browserstack.com/docs/app-accessibility/screen-reader-automation) and [Assistiv Labs](https://assistivlabs.com/articles/automating-screen-readers-for-accessibility-testing), but these are entire platforms that we probably don't want to deal with as part of our CI. The [AT Driver](https://github.com/w3c/at-driver) is also relevant here, but is still WIP. | ||||||
|
|
||||||
| ## Built-In Tools | ||||||
|
|
||||||
| Chrome has an accessibility tree viewer which is quite useful for getting item ordering with tab & screen reader navigation. | ||||||
|
|
||||||
| [VoiceOver](https://support.apple.com/en-euro/guide/voiceover/welcome/mac) is the built-in macOS/iOS screen reader and is helpful for manual testing. It's useful to get comfortable using it to put yourself in the user's shoes and spot issues. Windows has [NVDA](https://www.nvaccess.org/) and Android has [TalkBack](https://support.google.com/accessibility/android/answer/6283677?hl=en) as equivalents, but I haven't tried them yet. | ||||||
|
|
||||||
| # Automated Testing Organization | ||||||
|
|
||||||
| Based on the need 3 types of automated tests to cover accessibility: | ||||||
|
|
||||||
| 1. Static DOM analysis | ||||||
| 2. Visual regression snapshotting | ||||||
| 3. Screen reader | ||||||
|
|
||||||
| TODO | ||||||
|
|
||||||
| ## Out of Scope | ||||||
|
|
||||||
| ### Drag & Drop | ||||||
|
|
||||||
| There is a WCAG [guideline on drag & drop](https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html) which states that actions performed by drag & drop must have an alternative way of triggering them using regular clicks. We do technically have a way of doing this with keyboard shortcuts to move blocks up/down, but this doesn't fit the guideline as it uses keyboard shortcuts, not clicks. I'm not sure yet what the best UX pattern to solve this issue would be yet and think this requires a separate look. | ||||||
|
|
||||||
| ### Announcements | ||||||
|
|
||||||
| TODO | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { expect, test } from "vite-plus/test"; | ||
|
matthewlipski marked this conversation as resolved.
Resolved
|
||
| import { page } from "vite-plus/test/browser"; | ||
| import { | ||
| checkAriaSnapshot, | ||
| openEditor, | ||
| pressKey, | ||
| } from "@root/tests/a11y/helpers.js"; | ||
|
|
||
| // Toggle headings represent interactive blocks in this accessibility vertical slice. | ||
| // Keyboard access to toggle controls from within an editable block is not implemented. | ||
| test.skip("editable toggle heading: toggle is keyboard accessible while the editor has focus", async () => { | ||
| // TODO: The keyboard interaction for controls within an editable editor is TBD. | ||
| // Reach the toggle while the editor has focus, expand/collapse its children, | ||
| // and return to editing without changing the document content. | ||
| // In editable mode, skip the toggle in page tab navigation outside the editor; | ||
| // in read-only mode it must remain a regular tab stop (tested below). | ||
| // Capture its focus indicator and announcements of its name, role and expanded state. | ||
| // A direct toggle shortcut alone would not require a full browser test; | ||
| // this placeholder covers the eventual keyboard focus/navigation behavior. | ||
| }); | ||
|
|
||
| test("read-only toggle heading: Tab reaches the toggle and Enter expands/collapses it", async () => { | ||
| const id = "read-only-toggle-heading"; | ||
| // Reset only this fixture's persisted expansion state before each run. | ||
| localStorage.removeItem(`toggle-${id}`); | ||
| await openEditor( | ||
| [ | ||
| { | ||
| id, | ||
| type: "heading", | ||
| props: { level: 2, isToggleable: true }, | ||
| content: "Details", | ||
| children: [{ type: "paragraph", content: "Nested read-only content" }], | ||
| }, | ||
| ], | ||
| { editable: false }, | ||
| ); | ||
| const toggle = page.getByRole("button", { | ||
| name: "Expand or collapse", | ||
| exact: true, | ||
| }); | ||
| const content = page.getByText("Nested read-only content", { exact: true }); | ||
| await expect.element(content).not.toBeVisible(); | ||
| await expect.element(toggle).toHaveAttribute("aria-expanded", "false"); | ||
| await checkAriaSnapshot(page.getByRole("textbox"), "collapsed"); | ||
|
|
||
| // Start at Before editor and reach the toggle through the page's tab order. | ||
| // Do not focus the button directly: that would hide a keyboard trap. | ||
| for ( | ||
| let step = 0; | ||
| step < 3 && document.activeElement !== toggle.element(); | ||
| step++ | ||
| ) { | ||
| await pressKey("Tab"); | ||
| } | ||
| await expect.element(toggle).toHaveFocus(); | ||
| await pressKey("Enter"); | ||
| await expect.element(content).toBeVisible(); | ||
| await expect.element(toggle).toHaveAttribute("aria-expanded", "true"); | ||
| await checkAriaSnapshot(page.getByRole("textbox"), "expanded"); | ||
| await expect.element(toggle).toHaveFocus(); | ||
| await pressKey("Enter"); | ||
| await expect.element(content).not.toBeVisible(); | ||
| await expect.element(toggle).toHaveAttribute("aria-expanded", "false"); | ||
| await checkAriaSnapshot(page.getByRole("textbox"), "collapsed again"); | ||
| await expect.element(toggle).toHaveFocus(); | ||
| await pressKey("Tab"); | ||
| await expect | ||
| .element(page.getByRole("button", { name: "After editor", exact: true })) | ||
| .toHaveFocus(); | ||
| // TODO: Capture its focus indicator and screen-reader name/expanded state. | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html | ||
|
|
||
| exports[`read-only toggle heading: Tab reaches the toggle and Enter expands/collapses it > collapsed 1`] = ` | ||
| "- textbox: | ||
| - button "Expand or collapse" | ||
| - heading "Details" [level=2]" | ||
| `; | ||
|
|
||
| exports[`read-only toggle heading: Tab reaches the toggle and Enter expands/collapses it > collapsed again 1`] = ` | ||
| "- textbox: | ||
| - button "Expand or collapse" | ||
| - heading "Details" [level=2]" | ||
| `; | ||
|
|
||
| exports[`read-only toggle heading: Tab reaches the toggle and Enter expands/collapses it > expanded 1`] = ` | ||
| "- textbox: | ||
| - button "Expand or collapse" [expanded] | ||
| - heading "Details" [level=2] | ||
| - paragraph: Nested read-only content" | ||
| `; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's make sure this .md's are not merged, because afaik they're 1-off studies for this workstream. Anything in the repo should be long-lived
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, marked in the description as temporary. Just have to remember to remove them later, but still useful atm mainly so you can point agents to them.