Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
35b14f2
Initial a11y testing setup
matthewlipski Oct 8, 2026
a974076
Migrated from Playwright to Vitest Browser Mode
matthewlipski Oct 12, 2026
1fd7af8
Small refactor
matthewlipski Oct 12, 2026
9dee776
Moved axe scans to separate test files
matthewlipski Oct 12, 2026
177e755
Added read-only tests and moved interactive block tests vertical slic…
matthewlipski Oct 12, 2026
1de7a16
Removed `crypto.randomUUID` calls and added lint rule to disallow `ge…
matthewlipski Oct 13, 2026
75e158a
Made `saveTranscript` throw error when `screenReader` is not defined.
matthewlipski Oct 13, 2026
a6bafae
Changed animation handling for axe scans
matthewlipski Oct 13, 2026
66f1a48
Rename `spec` -> `test`
matthewlipski Oct 13, 2026
c92db4b
Added `@root` shorthand
matthewlipski Oct 13, 2026
463e237
Cleanup
matthewlipski Oct 13, 2026
7cf0798
Implemented remaining PR feedback
matthewlipski Oct 13, 2026
137f4d4
Small axe changes
matthewlipski Oct 14, 2026
8fcb355
Reworked axe scan document fixtures to cover much more issues (contra…
matthewlipski Oct 15, 2026
2c218d8
Merged main
matthewlipski Oct 19, 2026
f331a87
Skipped failing tests
matthewlipski Oct 22, 2026
e9ff65d
Updated GuidePup
matthewlipski Oct 22, 2026
2a5856f
Added comments for skipped tests
matthewlipski Oct 22, 2026
640faa9
Added ARIA snapshotting, removed screen reader testing from CI
matthewlipski Oct 27, 2026
8941dc5
Merge branch 'main' into a11y-testing
matthewlipski Oct 28, 2026
e7dd7ba
Merged main
matthewlipski Oct 28, 2026
f4a2e18
Updated visual snapshots with masks for videos
matthewlipski Oct 28, 2026
71930dd
Fixed flaky behaviour in formatting toolbar block type select focus
matthewlipski Oct 28, 2026
72eaa23
Updated a11y.yml
matthewlipski Oct 29, 2026
8e6108f
Reverted better auth version update in lockfile
matthewlipski Oct 29, 2026
b02e584
Added setup script to always wait 20ms when editor is focused to wait…
matthewlipski Oct 29, 2026
3503052
Refactored formatting toolbar tests
matthewlipski Nov 2, 2026
26abc63
Refactored remaining tests
matthewlipski Nov 2, 2026
d5cdcc5
Implemented PR feedback
matthewlipski Nov 4, 2026
f821d53
Removed separate config and GH workflow for a11y tests
matthewlipski Nov 5, 2026
fb25cdf
Implemented PR feedback
matthewlipski Nov 6, 2026
9ab8d5f
Merge branch 'main' into a11y-testing
matthewlipski Nov 6, 2026
ad280a5
Fixed build
matthewlipski Nov 6, 2026
1cf937b
Updated screenshots
matthewlipski Nov 6, 2026
89450d9
Fixed test
matthewlipski Nov 6, 2026
2354059
Skipped failing a11y test
matthewlipski Nov 6, 2026
18bcf7e
Updated agent skills
matthewlipski Nov 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .claude/skills/playwright-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ allowed-tools: Bash(playwright-cli:*) Bash(npx:*) Bash(npm:*)

# Browser Automation with playwright-cli

## BlockNote test conventions

When authoring or changing tests in this repository, follow [testing-skill](../testing-skill/SKILL.md). Colocated component/accessibility tests use Vitest browser mode, one `*.test.ts` file with separate describe groups, and the shared Docker e2e runner. Read its [accessibility reference](../testing-skill/references/accessibility-tests.md) for assertion scopes, navigation helpers, and native-only screen-reader tests. These repository conventions take precedence over the generic Playwright test-generation examples below and in the references: do not introduce a standalone Playwright runner, one `.spec.ts` per scenario, or native browser-suite runs.

## Quick start

```bash
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Spec-driven testing (plan → generate → heal)

