trilium/.claude/skills/ckeditor5-testing/references/running-and-config.md
Elian Doran e8eeb68b50
test(ckeditor5): run browser mode on Playwright instead of webdriverio
Vitest 5 handed the webdriverio provider to community maintenance, and no
stable 5.x of `@vitest/browser-webdriverio` was ever published — npm `latest`
is still 4.1.11, with only a 5.0.0-rc.1 from before vitest 5.0.0 shipped.
Pairing that v4 provider with `@vitest/browser` 5 kills the suite at startup:

    TypeError: Cannot read properties of undefined (reading 'project')
        at createBrowserServer (@vitest/browser/dist/index.js:7900:26)

`@vitest/browser-playwright` 5.0.0 is stable and first-party, and Playwright
1.62.1 is already here for the e2e suites, so the browser toolchain costs
nothing new. Only two of the 129 specs touch provider-visible API at all, both
through the provider-agnostic `userEvent`.

Playwright drives the browser over CDP with no separate driver, so the
chromedriver half of the NixOS workaround goes away: the dev shell keeps
`pkgs.chromium` and `CHROME_BIN`, which now reaches the provider as
`launchOptions.executablePath`. CI installs the browser in a step of its own
rather than inside the test step, whose 15-minute cap exists to catch a browser
session that never starts and should not also have to cover a 190 MB download.

Two specs depended on webdriverio behaviour and are fixed rather than skipped:

- The token-cost assertion read `1.234` where it wanted `1,234`.
  `toLocaleString()` takes the browser's locale, and Playwright inherits the
  host's where the old Chrome defaulted to en-US. Pinning `contextOptions.locale`
  keeps the suite from depending on the developer's machine.
- The format painter's drag-selection selected nothing. Playwright's
  `dragAndDrop` turns on drag interception, so the press reaches the page as an
  HTML5 drag intent instead of selecting text. Driving the press, move and
  release over CDP restores what the test is actually for: proving a *native*
  pointer interaction has updated the model selection by the time the `mouseup`
  listener runs.

Vitest 5 also moved failure screenshots from `.vitest-attachments` to
`.vitest`, which needs ignoring.

Verified: 127 files / 1626 tests green, coverage 99.91/99.54/100/99.98 against
the 99.5 gate. The Nix path is unexercised — `executablePath` is ordinary
Playwright, but it was not run from a dev shell.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 23:19:52 +02:00

7.9 KiB

Running tests & configuration (Trilium)

One package carries the CKEditor 5 tests: packages/ckeditor5 (the aggregate, which holds every in-tree plugin under src/plugins/). Its vitest.config.ts is built with defineConfig directly — there is no shared factory. Vitest is 4 or later.

Per-package scripts

Each package's package.json defines:

Script Command Purpose
test vitest Run the package's tests (configs set watch: false, so this is one-shot).
test:debug vitest --inspect-brk --no-file-parallelism --browser.headless=false Attach a debugger and watch a browser-mode run with a visible window.

Run a single package from anywhere in the monorepo:

pnpm --filter @triliumnext/ckeditor5 test

Or, from the package directory: vitest run. Add -t "name" to filter by test name, or a filename substring to filter by file.

Supplying the browser

Playwright downloads its own Chromium into a per-user cache (~/.cache/ms-playwright, %LOCALAPPDATA%\ms-playwright on Windows). Install it once with pnpm exec playwright install chromium; CI does this in the test step.

Where that build cannot execute — NixOS, where it is linked against libraries no store path provides and aborts on a missing libxcb.so.1CHROME_BIN hands Playwright a system browser instead:

Variable Read by Effect
CHROME_BIN packages/ckeditor5/vitest.config.ts Passed to the provider as launchOptions.executablePath, so Playwright launches that binary rather than its own download.
CHROME_BIN=/path/to/chromium pnpm --filter @triliumnext/ckeditor5 test

nix develop exports it from pkgs.chromium, which is why the plain command works inside the dev shell. There is no separate driver to supply — Playwright speaks CDP to the browser directly, which is what retired the old CHROMEDRIVER_PATH pairing.

A failing browser test writes a PNG into a gitignored __screenshots__ directory next to the spec. Clean those up when done.

The config shape

Both packages run Playwright browser mode: real headless Chromium via @vitest/browser-playwright, with real DOM and layout, gating src/** coverage at 100%. Trilium previously ran some plugins on happy-dom; no CKEditor package does now.

import { defineConfig } from 'vitest/config';
import svg from 'vite-plugin-svgo';
import { playwright } from '@vitest/browser-playwright';

export default defineConfig( {
	plugins: [ svg() ],
	test: {
		browser: {
			enabled: true,
			provider: playwright(),
			headless: true,
			ui: false,
			instances: [ { browser: 'chromium' } ]
		},
		include: [ 'src/**/*.spec.ts' ],       // math instead uses [ 'tests/**/*.[jt]s' ]
		setupFiles: [ './test/setup.ts' ],     // aggregate only — wires the editor-kit teardown
		globals: true,
		watch: false,
		coverage: {
			thresholds: { lines: 100, functions: 100, branches: 100, statements: 100 },
			provider: 'v8',
			include: [ 'src/**/*.{ts,tsx}' ],
			exclude: [ '**/*.{test,spec}.{ts,mts,cts,tsx,js,jsx}', '**/*.d.ts' ],
			reporter: [ 'text' ]
		}
	}
} );

Also standard: globals: true, the vite-plugin-svgo plugin so import icon from './x.svg' resolves, and coverage via v8 over src/** (test files themselves excluded).

Test-file location. packages/ckeditor5 uses co-located *.spec.ts next to the source — include: ['src/**/*.spec.ts'] — including inside plugin folders, e.g. src/plugins/collapsible/collapsible_editing.spec.ts. That is the repo-wide convention (see the writing-unit-tests skill).

The aggregate also sets setupFiles: ['./test/setup.ts'], which wires the global afterEach that destroys editors created through test/editor-kit.ts.

Coverage scope for the aggregate (packages/ckeditor5)

The aggregate used to import sibling ckeditor5-* packages, whose loaded src/ a plain --coverage run would instrument too, dragging the report below the aggregate's real number. Nothing sibling is left, but the report is still scoped to this package's own sources — packages/ckeditor5/vitest.config.ts does this with:

coverage: {
	provider: 'v8',
	allowExternal: false,                    // don't reach outside the package root
	include: [ 'src/**/*.{ts,tsx}' ],
	exclude: [
		'**/*.{test,spec}.{ts,mts,cts,tsx,js,jsx}', '**/*.d.ts',
		'**/node_modules/**', '**/ckeditor5-*/**' // <- keeps imported siblings out
	],
	reporter: [ 'text', 'lcov' ],
	reportsDirectory: './test-output/vitest/coverage'
}

reporter: ['text', 'lcov'] + that reportsDirectory are what the analyzing-coverage analyzer (lcov.info) and Codecov consume — keep them when adding coverage to a package.

When the session never starts

Two failure modes look like a broken suite but are environmental.

"Executable doesn't exist at …/ms-playwright/chromium-NNNN"

Playwright resolves a browser build keyed to its own version, so the cache is empty on a fresh checkout and goes stale whenever the pinned playwright moves to a build that was never downloaded. Both cases raise this at session start.

pnpm exec playwright install chromium

Run it from the repo root so the pinned playwright resolves. Where the downloaded build cannot execute at all, supply a system browser through CHROME_BIN instead — see Supplying the browser above.

The run hangs at [vite] [optimizer] bundling dependencies...

In a fresh git worktree the browser-mode suite reliably stalls there and never runs a test, eventually reporting "Browser connection was closed" or "no tests" — even after clearing node_modules/.vite and killing stray browsers. The cold Vite dep-optimize for the very large CKEditor dep set does not complete (it may be contending with the editor's own Vitest extension workers). The same spec runs in seconds from the main checkout, where that cache is warm.

So validate browser-mode specs from the main checkout: copy the new *.ts + *.spec.ts into <main>/packages/ckeditor5/src/plugins/, run them there (add --coverage.enabled --coverage.include='<file>' to check the 100 % gate against just that file), then delete the temporary copies. A spec that imports the plugin directly does not need the plugins.ts / toolbar wiring to be present in main. Client (apps/client) happy-dom tests are unaffected — this is browser-mode only.

After an aborted run

Killed runs leave orphaned headless Chrome processes (--test-type=webdriver, a scoped_dir user-data directory) holding resources. Kill those, and only those — never the user's interactive Chrome, and never the editor's Vitest extension workers or its Vite dev server. Also don't pipe vitest through Select-Object -Last N: it buffers everything until exit, so you lose all progress output.

Debugging

Browser-mode packages support an inspector + visible browser:

vitest --inspect-brk --no-file-parallelism --browser.headless=false
# i.e. the package's `test:debug` script

--no-file-parallelism keeps one file at a time so breakpoints are predictable; --browser.headless=false shows the Chrome window.

Root orchestration

The root package.json splits the run because the browser-mode packages compete for browser resources:

pnpm test:parallel     # everything except server and ckeditor5, in parallel
pnpm test:sequential   # server and ckeditor5, sequentially
pnpm test:all          # test:parallel && test:sequential

ckeditor5 must not run alongside another browser-mode suite — multiple headless Chrome instances at once exhaust resources. server is in the same group for a different reason (shared test DB, per CLAUDE.md), not browser limits. Everything else runs in parallel.

Notes

  • Both packages are at 100% coverage and gated there, so a change that adds a line adds a test.
  • There are no manual-test or memory-leak harnesses in the Trilium plugin packages (those exist only in the upstream ckeditor5 monorepo).