feat(racetrack): a hosted playground and scenario-driven test runner for PTE plugins#2641
Closed
christianhg wants to merge 7 commits into
Closed
feat(racetrack): a hosted playground and scenario-driven test runner for PTE plugins#2641christianhg wants to merge 7 commits into
christianhg wants to merge 7 commits into
Conversation
🦋 Changeset detectedLatest commit: 04957f5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
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 |
Contributor
📦 Bundle Stats —
|
| Metric | Value | vs main (ab44043) |
|---|---|---|
| Internal (raw) | 744.1 KB | - |
| Internal (gzip) | 142.8 KB | - |
| Bundled (raw) | 1.35 MB | - |
| Bundled (gzip) | 303.8 KB | - |
| Import time | 97ms | -0ms, -0.3% |
@portabletext/editor/behaviors
| Metric | Value | vs main (ab44043) |
|---|---|---|
| Internal (raw) | 467 B | - |
| Internal (gzip) | 207 B | - |
| Bundled (raw) | 424 B | - |
| Bundled (gzip) | 171 B | - |
| Import time | 3ms | +0ms, +2.0% |
@portabletext/editor/plugins
| Metric | Value | vs main (ab44043) |
|---|---|---|
| Internal (raw) | 3.6 KB | - |
| Internal (gzip) | 1021 B | - |
| Bundled (raw) | 3.4 KB | - |
| Bundled (gzip) | 952 B | - |
| Import time | 8ms | +0ms, +0.6% |
@portabletext/editor/selectors
| Metric | Value | vs main (ab44043) |
|---|---|---|
| Internal (raw) | 76.3 KB | - |
| Internal (gzip) | 14.3 KB | - |
| Bundled (raw) | 72.4 KB | - |
| Bundled (gzip) | 13.3 KB | - |
| Import time | 8ms | -0ms, -0.9% |
@portabletext/editor/traversal
| Metric | Value | vs main (ab44043) |
|---|---|---|
| Internal (raw) | 9.2 KB | - |
| Internal (gzip) | 2.4 KB | - |
| Bundled (raw) | 9.3 KB | - |
| Bundled (gzip) | 2.4 KB | - |
| Import time | 5ms | -0ms, -0.4% |
@portabletext/editor/utils
| Metric | Value | vs main (ab44043) |
|---|---|---|
| Internal (raw) | 30.6 KB | - |
| Internal (gzip) | 6.5 KB | - |
| Bundled (raw) | 28.4 KB | - |
| Bundled (gzip) | 6.1 KB | - |
| Import time | 6ms | -0ms, -0.5% |
🗺️ . · ./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.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Racetrack is the hosted playground and scenario-driven test runner for Portable Text Editor plugins. Customers wire their plugin into Racetrack, drive it with scenarios authored visually, watch them go green. The same `.feature` file runs in Racetrack playback AND in the customer's vitest CI - identical results. This v0 POC is hard-coded to the mention-picker plugin and exists to prove the loop end-to-end across three panels: - Scenarios (left): `mention-picker.feature` rendered with one status dot per step; idle, running, pass, or fail. After a run the failing step's error message paints in red. - Playground (centre): a live `EditorProvider` with the mention-picker wired in. Type `@` to trigger the picker for real. - Runner (right): a "Run scenarios" button that instantiates a second, isolated editor inside the panel and drives it through every scenario via `createTestEditor` from `@portabletext/editor/test/vitest`. Each scenario's Before hook spins up a fresh editor; pass/fail results paint back into the left panel. The interesting engineering is the shim. Step definitions in `packages/editor/src/test/vitest/step-definitions.tsx` import `userEvent`, `page`, and `expect.element` from `vitest/browser`, plus `vi.waitFor`/`expect`/`assert` from `vitest`. `createTestEditor` imports `render` from `vitest-browser-react`. All three are virtual modules vitest's runtime injects only inside the test worker. Racetrack does not run inside vitest, so `vite.config.ts` aliases `vitest/browser`, `vitest`, and `vitest-browser-react` to local files in `src/runner/`. The shims implement just enough surface to cover the call sites in `step-definitions.tsx` and `test-editor.tsx`, wrapping `@testing-library/user-event` for keyboard / click / type and a tiny `expect` polyfill for the value and element matchers used. Anything not yet covered throws a loud error so the next missing surface shows up immediately. The same source code therefore runs in two contexts: in the customer's vitest CI it resolves to vitest's real runtime, in Racetrack to our shims. Identical step definitions, identical assertions, two execution environments.
…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.
1b383f1 to
4d54754
Compare
The runner mounts a second editor inside `<div data-racetrack-test-target />` alongside the playground editor. Both editors render `<MentionPickerPlugin>`, so both emit `data-testid="keyword"`, `"matches"`, `"state"`, `"selectedIndex"` into the DOM. `attachLocators` used `page.getByTestId(...)` unscoped, which matched the playground's plugin (higher in document order) and read its state instead of the runner's, so every state-reading scenario failed even though the keyboard was reaching the test-target editor.
Scope the four locators to `page.locator('[data-racetrack-test-target]')` so each query starts inside the runner's container.
Add `locator(selector)` to the `vitest/browser` shim's `Locator` and `page` surfaces; it was the last missing primitive needed by this scoping pattern.
19/19 mention-picker scenarios pass.
Restructure racetrack to load any plugin by id from a small registry.
Each entry owns its feature file, plugin component, hook construction,
and any plugin-specific steps. The runner panel is now generic over
entry; the playground panel renders the entry's component inside an
`<EditorProvider>`.
The header shows the current entry and an entry picker dropdown.
Switching entries resets the run state. The right-side panel reads
`feature.scenarios.length` from the parsed feature, but parsed now
goes through `@cucumber/gherkin` directly so Scenario Outlines expand
one pickle per Examples row, matching what `compileFeature` produces.
This keeps the left panel's index-per-step in lockstep with the
runner's `onStep` callbacks.
Add a second entry: input-rule edge cases, lifted from
`packages/plugin-input-rule/src/edge-cases.test.tsx`. Nine text
transform rules sharing one editor, declarative steps only, no custom
locator scoping. 51 of 57 scenarios green; 6 scenarios touching
inline objects diverge from vitest CI (57/57 there). Known limitation
in racetrack's textspec / userEvent interaction around `{stock-ticker}`
inline objects; tracked separately.
Vocabulary aligned to the racing metaphor: 'Start race' / 'Racing...'
button, 'Track' for the scenarios panel, 'Paddock' for the playground,
'Race control' for the runner, 'finished/crashed' for pass/fail.
`@testing-library/user-event`'s `.type(element, text)` clicks the
element first before typing, which collapses the contenteditable
selection to wherever the click lands. `vitest/browser`'s
`userEvent.type` and Playwright's `page.keyboard.type` type at the
current selection without re-clicking.
PTE step definitions like `the caret is put after "c"` set the
selection via `editor.send({type: 'select', ...})` and then call
`userEvent.type(locator, text)` expecting the next keystroke to land
at the placed caret. With auto-click that selection got blown away
and typing always landed at the end of the editor, which broke every
scenario that mixed programmatic caret placement with inline objects.
Pass `skipClick: true` and only focus the element if it isn't already
the active element. Closes the gap: input-rule edge-cases scenarios
that touch `{stock-ticker}` inline objects now match vitest CI
(57/57).
…le deps Adds `@cucumber/gherkin`, `@cucumber/messages`, and `@portabletext/plugin-input-rule` to `apps/racetrack`'s installed tree. `@cucumber/*` were transitive via racejar; promoting them to direct deps so racetrack's feature-parser can use them without violating the monorepo's transitive-import discipline.
Adds a fourth panel between Paddock and Race control that shows the car's source files in a read-only viewer with a tab strip per file. Each garage entry now declares an `engine` field: an array of `{name, language, source}` entries loaded via `?raw` imports at build time, so the viewer shows exactly what is on disk without bundling a syntax-highlighting server.
mention-picker exposes Plugin.tsx, entry.tsx, steps.ts, feature.feature. input-rule edge-cases exposes Plugin.tsx, entry.tsx, feature.feature.
Tab switching across files works without round-trips; switching cars rebuilds the tab strip from the new entry's engine array.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
.featurefile 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.
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:@portabletext/plugin-typeahead-picker's 19 scenarios, lifted as-is.@portabletext/plugin-input-rule's 57 expanded scenarios across nine text-transform rules sharing one editor.Both run end-to-end and match their vitest CI baseline exactly (19/19 and 57/57).
Four panels
.featurefile rendered with one status dot per step. Idle, running, pass, fail.Plugin.tsx,entry.tsx,steps.ts,feature.feature) in a read-only tab strip.Switching cars from the header dropdown swaps the playground component, the engine source, the feature file, and the runner's hooks together.
How it actually runs in the browser
The step library at
packages/editor/src/test/vitest/step-definitions.tsximportsuserEvent,page, andexpect.elementfromvitest/browser, plusvi.waitFor/expectfromvitest.createTestEditorimportsrenderfromvitest-browser-react. Those are virtual modules vitest's runtime injects only inside the test worker, so racetrack ships its own shims atapps/racetrack/src/runner/that implement just enough surface to drive the editor through scenarios in the browser:vitest-browser-shim.ts- wraps@testing-library/user-eventfor keyboard/click/type, plus minimalpageandLocator.vitest-shim.ts-vi.waitForand the smallexpectpolyfill used by the editor matchers.vitest-browser-react-shim.ts- arender()that uses React 19'screateRootand scopes the container to[data-racetrack-test-target].vite.config.tsaliasesvitest/browser,vitest, andvitest-browser-reactto these shims. Step definitions andcreateTestEditorcompile and run unchanged; the same code therefore runs in two contexts - in CI it resolves to vitest's real runtime, in Racetrack to the shims.The runner re-implements racejar's vitest gherkin driver in 163 lines at
src/runner/browser-runner.ts, emittingonStepcallbacks the UI subscribes to.compileFeaturefrom racejar's root export is runner-agnostic and does the heavy lifting.Surfacing real platform gaps
Racetrack has already produced two upstream fixes worth landing on their own:
createTestEditor's returned locator was unscoped - it discardedrender()'s scoped locator and built a document-widepage.getByRole('textbox')lookup. Hidden by vitest's iframe-per-test isolation; surfaced when two editors share a DOM. Split out as #2644.userEvent.typein this branch's shim collapsed contenteditable selections -@testing-library/user-event's.type()clicks first, which thevitest/browserand Playwright equivalents do not. Six input-rule scenarios that combined programmatic caret placement (the caret is put after "c") with typing failed in racetrack while passing in vitest CI. Fixed inapps/racetrack/src/runner/vitest-browser-shim.ts(this branch's242c2cef9).Garage shape
Adding a new car is three files plus a line in
index.ts. The runner panel is generic overGarageEntry; the playground panel rendersentry.PlaygroundComponent; the engine panel readsentry.engine(an array of{name, language, source}objects loaded via?rawat build time).