For BlockNote repository tests, use [testing-skill](../../testing-skill/SKILL.md) and its [accessibility conventions](../../testing-skill/references/accessibility-tests.md). The generic runner setup and one-test-per-file instructions here do not apply to BlockNote's Vitest browser suite.

End-to-end workflow for authoring and maintaining Playwright tests using `playwright-cli`. The three sections below can be used independently:

- **Planning** — explore the app, produce a spec file describing what to test.
Expand Down
10 changes: 9 additions & 1 deletion .claude/skills/testing-skill/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: testing-skill
description: Instructions for writing, running, and updating unit/end-to-end tests. Should be used when prompted specifically to add tests for a given feature, bug, or regression.
description: Instructions for writing, running, and updating unit, end-to-end, and colocated accessibility tests. Use when adding or changing tests for a feature, bug, or regression.
---

# Testing
Expand Down Expand Up @@ -52,6 +52,12 @@ The device-only behaviors above, checked on real hardware before a release:

`tests/src/end-to-end`: Tests that need a real browser and span multiple packages go here — chiefly tests which interact with the editor UI or simulate user interaction, but also browser integration tests that exercise complete flows without interaction (e.g. exporting a full document, static rendering). New subdirectories can be added if the functionality being tested is not covered by any of the existing ones. Important note about existing E2E tests - many are written poorly and should only loosely be used as reference. We want to avoid abstraction layers and `waitForTimeout` as much as possible.

### Colocated Component and Accessibility Tests

Component accessibility coverage is colocated with the component/block, but runs in the existing Docker e2e suite. Use one `*.test.ts` file with separate `test.describe("behavior")`, `"keyboard"`, `"ARIA"`, `"axe"`, and, where needed, `"visual"` groups. Native screen-reader edge cases belong in an optional `*.screen-reader.test.ts` file and run locally only, outside regular e2e/CI.

When adding or changing this coverage, read [Accessibility test structure and helpers](references/accessibility-tests.md). It defines each group's assertions, fixture/helper conventions, discovery configuration, and run commands. Use the colocated FormattingToolbar, BlockNoteView, and Heading tests as the concrete examples; do not create per-component `a11y/` directories or separate files for each assertion type.

## When & How to Add Tests

Placement and proof obligations are governed by the ladder above: pick the rung red-first, and verify every new test fails without the code change it covers (for edge cases and new features too — existing tests continuing to pass is exactly the situation that demands new red-first ones).
Expand All @@ -76,6 +82,8 @@ bash tests/docker-run.sh -e CI=1 -- --run [filters]

**Never run the browser suite natively** (`vp test -c vite.config.browser.ts` outside the container) — not even "just one file". Two failure modes, both silent: the screenshot matcher _seeds_ a new `-darwin`/`-win32` baseline for every screenshot test that has none (passing without comparing anything, and littering the tree with hundreds of unvetted PNGs — these are gitignored as a backstop, and only `-linux` baselines are tracked), and several suites genuinely behave differently outside Linux (e.g. caret placement is font-metric-dependent, so platform-shared JSON snapshots mismatch). If Docker isn't running, notify the user to launch it instead of falling back to a native run.

Native screen-reader tests use the separate local runner described in the accessibility reference; the Docker-only rule above applies to the regular browser suite.

A specific test file may be targeted by appending (part of) its name as a filter. A single browser may be targeted with `--project "e2e (chromium)"`. Individual tests in a file may be disabled using `skip`, i.e. `test.skip("Test name", ...)` (remember to revert this once all tests pass).

Screenshot baselines can be regenerated with `--update=true` (as `e2e:updateSnaps` does). Always attach the value: vitest declares the flag as `-u, --update [type]` (it also accepts `new`/`all`/`none`), so a bare `-u` before a filter swallows the filter as its value and silently runs the **whole** suite in update mode. `--update=true` cannot, so filters stay filters in any position. Note that `-u` only rewrites baselines whose comparison **fails** — a small intended change (e.g. a short text edit) that fits inside the suite's 2% pixel tolerance leaves the baseline stale while the test passes. To force a fresh capture, delete the baseline file first. Baselines are per-browser (`<name>-<browser>-linux.png`); after regenerating, always inspect the images before committing them.
Expand Down
92 changes: 92 additions & 0 deletions .claude/skills/testing-skill/references/accessibility-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Colocated component and accessibility tests

