Skip to content

feat(racetrack): a hosted playground and scenario-driven test runner for PTE plugins#2646

Closed
christianhg wants to merge 3 commits into
mainfrom
feat/racetrack
Closed

feat(racetrack): a hosted playground and scenario-driven test runner for PTE plugins#2646
christianhg wants to merge 3 commits into
mainfrom
feat/racetrack

Conversation

@christianhg
Copy link
Copy Markdown
Member

Racetrack is a hosted playground and scenario-driven test runner for Portable Text Editor plugins. Wire your plugin in, drive it with scenarios you author visually, watch them go green. The same .feature file runs in Racetrack playback AND in the customer's vitest CI - identical results.

This is the long-running branch where Racetrack is built inside the editor monorepo while PTE v7 stabilises. It will not be merged in this shape. Platform fixes that fall out (one already has, #2644) get split into their own PRs and merged independently. Each push to this branch generates a Vercel preview at https://racetrack.sanity.dev/ so the work is continuously testable.

What's here today

The app at apps/racetrack/ is private, runs against the monorepo's workspace sources, and currently boots a fixed garage of two cars:

  • Mention picker - @portabletext/plugin-typeahead-picker's mention picker scenarios, lifted verbatim from packages/plugin-typeahead-picker/src/mention-picker.test.tsx. 19 scenarios, all green.
  • Input rule edge cases - @portabletext/plugin-input-rule's edge-case scenarios, lifted verbatim from packages/plugin-input-rule/src/edge-cases.test.tsx. 57 scenarios, all green, matching the vitest CI baseline exactly.

The same .feature files run unmodified in their packages' vitest browser tests; the runner here just provides the playback and visualisation surface.

How it works

Four panels. Scenarios on the left (parsed via @cucumber/gherkin, scenario outlines expanded into individual pickles). A live editor in the middle with the car's plugin wired in. A second editor on the right that each scenario drives via racejar's runner-agnostic compileFeature. An engine panel surfaces the car's source as read-only tabs - Plugin.tsx, entry.tsx, steps.ts, feature.feature - imported via Vite's ?raw suffix at build time.

A car lives at src/garage/<name>/ with a fixed shape: an entry.tsx that exports a GarageEntry manifest pairing the feature file with the React plugin, step definitions, hooks, and parameter types. The App reads from src/garage/index.ts and a dropdown switches cars.

The runner re-implements racejar's vitest driver as 49 lines of plain async iteration, then aliases vitest, vitest/browser, and vitest-browser-react to in-app shims so existing step definitions (@portabletext/editor/test/vitest) and createTestEditor import cleanly outside a vitest worker. The shims diverge from vitest in two places, both deliberate:

  • Locator.locator(selector) and page.locator(selector) are added so step definitions can scope assertions to the test target rather than picking up the playground editor that shares the DOM.
  • @testing-library/user-event's .type() is invoked with {skipClick: true} so typing into a contenteditable doesn't collapse the existing selection - matching Playwright's page.keyboard.type() behaviour used by vitest's browser runner.

Both divergences were validated by running the same .feature files in vitest CI and confirming identical pass counts.

What comes next

The current cars validate the runtime; the next chunks are scenario authoring inside the app (drag-from-palette + AI-assisted drafting), exporting .feature files back to the consumer's repo, and the structured-lists plugin as the dogfood target.

Brief at /project-brief.md. Architecture and design at /specs/racetrack*.md.

…red editor

`createTestEditor` from `@portabletext/editor/test/vitest` discarded the scoped locator from its underlying `render()` and built a document-wide `page.getByRole('textbox')` lookup. Two calls in the same DOM would have the second one's locator address the first editor. Vitest's iframe-per-test isolation hid this; embedders that share a DOM across editors hit it.

Use the scoped locator from `render()` so each editor's locator targets exactly that editor.

Add a regression test that mounts two editors, types different text into each, asserts each editor ended up with its own content.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 11, 2026

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

Project Deployment Actions Updated (UTC)
portable-text-example-basic Ready Ready Preview, Comment May 11, 2026 3:57pm
racetrack Ready Ready Preview, Comment May 11, 2026 3:57pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
portable-text-editor-documentation Skipped Skipped May 11, 2026 3:57pm
portable-text-playground Skipped Skipped May 11, 2026 3:57pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 11, 2026

🦋 Changeset detected

Latest commit: 1ceae1d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@portabletext/editor Patch
racetrack Patch
@portabletext/plugin-character-pair-decorator Patch
@portabletext/plugin-emoji-picker Patch
@portabletext/plugin-input-rule Patch
@portabletext/plugin-markdown-shortcuts Patch
@portabletext/plugin-one-line Patch
@portabletext/plugin-paste-link Patch
@portabletext/plugin-sdk-value Patch
@portabletext/plugin-typeahead-picker Patch
@portabletext/plugin-typography Patch
@portabletext/toolbar Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

ghost commented May 11, 2026

📦 Bundle Stats — @portabletext/editor

Compared against main (52da58bb)

@portabletext/editor

Metric Value vs main (52da58b)
Internal (raw) 743.8 KB -298 B, -0.0%
Internal (gzip) 142.8 KB -74 B, -0.1%
Bundled (raw) 1.35 MB -298 B, -0.0%
Bundled (gzip) 303.8 KB -57 B, -0.0%
Import time 99ms +1ms, +0.7%

@portabletext/editor/behaviors

Metric Value vs main (52da58b)
Internal (raw) 467 B -
Internal (gzip) 207 B -
Bundled (raw) 424 B -
Bundled (gzip) 171 B -
Import time 3ms +0ms, +0.3%

@portabletext/editor/plugins

Metric Value vs main (52da58b)
Internal (raw) 3.3 KB -298 B, -8.1%
Internal (gzip) 948 B -73 B, -7.1%
Bundled (raw) 3.1 KB -298 B, -8.6%
Bundled (gzip) 878 B -74 B, -7.8%
Import time 8ms -0ms, -0.6%

@portabletext/editor/selectors

Metric Value vs main (52da58b)
Internal (raw) 76.3 KB -
Internal (gzip) 14.3 KB -
Bundled (raw) 72.4 KB -
Bundled (gzip) 13.3 KB -
Import time 8ms -0ms, -1.1%

@portabletext/editor/traversal

Metric Value vs main (52da58b)
Internal (raw) 9.2 KB -
Internal (gzip) 2.4 KB -
Bundled (raw) 9.3 KB -
Bundled (gzip) 2.4 KB -
Import time 5ms +0ms, +4.0%

@portabletext/editor/utils

Metric Value vs main (52da58b)
Internal (raw) 30.6 KB -
Internal (gzip) 6.5 KB -
Bundled (raw) 28.4 KB -
Bundled (gzip) 6.1 KB -
Import time 7ms +0ms, +1.6%

🗺️ . · ./behaviors · ./plugins · ./selectors · ./traversal · ./utils · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 11, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​testing-library/​user-event@​14.6.110010010086100

View full report

Layer the runtime on top of the skeleton landing page.

Four-panel app: scenarios on the left (driven by Cucumber's gherkin parser
to expand outlines into pickles), playground in the middle (a live editor
with the car's plugin wired in), runner on the right (a second editor that
each scenario drives via racejar's runner-agnostic `compileFeature`), and
an engine panel that shows the car's source - `Plugin.tsx`, `entry.tsx`,
`steps.ts`, `feature.feature` - read-only via Vite's `?raw` imports.

A car lives at `src/garage/<name>/` with a fixed shape: `entry.tsx`
exports a `GarageEntry` manifest pairing a feature file with the React
plugin, step definitions, hooks, and parameter types. The App reads from
`src/garage/index.ts`; a dropdown switches cars.

Two cars in the garage:

- `mention-picker` (19/19 scenarios). Lifted from
  `packages/plugin-typeahead-picker/src/mention-picker.test.tsx` with no
  changes to scenarios; only the test-target wiring moves.
- `input-rule-edge-cases` (57/57 scenarios). Lifted from
  `packages/plugin-input-rule/src/edge-cases.test.tsx`; same scenarios,
  matches vitest CI pass count exactly.

The runner re-implements racejar's vitest driver as 49 lines of plain
async iteration, then aliases `vitest`, `vitest/browser`, and
`vitest-browser-react` to in-app shims so existing step definitions
(`@portabletext/editor/test/vitest`) and `createTestEditor` import
cleanly without a vitest worker. The shims diverge from vitest in two
places, both deliberate:

- `Locator.locator(selector)` and `page.locator(selector)` are added
  so step definitions can scope assertions to the test target rather
  than picking up the playground editor that shares the DOM.
- `@testing-library/user-event`'s `.type()` is invoked with
  `{skipClick: true}` so typing into a contenteditable doesn't collapse
  the existing selection - matching Playwright's
  `page.keyboard.type()` behaviour used by vitest's browser runner.

Knip is told the shim modules are entry points (their exports are
consumed indirectly through Vite aliases).
`getGlobalScope` had a chain - `globalThis` first, then `window`, `self`,
`global`. `globalThis` is universal in every environment the editor
supports (browsers, Node 12+, workers, Deno, Bun), so the rest of the
chain is unreachable.

Beyond being dead, the `global` branch needs `@types/node` in scope to
type-check. Consumers whose tsconfig doesn't pull Node types - any app
that aliases `@portabletext/editor` directly to its `src/` - tripped
on it. Simplify to a single `globalThis` read.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant