Skip to content

[lexical-extension][lexical-react][lexical-playground] Chore: Add Vitest browser-mode tests via the Playwright runner#8614

Merged
potatowagon merged 7 commits into
facebook:mainfrom
etrepum:claude/epic-ramanujan-eigUY
Jun 3, 2026
Merged

[lexical-extension][lexical-react][lexical-playground] Chore: Add Vitest browser-mode tests via the Playwright runner#8614
potatowagon merged 7 commits into
facebook:mainfrom
etrepum:claude/epic-ramanujan-eigUY

Conversation

@etrepum

@etrepum etrepum commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Description

Our unit tests run under jsdom and lean heavily on hand-written polyfills in vitest.setup.mts (PointerEvent, DataTransfer, ClipboardEvent, execCommand, Range.getBoundingClientRect, contentEditable/Selection behavior, …) to stand in for functionality jsdom doesn't implement. That mocking is fragile and can drift from real browser behavior.

This PR adds a Vitest browser project that runs in a real browser through the Playwright runner, so tests that depend on a real layout/selection engine can be ported out of jsdom. Existing unit tests are untouched.

What's included:

  • Vitest browser project (vitest.config.mts): Playwright provider, headless,
    collecting packages/**/__tests__/browser/**/*.test.{ts,tsx}. The browser set is
    driven by the comma-separated VITEST_BROWSER env var (default chromium), and
    React is deduped via optimizeDeps so browser tests share a single React instance
    with the source-aliased @lexical/react.

  • Scripts: pnpm run test-browser and pnpm run test-browser-watch.

  • CI:

    • Default (every PR/push): linux + chromium, wired into call-core-tests.yml via a
      new reusable call-browser-tests.yml.
    • Extended (tests-extended.yml): a fully-specified include matrix — ubuntu+firefox,
      macOS+{chromium,firefox,webkit}, windows+{chromium,firefox}. ubuntu+chromium is
      already the default job and WebKit only runs on macOS, so no combination is
      generated just to be skipped.
  • Example tests using the extension APIs (not createEditor):

    • lexical-extension/…/BuildEditorFromExtensions.test.ts — builds an editor whose
      contentEditable is created in the extension's afterRegistration phase, and
      asserts real Range.getBoundingClientRect and the Selection API.
    • lexical-react/…/LexicalExtensionComposer.test.tsx — renders a real
      contentEditable and checks real layout.

    Both clean up with onTestFinished(() => editor.dispose()) rather than using:
    Explicit Resource Management (using / Symbol.dispose / Disposable) isn't
    supported in WebKit/Safari yet, so the syntax can't be relied on in browser tests
    (it stays fine in jsdom/Node unit tests). This is documented in AGENTS.md.

  • Dependency bumps: vitest 4.1.5 → 4.1.8 (+ @vitest/browser-playwright,
    playwright); vite 8.0.10 → 8.0.16; @vitejs/plugin-react 6.0.1 → 6.0.2.

  • Playground vite config: the newer vite types overflow TypeScript's recursive
    PluginOption comparison ("Excessive stack depth"). Binding the one array-valued
    plugin entry (react()) to PluginOption[] via a checked annotation lets the
    plugins array infer as PluginOption[] without an as cast.

  • Ignore browser-mode artifact dirs (__screenshots__, .vitest-attachments) and
    document the workflow in AGENTS.md.

Test plan

Before

Behavior that needs a real layout/selection engine (Range.getBoundingClientRect, the
Selection API, contentEditable) could only be tested under jsdom by stubbing it in
vitest.setup.mts; there was no browser-mode harness.

After

pnpm run test-browser runs the new suite in a real browser (default chromium), and
passes on Chromium, Firefox, and WebKit (VITEST_BROWSER=webkit):

 RUN  v4.1.8

 Test Files  2 passed (2)
      Tests  4 passed (4)

pnpm run tsc, eslint, and prettier all pass.

claude and others added 7 commits June 2, 2026 17:12
Adds a Vitest "browser" project that runs in a real browser through the
Playwright provider, so unit tests that lean heavily on jsdom mocks (real
layout, Selection/Range APIs, contentEditable) can be ported to a more
realistic environment.

- Upgrade vitest 4.1.5 -> 4.1.8 and add @vitest/browser-playwright and
  playwright so the whole vitest stack stays on one consistent version.
- New "browser" project in vitest.config.mts: Playwright provider, headless,
  collects packages/**/__tests__/browser/**/*.test.{ts,tsx}. The browser set
  is driven by the comma-separated VITEST_BROWSER env var (default chromium),
  with react()/optimizeDeps tuned so React renders share one React instance.
- New root scripts: test-browser and test-browser-watch.
- CI: default run is linux + chromium (call-browser-tests.yml wired into
  call-core-tests.yml); the extended suite fans out across
  ubuntu/macos/windows x chromium/firefox/webkit.
- Example tests use the extension APIs (buildEditorFromExtensions and
  LexicalExtensionComposer) rather than createEditor.
- Ignore vitest browser artifact dirs (__screenshots__, .vitest-attachments)
  and document the workflow in AGENTS.md.

https://claude.ai/code/session_01C9r2wFA2WU5gsjFHRDmeCz
Keeps the vite side of the test/build stack on the latest releases now that
vitest and the playwright runner are current.

The newer vite type definitions overflow TypeScript's structural comparison
("Excessive stack depth") on the playground's defineConfig call, so annotate
the config function's return as UserConfig and type the plugins array as
PluginOption[]. This is type-only; the resolved config is unchanged.

https://claude.ai/code/session_01C9r2wFA2WU5gsjFHRDmeCz
Replace the os x browser cartesian product with an explicit include list so
the matrix only contains combinations we actually run:

- Drop ubuntu + chromium; it is already covered by the default browser job in
  call-core-tests.yml.
- List WebKit only on macOS instead of generating webkit + linux/windows jobs
  that were always skipped.

With the policy now encoded in the matrix, the webkit-on-macOS guard in
call-browser-tests.yml is redundant, so remove it.

https://claude.ai/code/session_01C9r2wFA2WU5gsjFHRDmeCz
Drop the cleanups array / afterEach teardown in favor of the same
`using` Disposable pattern the unit tests use:

- buildEditorFromExtensions returns a Disposable editor, so each test does
  `using editor = setUpEditor()` and reads the contentEditable back via
  editor.getRootElement() rather than returning it from the helper. document
  .body is reset between tests, so the root element needs no explicit removal,
  and everything stays synchronous (no editor.update/dispatchCommand).
- The React composer test mounts via a small `using view = renderReact(...)`
  Disposable helper that unmounts on scope exit instead of in an afterEach.

https://claude.ai/code/session_01C9r2wFA2WU5gsjFHRDmeCz
The "Excessive stack depth" overflow on the newer vite types comes from the
single array-valued plugins entry (react() returns Plugin[]). Binding it to a
PluginOption[] via a checked annotation — `const reactPlugins: PluginOption[] =
react()` — and spreading it lets the plugins array infer as PluginOption[]
without an `as` cast on the array (every other entry is a Plugin, which is a
PluginOption). This replaces the broad `as PluginOption[]` assertion with a
type-checked widening scoped to the one entry that needs it.

https://claude.ai/code/session_01C9r2wFA2WU5gsjFHRDmeCz
`using`/`Disposable` (Explicit Resource Management) isn't supported in
WebKit/Safari yet — the syntax throws a SyntaxError there, and Vite serves
source untransformed in dev/serve mode, so the browser suite couldn't run on
webkit. Rather than maintain a syntax-lowering plugin plus a Symbol.dispose
polyfill just for the tests, clean up with onTestFinished(() => editor.dispose())
— editor.dispose() is a plain method on the buildEditorFromExtensions result and
works in every engine.

Also document in AGENTS.md that `using`/`Disposable` must not be relied on in
browser tests or any browser-facing code (it stays fine in jsdom/Node unit
tests).

https://claude.ai/code/session_01C9r2wFA2WU5gsjFHRDmeCz
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 2, 2026
@vercel

vercel Bot commented Jun 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment Jun 2, 2026 10:10pm
lexical-playground Ready Ready Preview, Comment Jun 2, 2026 10:10pm

Request Review

@potatowagon

Copy link
Copy Markdown
Contributor

Review from Navi (on behalf of @potatowagon)

This is a well-structured infrastructure PR that adds Vitest browser-mode testing via the Playwright runner. Here's what I verified:

What this PR does:

  • Adds a new Vitest "browser" project that runs tests in a real browser (Playwright) instead of jsdom
  • Introduces pnpm run test-browser / test-browser-watch scripts
  • CI integration: default linux+chromium on every PR, extended matrix (firefox, webkit, macOS, windows) in tests-extended.yml
  • Example tests for buildEditorFromExtensions and LexicalExtensionComposer that exercise real layout/selection APIs
  • Dependency bumps: vitest 4.1.5→4.1.8, vite 8.0.10→8.0.16, @vitejs/plugin-react 6.0.1→6.0.2
  • Playground vite config fix for TypeScript 'Excessive stack depth' overflow with newer vite types
  • Documents using/Disposable limitation in WebKit in AGENTS.md

What I checked:

  1. CI status: All checks pass — browser test (1m21s), unit tests, integrity, e2e canary all green.
  2. Correctness of vitest config: The browser project properly deduplicates React via optimizeDeps.include to avoid the null dispatcher error with hooks.
  3. WebKit compatibility: The using/Disposable issue is properly handled — browser tests use onTestFinished(() => editor.dispose()) instead, well-documented.
  4. CI workflow design: Reusable call-browser-tests.yml with matrix inputs is clean. Extended matrix avoids redundant combinations.
  5. Type safety: The PluginOption[] widening for react() is a correct, minimal fix.
  6. Test quality: Both example tests assert meaningful real-browser behavior that jsdom cannot provide.
  7. No regressions: Existing unit tests are completely untouched. The new project is additive.

Verdict: Safe to approve. Clean infrastructure work from a maintainer with comprehensive CI coverage already green. No behavioral changes to existing code.

— via Navi on behalf of @potatowagon

@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Jun 2, 2026
@potatowagon potatowagon added this pull request to the merge queue Jun 3, 2026
Merged via the queue into facebook:main with commit 68f5ac9 Jun 3, 2026
50 checks passed
mayrang added a commit to mayrang/lexical that referenced this pull request Jun 21, 2026
…anup convention

Convert 10 inline `if (!SUPPORTS_COMPOSED_RANGES) { return }` guards to
test.skipIf so unsupported browsers show "skipped" instead of false "passed"
and avoid unnecessary setUpShadowEditor setup/teardown.

Switch useDynamicPositioning regression test from manual removeChild cleanup
to onTestFinished, matching the pattern used in ShadowRootSelection tests
and LexicalExtensionComposer (PR facebook#8614).
@etrepum etrepum mentioned this pull request Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

3 participants