All paths below are relative to the repository root. These are browser integration tests, even though their filenames end in `.test.ts`. They use the existing e2e configuration and CI jobs, not a separate accessibility browser project or workflow.

## Files and fixtures

Place files directly beside the component or block:

```text
FormattingToolbar.tsx
FormattingToolbar.test.ts
FormattingToolbar.screen-reader.test.ts # optional, native/local only
testHelpers.ts # shared navigation, when needed
__snapshots__/ # ARIA snapshots
__screenshots__/ # browser-specific visual baselines
```

Examples to read before extending an area:

- `packages/react/src/components/FormattingToolbar/FormattingToolbar.test.ts`
- `packages/react/src/editor/BlockNoteView.test.ts`
- `packages/core/src/blocks/Heading/block.test.ts`

Keep the browser scenarios in one file, grouped by assertion purpose. Do not split them into `.keyboard.test.ts`, `.aria.test.ts`, `.axe.test.ts`, or `.visual.test.ts` files or put them in a component-local `a11y/` directory. Only add groups relevant to the component.

Declare document fixtures in their consuming test files, with names ending in `Document` and typed as `PartialBlock[]`. Prefer plain data; avoid fixture-selection factories and separate `fixtures.ts` files. If both browser and screen-reader tests need a small fixture, declare it in each file rather than importing one test file from another. The common schema examples in `tests/a11y/documents.ts` remain shared infrastructure for broad editor coverage.

Write explicit named tests for each toolbar item. Share repeated test logic through named functions near the start of the file or describe block, rather than registering all item tests in a loop. Use specific names such as `toolbarButton`, `toolbarMenu`, and `toolbarItem`, rather than `control`.

## Describe blocks and assertion scope

The same keyboard actions may appear in several groups. Each group checks a different outcome; avoid adding every kind of assertion to every test.

| Group | Actions and assertions |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `behavior` | Navigate using the keyboard and activate with Enter. Assert the component's functional effect. For document edits, subscribe with `editor.onChange(vi.fn())` after navigation and before activation, register the unsubscribe with `onTestFinished`, and assert the callback ran. View-only changes may need another observable effect: toggle-heading tests check persisted expansion state. Keep detailed editor-command unit tests colocated with those commands rather than duplicating them here. |
| `keyboard` | Assert the relevant focus transitions explicitly with `toHaveFocus()`: entering the editor, reaching the toolbar/menu/item, and returning to the editor or leaving it. Assert visibility and, for document traversal, selection/block/table-cell changes using `readSelection()`. Check the visual focus indicator as well; DOM focus alone does not prove that focus is visible. |
| `ARIA` | Follow the keyboard interaction and use `checkAriaSnapshot(locator, name)` before and after activation to check names, roles, pressed/expanded state, etc. Scope item tests to the item: adding another toolbar button must not invalidate Bold's snapshots. Use a separate whole-toolbar tree snapshot to cover membership and overall structure. If activation changes the item's accessible name, locate its resulting name explicitly. |
| `axe` | Open the relevant UI state and call `checkAxe(annotate, scope)`. Scan a toolbar, menu, or popover independently where appropriate; use a whole-document scan for broad schema coverage. The helper asserts no violations and annotates incomplete results for manual review. Axe scans do not replace keyboard navigation or focus-indicator checks. |
| `visual` | Cover visual accessibility scenarios such as text-spacing overrides and 200% text resizing. Use `screenshotFull` for tall documents, wait for fonts/media, and mask nondeterministic video frames where needed. These checks concern layout/readability rather than individual button activation. |

On `a11y-testing`, keyboard focus appearance is currently checked with item-level screenshots. FormattingToolbar's `checkScreenshotWithPadding` captures the item plus 2 px on each side so its focus ring is not clipped; avoid whole-toolbar screenshots for individual buttons. Keep visual-indicator assertions separate from navigation's `toHaveFocus()` assertions. Follow the checked-out branch's implementation when changing this mechanism; do not assume newer helpers from another branch exist.

## Helpers and synchronization

Import the common harness from `@root/tests/a11y/helpers.js`:

