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>
This commit is contained in:
Elian Doran 2026-09-07 23:03:43 +02:00
parent 4b137134ee
commit e8eeb68b50
No known key found for this signature in database
19 changed files with 149 additions and 1131 deletions

View File

@ -117,5 +117,5 @@ integration items at the end are specific to this monorepo.
`ClassicEditor` (Decoupled), `PopupEditor` (Balloon + `BlockToolbar`).
- [ ] Block widgets enforce structural invariants with `registerPostFixer` (admonition,
collapsible) rather than relying on command-side cleanup.
- [ ] **Tests use the right environment**: happy-dom for unit/model logic; WebdriverIO
- [ ] **Tests use the right environment**: happy-dom for unit/model logic; Playwright
(browser) only where real DOM/layout is required.

View File

@ -188,9 +188,9 @@ pnpm --filter @triliumnext/ckeditor5-<feature> lint # eslint-config-ckedi
pnpm --filter @triliumnext/ckeditor5-<feature> stylelint # theme CSS
```
Tests run in a real headless Chrome that webdriverio downloads. Where that build cannot run — NixOS
being the case in point — set `CHROME_BIN` and `CHROMEDRIVER_PATH` to a matching system pair, which
`nix develop` already exports; don't start a driver by hand.
Tests run in a real headless Chromium that Playwright downloads (`pnpm exec playwright install
chromium`). Where that build cannot run — NixOS being the case in point — set `CHROME_BIN` to a
system browser, which `nix develop` already exports.
For test setup, model/view assertions, and command/UI test patterns, use the separate
**`ckeditor5-testing`** skill.

View File

@ -23,7 +23,7 @@ the companion skills rather than duplicating them:
- **`ckeditor5-plugin-development`** — its `references/review-checklist.md` (architecture, schema,
conversion, commands, UI, a11y) and `references/conventions.md` (naming, imports, JSDoc, TS).
- **`ckeditor5-testing`** — its review checklist and patterns for the test side (Vitest, browser-mode
vs. `@vitest/browser-webdriverio` browser mode, real `ClassicEditor.create`).
vs. `@vitest/browser-playwright` browser mode, real `ClassicEditor.create`).
Use those for "does this follow the conventions"; use this skill for **how to drive the review**
and **what subtle things tend to be wrong**.
@ -79,8 +79,8 @@ plugin; sanity-checking your own feature before opening a PR. For *writing* the
`no-legacy-imports`) and `stylelint-config-ckeditor5` — a diff that breaks them fails lint.
5. **Run the tests for the affected package.** Use Vitest via
`pnpm --filter @triliumnext/ckeditor5 test` (or `...-math`); the two run sequentially because
each spins up headless Chrome. Both use **`@vitest/browser-webdriverio` browser mode** (NOT
Playwright) and gate `src/**` at **100% coverage**. Coverage ≠ correctness: confirm
each spins up headless Chromium. Both use **`@vitest/browser-playwright` browser mode** and
gate `src/**` at **100% coverage**. Coverage ≠ correctness: confirm
the *change itself* is tested, not just that lines are hit. A bug fix with no new/changed test is
a red flag even when coverage stays green.
6. **Observe behavior.** Attach the CKEditor Inspector (model / view / schema / commands), then:

View File

@ -294,8 +294,8 @@ nothing loads it, no button shows, or lint/license/localization is off.
expected to suppress native behaviour; an assertion made immediately after an event the browser
dispatches asynchronously (`<details>` `toggle`, for one); or `preventDefault` used as proof
that a handler ran when a CKEditor plugin also calls it.
- Why: both CKEditor packages run in **real headless Chrome** (`@vitest/browser-webdriverio`, NOT
Playwright). Tests ported from the old happy-dom setup relied on stubbed layout and synchronous
- Why: both CKEditor packages run in **real headless Chromium** (`@vitest/browser-playwright`).
Tests ported from the old happy-dom setup relied on stubbed layout and synchronous
events, and pass or fail for the wrong reasons here.
- Fix: make synthetic events cancelable, await the real event before asserting, and prove a handler
ran by spying on `editor.execute` or asserting the model.

View File

@ -4,7 +4,7 @@ description: >-
Testing CKEditor 5 plugins in the Trilium monorepo. Use when adding or
reviewing unit tests for the packages/ckeditor5 aggregate (including its
in-tree plugins under src/plugins/), debugging a
failing test, or setting up a package's test runner. Covers the WebdriverIO
failing test, or setting up a package's test runner. Covers the Playwright
browser-mode Vitest setup, the vitest.config.ts, testing against a real ClassicEditor, the
model/view helpers imported from 'ckeditor5' (_setModelData / _getModelData /
_getViewData and their {}/[] selection syntax), vi spies/mocks, idiomatic
@ -40,10 +40,10 @@ Trilium testing (Preact components, jQuery widgets, server routes), use `writing
- **Runner:** Vitest 4 or later. **No shared factory** — each package has its own
`vitest.config.ts` built with `defineConfig` directly.
- **One environment: WebdriverIO browser mode** (`@vitest/browser-webdriverio`, headless Chrome),
- **One environment: Playwright browser mode** (`@vitest/browser-playwright`, headless Chromium),
used by `ckeditor5`. Real DOM and real layout, so
`getBoundingClientRect()`, `elementFromPoint()` and pointer events behave as in a browser. Gates
`src/**` coverage at 100% (lines/functions/branches/statements). This is **not Playwright**.
`src/**` coverage at 100% (lines/functions/branches/statements).
- Trilium used to run some plugins on **happy-dom**; that is gone. If you are porting an old
test, note the two differences that bite: happy-dom returned zeros from layout APIs (so
measurement-dependent code silently "passed"), and it fired some DOM events synchronously
@ -87,21 +87,19 @@ run a filtered package suite instead (`pnpm --filter @triliumnext/ckeditor5 test
leave the aggregates to CI. Each package exposes `"test": "vitest"` and
`"test:debug": "vitest --inspect-brk --no-file-parallelism --browser.headless=false"`.
**When the downloaded browser cannot run** — on NixOS the Chrome for Testing build and chromedriver
webdriverio fetches into `/tmp` are linked against libraries no store path provides and die on a
missing `libxcb.so.1` — point the suite at a system pair instead:
Playwright downloads its own Chromium on demand; install it once with `pnpm exec playwright install
chromium` from the repo root.
**When that build cannot run** — on NixOS it is linked against libraries no store path provides and
dies on a missing `libxcb.so.1` — point the suite at a system browser instead:
```bash
CHROME_BIN=/path/to/chromium CHROMEDRIVER_PATH=/path/to/chromedriver \
pnpm --filter @triliumnext/ckeditor5 test
CHROME_BIN=/path/to/chromium pnpm --filter @triliumnext/ckeditor5 test
```
`CHROMEDRIVER_PATH` is webdriverio's own variable and makes it spawn that driver on a free port;
`CHROME_BIN` is read by the package's `vitest.config.ts` and passed as `goog:chromeOptions.binary`,
which also stops the browser download (`setupPuppeteerBrowser` returns early for a string `binary`).
The two versions must match at least in their major. `nix develop` sets both from `pkgs.chromium`
and `pkgs.chromedriver`, so inside the dev shell the plain command works — **don't** start a driver
by hand or write a local override config.
`CHROME_BIN` is read by the package's `vitest.config.ts` and passed to the provider as
`launchOptions.executablePath`. `nix develop` sets it from `pkgs.chromium`, so inside the dev shell
the plain command works — **don't** write a local override config.
Failed browser tests dump PNGs into a gitignored `__screenshots__` beside the spec; delete them
afterwards.
@ -230,7 +228,7 @@ into later specs.) See `references/patterns.md` for the recipe.
|------|-----------|
| `references/test-utilities.md` | Testing against a real `ClassicEditor` (lifecycle, `licenseKey: 'GPL'`), and the `_setModelData`/`_getModelData`/`_getViewData` helpers from `'ckeditor5'` + the `[]`/`{}` selection syntax. |
| `references/patterns.md` | Idiomatic recipes per concern (schema, conversion round-trips, commands, UI, keystrokes, events, async), all against a real editor; the `glob`/clipboard/jQuery-`$` stubbing recipe (via the globals kit's `installGlobMock`/`mockClipboard`); note on the 100% coverage gate for browser-mode packages. |
| `references/running-and-config.md` | The WebdriverIO `vitest.config.ts` shape, `pnpm --filter` commands, the debug command, `pnpm test:parallel`/`test:sequential` (ckeditor5 + math sequential), coverage thresholds, and troubleshooting a session that never starts (staged-Chrome/chromedriver version mismatch, the worktree dep-optimizer hang, orphaned headless Chrome). |
| `references/running-and-config.md` | The WebdriverIO `vitest.config.ts` shape, `pnpm --filter` commands, the debug command, `pnpm test:parallel`/`test:sequential` (ckeditor5 + math sequential), coverage thresholds, and troubleshooting a session that never starts (a missing Playwright browser build, the worktree dep-optimizer hang, orphaned headless Chromium). |
| `references/test-conventions.md` | Trilium test **conventions & gotchas**: real-browser event timing, real-editor teardown, the both-assertion-styles note, unreachable code vs. the 100% gate, and the pointer to `writing-unit-tests`. |
## Quick review checklist

View File

@ -23,50 +23,51 @@ 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 and driver
### Supplying the browser
webdriverio downloads a Chrome for Testing build and a matching chromedriver into `/tmp` on first
run. Where those cannot execute — NixOS, where they are linked against libraries no store path
provides and abort on a missing `libxcb.so.1` — two variables hand it a system pair instead:
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.1``CHROME_BIN` hands Playwright a system browser
instead:
| Variable | Read by | Effect |
|---|---|---|
| `CHROMEDRIVER_PATH` | webdriverio (`@wdio/utils` `startWebDriver`) | Spawns that driver on a free port instead of downloading one. |
| `CHROME_BIN` | `packages/ckeditor5/vitest.config.ts` | Passed as `goog:chromeOptions.binary`; `setupPuppeteerBrowser` returns early for a string `binary`, so no browser is downloaded either. |
| `CHROME_BIN` | `packages/ckeditor5/vitest.config.ts` | Passed to the provider as `launchOptions.executablePath`, so Playwright launches that binary rather than its own download. |
```bash
CHROME_BIN=/path/to/chromium CHROMEDRIVER_PATH=/path/to/chromedriver \
pnpm --filter @triliumnext/ckeditor5 test
CHROME_BIN=/path/to/chromium pnpm --filter @triliumnext/ckeditor5 test
```
The versions must match at least in their major. `nix develop` exports both from `pkgs.chromium`
and `pkgs.chromedriver` (same nixpkgs revision, so they agree), which is why the plain command works
inside the dev shell. Starting a chromedriver by hand and writing a local config that connects to
its port does work, but it is strictly more setup — reach for the variables.
`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 **WebdriverIO browser mode**: real headless Chrome via
`@vitest/browser-webdriverio` (**not** Playwright), with real DOM and layout, gating `src/**`
coverage at 100%. Trilium previously ran some plugins on happy-dom; no CKEditor package does now.
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.
```ts
import { defineConfig } from 'vitest/config';
import svg from 'vite-plugin-svgo';
import { webdriverio } from '@vitest/browser-webdriverio';
import { playwright } from '@vitest/browser-playwright';
export default defineConfig( {
plugins: [ svg() ],
test: {
browser: {
enabled: true,
provider: webdriverio(),
provider: playwright(),
headless: true,
ui: false,
instances: [ { browser: 'chrome' } ]
instances: [ { browser: 'chromium' } ]
},
include: [ 'src/**/*.spec.ts' ], // math instead uses [ 'tests/**/*.[jt]s' ]
setupFiles: [ './test/setup.ts' ], // aggregate only — wires the editor-kit teardown
@ -122,29 +123,19 @@ analyzer (`lcov.info`) and Codecov consume — keep them when adding coverage to
Two failure modes look like a broken suite but are environmental.
### "This version of ChromeDriver only supports Chrome version N"
### "Executable doesn't exist at …/ms-playwright/chromium-NNNN"
webdriverio auto-manages the driver by detecting the installed Chrome's version. When Chrome has a
**staged update** — a `new_chrome.exe` and a new version folder sitting in the install directory,
waiting for a browser restart — detection reads the *staged* version and downloads that
chromedriver, while the `chrome.exe` that actually launches is still the old major. Every run then
dies at session start.
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.
Restarting Chrome fixes it permanently. To run before then, point wdio at the matching driver
already in its cache (`%TEMP%\chromedriver\win64-<version>\` on Windows) by editing
`packages/ckeditor5/vitest.config.ts` — at the **provider factory** level, because
`@vitest/browser-webdriverio` drops per-instance options and only the factory's reach `remote()`:
```ts
provider: webdriverio({
capabilities: {
"wdio:chromedriverOptions": { binary: "<cached>/chromedriver-win64/chromedriver.exe" }
}
}),
```bash
pnpm exec playwright install chromium
```
**Revert that edit after the run — never commit it.** `browserVersion` pins do not help at either
level; wdio still resolves the local binary.
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...`

View File

@ -4,10 +4,10 @@ Trilium never used Karma/Mocha/Sinon — the plugin tests are Vitest from the st
migration to do. This reference collects the Trilium-specific conventions and traps when writing
CKEditor 5 plugin tests.
## The environment: WebdriverIO browser mode
## The environment: Playwright browser mode
`packages/ckeditor5` runs its tests in **real headless Chrome** via
`@vitest/browser-webdriverio`, so layout, `getBoundingClientRect()`, `elementFromPoint()` and
`packages/ckeditor5` runs its tests in **real headless Chromium** via
`@vitest/browser-playwright`, so layout, `getBoundingClientRect()`, `elementFromPoint()` and
pointer events all behave as they do in a browser. Both gate `src/**` at 100% coverage.
Trilium used to run some plugins on happy-dom. Nothing does now, but the difference matters when

View File

@ -82,7 +82,7 @@ coverage: {
- **`vi.resetModules()` drops `importOriginal`-based partial mocks.** The next dynamic import of the module gets the *real* dependency back, not your stub, and the symptom reads as "the mock isn't applying" rather than as a reset problem. Prefer registering singletons and IPC handlers **once** and ordering the tests instead — put the one that latches module-level state last. Where a reset is genuinely needed (a value cached on first call), mock with a full factory rather than an `importOriginal` partial.
- **Don't assert on translated (i18n) strings** — assert structure/keys/behavior (classes, counts, ids), not human-readable English.
- **happy-dom is not a browser:** `getBoundingClientRect()` returns zeros, `ResizeObserver`/layout/visibility are stubs. Anything pixel/size/scroll-based needs `@vitest/browser`, not happy-dom.
- **`@vitest/browser` real-browser mode IS configured** — the `packages/ckeditor5`, `-mermaid` and `-math` bundles run their co-located `src/**/*.spec.ts` in headless Chrome (`@vitest/browser-webdriverio`; see `packages/ckeditor5/vitest.config.ts`). These are the browser-mode `test:sequential` suites. Reserve real-browser mode for genuine layout/integration needs (CKEditor, Excalidraw, Modal transitions, size measurement); normal unit tests stay on happy-dom.
- **`@vitest/browser` real-browser mode IS configured** — the `packages/ckeditor5`, `-mermaid` and `-math` bundles run their co-located `src/**/*.spec.ts` in headless Chromium (`@vitest/browser-playwright`; see `packages/ckeditor5/vitest.config.ts`). These are the browser-mode `test:sequential` suites. Reserve real-browser mode for genuine layout/integration needs (CKEditor, Excalidraw, Modal transitions, size measurement); normal unit tests stay on happy-dom.
- **WASM scrypt is ~10× slower** under the standalone suite (pure-JS `scrypt-js` under V8 coverage instrumentation, vs Node's native `scryptSync`) — enough to blow the 5s default. A core spec that hashes a password bumps the timeout for the standalone runtime only; copy the guard from `packages/trilium-core/src/routes/api/login.spec.ts:13`:
```ts
const isBrowserRuntime = typeof window !== "undefined";

View File

@ -429,22 +429,18 @@ jobs:
if: env.AFFECTED == 'true'
run: pnpm install --frozen-lockfile
- name: Install Playwright's Chromium
if: env.AFFECTED == 'true'
run: pnpm exec playwright install --with-deps chromium
- name: Run the CKEditor 5 aggregate tests
id: test-ckeditor5
if: env.AFFECTED == 'true'
# The suite drives a real headless Chrome through webdriverio. When the browser session
# The suite drives a real headless Chromium through Playwright. When the browser session
# fails to start, vitest waits on it rather than erroring, so cap the step instead of
# letting it sit until the job's default timeout.
timeout-minutes: 15
run: |
export CHROMEDRIVER_PATH=$(which chromedriver || find /usr/local/share -name chromedriver -type f 2>/dev/null | head -1)
echo "Using chromedriver at: $CHROMEDRIVER_PATH"
# A chromedriver whose major version does not match the installed Chrome fails the
# session handshake, which surfaces as a hang or a mid-run disconnect rather than a
# clear error — so record both up front.
"$CHROMEDRIVER_PATH" --version || echo "could not read the chromedriver version"
(google-chrome --version || chromium --version || chromium-browser --version) 2>/dev/null \
|| echo "could not read the Chrome version"
# Browser mode keeps one page for the whole run, and v8 coverage data accumulates in it:
# measured locally, the heap reaches ~850MB by the 100th spec file with --coverage against
# ~240MB without it. That is where a CI runner's renderer gives out — the session either

1
.gitignore vendored
View File

@ -37,6 +37,7 @@ vite.config.*.timestamp*
vitest.config.*.timestamp*
test-output
.vitest-reports
.vitest
__screenshots__
.vitest-attachments

View File

@ -203,7 +203,7 @@ Use `note.getOwnedAttribute()` for direct, `note.getAttribute()` for inherited.
- **Core tests** (`packages/trilium-core/src/**/*.spec.ts`): `trilium-core` has no runner of its own — the **server and standalone suites both include** its specs (`apps/server/vite.config.mts`, `apps/standalone/vite.config.mts`) and run them against different platform providers (node + better-sqlite3 vs. happy-dom + sqlite-wasm). Green under `pnpm --filter server test` is **not** proof; run `pnpm --filter standalone test` as well. See the `writing-unit-tests` skill for the cross-runtime traps
- **E2E tests** (`packages/trilium-e2e/`): Shared Playwright tests, run via `pnpm --filter server e2e` or `pnpm --filter standalone e2e`
- **ETAPI tests** (`apps/server/spec/etapi/`): External API contract tests
- **Browser-mode tests** (`packages/ckeditor5`) drive a real headless Chrome via `@vitest/browser-webdriverio`; where its downloaded Chrome cannot run (NixOS), point `CHROME_BIN`/`CHROMEDRIVER_PATH` at a matching system pair — never start a chromedriver by hand or add a local override config. See the `ckeditor5-testing` skill
- **Browser-mode tests** (`packages/ckeditor5`) drive a real headless Chromium via `@vitest/browser-playwright` (`pnpm exec playwright install chromium` once); where that browser cannot run (NixOS), point `CHROME_BIN` at a system one — never add a local override config. See the `ckeditor5-testing` skill
- **Build validation tests** check artifact integrity
- **Write concise tests**: Group related assertions together in a single test case rather than creating many one-shot tests
- **Extract and test business logic**: When adding pure business logic (e.g., data transformations, migrations, validations), extract it as a separate function and always write unit tests for it

View File

@ -70,17 +70,17 @@ Note that some integration tests rely on an in-memory database in order to funct
### Browser-mode tests for the text editor
`packages/ckeditor5` runs its tests in a real headless Chrome, through `@vitest/browser-webdriverio`, because the editor needs a real DOM and real selection handling. By default webdriverio downloads both a Chrome for Testing build and a matching chromedriver, which is what happens on a normal machine and needs no setup.
`packages/ckeditor5` runs its tests in a real headless Chromium, through `@vitest/browser-playwright`, because the editor needs a real DOM and real selection handling. Playwright downloads the browser itself; install it once with `pnpm exec playwright install chromium` from the repository root.
Where those downloaded binaries cannot run — NixOS being the case in point, since they are dynamically linked against libraries no store path provides and die on a missing `libxcb.so.1` — point the suite at a system browser and driver instead:
Where that downloaded browser cannot run — NixOS being the case in point, since it is dynamically linked against libraries no store path provides and dies on a missing `libxcb.so.1` — point the suite at a system browser instead:
```
CHROME_BIN=/path/to/chromium CHROMEDRIVER_PATH=/path/to/chromedriver pnpm --filter @triliumnext/ckeditor5 test
CHROME_BIN=/path/to/chromium pnpm --filter @triliumnext/ckeditor5 test
```
`CHROMEDRIVER_PATH` is webdriverio's own variable; `CHROME_BIN` is read by the package's `vitest.config.ts` and passed through as a capability, which also stops webdriverio from downloading a browser at all. The two versions have to match, at least in their major.
`CHROME_BIN` is read by the package's `vitest.config.ts` and passed to the provider as `launchOptions.executablePath`, so Playwright launches that binary rather than its own download. There is no separate driver to supply — Playwright speaks CDP to the browser directly.
The Nix dev shell (`nix develop`) sets both from `pkgs.chromium` and `pkgs.chromedriver`, so inside it the tests run unchanged.
The Nix dev shell (`nix develop`) sets it from `pkgs.chromium`, so inside it the tests run unchanged.
### REST API testing for the server

View File

@ -414,18 +414,16 @@
pnpm
electron
nodejs.python
# For the browser-mode tests (packages/ckeditor5). The Chrome and chromedriver
# webdriverio downloads for itself are dynamically linked against libraries no NixOS
# system provides, so they die on a missing libxcb.so.1; these come from the same
# nixpkgs revision, so their versions match.
# For the browser-mode tests (packages/ckeditor5). The Chromium Playwright downloads
# for itself is dynamically linked against libraries no NixOS system provides, so it
# dies on a missing libxcb.so.1.
pkgs.chromium
pkgs.chromedriver
];
# Read by packages/ckeditor5/vitest.config.ts and by webdriverio itself, respectively.
# Without them webdriverio downloads its own pair and the suite cannot start.
# Read by packages/ckeditor5/vitest.config.ts and passed to Playwright as
# `launchOptions.executablePath`. Without it Playwright launches its own Chromium and the
# suite cannot start.
CHROME_BIN = "${pkgs.chromium}/bin/chromium";
CHROMEDRIVER_PATH = "${pkgs.chromedriver}/bin/chromedriver";
};
}
);

View File

@ -62,7 +62,7 @@
"@types/express": "5.0.6",
"@types/node": "24.13.3",
"@typescript/native": "npm:typescript@7.0.2",
"@vitest/browser-webdriverio": "4.1.11",
"@vitest/browser-playwright": "5.0.0",
"@vitest/coverage-v8": "5.0.0",
"@vitest/ui": "5.0.0",
"cross-env": "10.1.0",
@ -76,6 +76,7 @@
"http-server": "14.1.1",
"jiti": "2.7.0",
"js-yaml": "5.4.1",
"playwright": "1.62.1",
"tslib": "2.8.1",
"tsx": "4.23.13",
"typescript": "6.0.3",

View File

@ -10,8 +10,7 @@
},
"devDependencies": {
"@vitest/browser": "5.0.0",
"vitest": "5.0.0",
"webdriverio": "9.31.5"
"vitest": "5.0.0"
},
"dependencies": {
"@triliumnext/commons": "workspace:*",

View File

@ -92,7 +92,7 @@ this package.
| `utils.ts` | Model/view tree query helpers |
| `constants.ts` | Element, class, attribute and command name tables |
Tests sit beside each source as `*.spec.ts` and run in the aggregate's WebdriverIO browser-mode
Tests sit beside each source as `*.spec.ts` and run in the aggregate's Playwright browser-mode
suite against a real `ClassicEditor`. Fixtures come from `test/footnotes-kit.ts` and build the
document with a model writer rather than `_setModelData()` — the parser coerces numeric-looking
attribute values (`data-footnote-index="1"`) to numbers where the plugin writes and compares

View File

@ -1,4 +1,4 @@
import { userEvent } from "vitest/browser";
import { cdp, userEvent } from "vitest/browser";
import {
Bold,
type ButtonView,
@ -157,8 +157,10 @@ describe("FormatPainterUI", () => {
if (!(second instanceof HTMLElement) || !(third instanceof HTMLElement)) {
throw new Error("target paragraphs are not attached");
}
// A real pointer press, move and release — i.e. a native text drag-select.
await userEvent.dragAndDrop(second, third);
// A real pointer press, move and release — i.e. a native text drag-select. Driven over
// CDP because `userEvent.dragAndDrop` turns on Playwright's drag interception, which
// reports a drag intent to the page instead of selecting any text.
await dragSelect(second, third);
const selection = editor.model.document.selection;
expect(selection.isCollapsed).toBe(false);
@ -208,3 +210,21 @@ describe("FormatPainterUI", () => {
});
});
});
/**
* Presses at the start of `from`, drags to the middle of `to` and releases, the way a user selects
* text across two elements.
*
* `CDPSession` carries no methods of its own, so the one command this needs is named here.
*/
async function dragSelect(from: Element, to: Element) {
const session = cdp() as { send(method: string, params: Record<string, unknown>): Promise<unknown> };
const start = from.getBoundingClientRect();
const end = to.getBoundingClientRect();
const press = { x: start.left + 2, y: start.top + start.height / 2 };
const release = { x: end.left + end.width / 2, y: end.top + end.height / 2 };
await session.send("Input.dispatchMouseEvent", { type: "mousePressed", button: "left", buttons: 1, clickCount: 1, ...press });
await session.send("Input.dispatchMouseEvent", { type: "mouseMoved", button: "left", buttons: 1, ...release });
await session.send("Input.dispatchMouseEvent", { type: "mouseReleased", button: "left", buttons: 0, clickCount: 1, ...release });
}