- `openEditor(document, { editable, theme, onReady }?)` mounts the editor and surrounding "Before editor" / "After editor" buttons, waits for readiness, and returns the editor. Normal browser runs start focused on "Before editor"; native screen-reader runs start the reader.
- `pressKey(key)` sends real keyboard input through the browser provider and waits for pending editor focus reconciliation before the next key. Use `selectToLineEndKey` for platform-appropriate selection.
- `readSelection()` exposes block ID, table-cell coordinates, offset, and selected text for navigation assertions.
- `checkAriaSnapshot(locator, name)` captures the scoped browser accessibility tree into Vitest snapshots.
- `checkAxe(annotate, scope?)` runs axe with optional CSS scope and reports violations/incomplete results.
- `saveTranscript()` captures a native reader transcript and stops the reader in `finally`; register it with `afterEach` in screen-reader files.

Keep reusable multi-step navigation in the colocated `testHelpers.ts`, such as `focusToolbarItem`, `focusToggleHeading`, or `navigateDefaultSchemaDocument`. Keep behavior assertions out of those action helpers. The keyboard group's corresponding `...WithAssertions` function follows the same navigation steps, adding focus/visibility/selection assertions. Inline trivial one- or two-line wrappers around `pressKey`, storage resets, or element lookup. Avoid generic setup helpers that mix fixture choice, navigation, and spying.

Navigate to a requested item by advancing with keyboard input until that item has focus, with the test timeout bounding failure. Do not maintain a duplicate list of enabled toolbar buttons or calculate a fixed number of Tab presses. Use accessible locators (`getByRole` with a name, `getByLabel`) where available.

Wait on observable readiness with retrying assertions, not arbitrary sleeps. This also applies outside the keyboard group when needed to drive the next action reliably. For example, Mantine asynchronously focuses a `[data-autofocus]` placeholder after opening a menu: wait for menu visibility, then assert that placeholder has focus before sending ArrowDown. Visibility alone is not enough. Prefer these explicit assertions to `vi.waitUntil` polling for focus. Use live locators for asynchronously mounted elements; passing the result of an early `document.querySelector()` to an assertion can permanently capture `null`.

## Native screen-reader edge cases

Use a separate `*.screen-reader.test.ts` only for actual reader announcements/navigation that ARIA snapshots cannot establish. Reuse keyboard navigation helpers, call `openEditor`, and register `afterEach(saveTranscript)`. Avoid duplicating every browser scenario. Transcripts are reader-specific; review changes rather than accepting them blindly.

These tests run locally with VoiceOver on macOS or NVDA on Windows and require the installed reader and GuidePup permissions. They must stay excluded from regular e2e, node tests, and CI. `tests/a11y/vite.config.ts` is for this native runner only.

## Discovery and commands

The colocated `.test.ts` browser files are explicitly registered. When adding an area, update:

- `tests/vite.config.browser.ts`: include its browser test in the shared e2e suite; preserve the screen-reader exclusions, including desktop instance exclusions.
- The package's `vite.config.ts`: exclude the browser test from node/jsdom unit runs.
- The package's `tsconfig.json` and `tests/a11y/tsconfig.json`: keep test-only files/helpers out of production declarations and include them in the accessibility type-check project.
- Root `vite.config.ts`: extend the accessible-locator lint override for the new test files where necessary.
- `tests/a11y/run.mjs`: add the area's browser test to the default filters for the `a11y` shortcut.

Run from the repository root:

```bash
# Existing e2e suite, always Docker; narrow by file, group, and/or browser.
vp run e2e FormattingToolbar.test.ts
vp run e2e FormattingToolbar.test.ts -t keyboard --project "e2e (webkit)" --retry=0
vp run a11y

# Update only the selected suite, then review and rerun without updating.
vp run e2e:updateSnaps FormattingToolbar.test.ts

# Native/local only, never part of regular e2e or CI.
vp run a11y:screen-reader FormattingToolbar.screen-reader.test.ts
```

Keep known functionality gaps skipped with a brief comment explaining the actual failure. Use `test.skipIf(server.browser === "webkit")`, for example, when only that browser is affected, preserving other browsers' coverage. Do not skip timing failures or increase retries to hide them: fix readiness/locator handling and verify repeated runs with retries disabled. Do not refresh snapshots to accept an unexplained regression.
Loading
Loading