View File

@ -1,14 +1,13 @@
import { resolve } from "node:path";
import { webdriverio } from "@vitest/browser-webdriverio";
import { playwright } from "@vitest/browser-playwright";
import { defineConfig } from "vitest/config";
/**
* The browser to drive, when the one webdriverio would fetch for itself cannot run on NixOS the
* downloaded Chrome for Testing dies on a missing `libxcb.so.1`, since nothing outside the store
* provides it. Point `CHROME_BIN` at a system Chrome/Chromium and webdriverio skips the download
* altogether; pair it with `CHROMEDRIVER_PATH` (webdriverio's own variable) for the driver, which
* has the same problem. The Nix dev shell sets both.
* The browser to drive, when the one Playwright downloads for itself cannot run on NixOS the
* bundled Chromium dies on a missing `libxcb.so.1`, since nothing outside the store provides it.
* Point `CHROME_BIN` at a system Chrome/Chromium and Playwright launches that instead. The Nix dev
* shell sets it.
*/
const systemChrome = process.env.CHROME_BIN;
@ -16,12 +15,15 @@ export default defineConfig({
test: {
browser: {
enabled: true,
provider: webdriverio(systemChrome
? { capabilities: { browserName: "chrome", "goog:chromeOptions": { binary: systemChrome } } }
: {}),
provider: playwright({
// Specs assert en-US number formatting (`toLocaleString()` renders 1,234 there and
// 1.234 under a European locale), so the host machine's locale must not decide.
contextOptions: { locale: "en-US" },
...(systemChrome ? { launchOptions: { executablePath: systemChrome } } : {})
}),
headless: true,
ui: false,
instances: [{ browser: "chrome" }]
instances: [{ browser: "chromium" }]
},
include: ["src/**/*.spec.ts"],
setupFiles: ["./test/setup.ts"],

File diff suppressed because it is too large Load Diff