describe(me) docs
early v0.12.1 browser mode · jsdom Live · Foundations
Examples npm GitHub

How it works

Recording happens in the test, writing in a Vitest reporter, replay in a static viewer. This page follows one test through all three.

Architecture #

The plugin sets up both sides once. In the test, the render adapter and the patched interactions call the recorder, which hands its frames to the reporter in Node through task.meta. The reporter writes .describe-me, which describe-me dev and describe-me build read. @describe-me/vitest/plugin at config time: setup file, reporter, render redirect setupFiles · resolveId reporters In the test · browser-mode iframe or jsdom Render adapter @describe-me/react Patches and hooks @describe-me/vitest/setup or …/setup-dom beforeEach · afterEach Recorder @describe-me/core settles, then serializes the DOM rrweb-snapshot task.meta.describeMe In Node Reporter @describe-me/vitest/reporter writes .describe-me/ manifest.json snapshots/ styles/ assets/ served, watched copied as __data/ Viewer · describe-me describe-me dev watches manifest.json describe-me build a static site set up once, at config time calls and files, in every run the hand-over to Node
The plugin sets up both sides once. After that, every frame goes from the recorder in the test to the reporter in Node, which writes plain files for the viewer.

What each part does, one level below the overview:

One test, step by step #

This is the test Menu > selects an item and reports it of the browser-mode example. It renders a menu, opens it inside a step() and clicks an item.

The hooks begin the recording. The render adapter and each patched interaction call capture on the recorder, and step captures after its body. After the test, the hooks flush, take the closing frame, end the recording, hand the frames to the reporter through task.meta and tear down. The reporter builds the test entry when the module ends and writes the manifest when the run ends. Test Menu.test.tsx Adapter or patch render · userEvent Recorder @describe-me/core Hooks setup file Reporter in Node beforeEach · begin() render(<Menu />) capture('render') renderFrame decides when userEvent.click(…) beforeInteraction() capture('action', label) settle, then serialize step(label, fn) fn, then capture('step') afterEach · flush() capture('end') unless flush() took one end() compacts the frames TestRecord task.meta.describeMe only with frames teardown() adapters unmount onTestModuleEnd test entries and snapshot files onTestRunEnd clean-up, then manifest.json
Solid arrows are calls inside the test. The accent arrow is the only way a frame leaves the test: Vitest sends task.meta to Node.
  1. Before the test The setup file's beforeEach calls recorder.begin() when the test file is one the plugin records. A concurrent test is not recorded, because it would share the one recorder of the page. The hook prints a warning once.
  2. Render The adapter calls the original render, names the component and its props with setComponent(), and calls capture('render'). The renderFrame option decides when that frame is taken: right away, before the next interaction, or once a loader is gone. See Render frame timing.
  3. Interactions Each patched interaction calls beforeInteraction(), which takes a render frame still held back. Then the interaction runs, the wrapper reports what it did to hover and focus with noteGesture(), and capture('action', label) settles and serializes. Settling waits one macrotask on real timers, which is enough for React to commit, also under fake timers. It does not wait for requestAnimationFrame, because a frame holds the DOM and its styles, not layout. In jsdom the wait runs with React's act flag off, as Testing Library's own waits do. User-event in jsdom under @testing-library/react skips the settle.
  4. step() It takes a held-back render frame, runs its body and captures after it. When the body already produced this exact DOM, as the click does here, the step only names that frame: open the menu · click(button).
  5. After the test The afterEach hook calls flush(), which waits for the captures still in flight and takes a held-back render frame. Unless it took one, the hook calls capture('end'), which costs nothing when the test recorded no frame. end() compacts the frames. A closing frame equal to the frame before it is dropped, and a step equal to the frame before it names that frame. The record goes into task.meta.describeMe only when there are frames. Then teardown() lets the adapters unmount.
  6. In Node When a test file ends, the reporter's onTestModuleEnd builds the entry of each recorded test and writes its snapshots. When the run ends, onTestRunEnd reads the props of the components, deletes the files no frame needs, writes foundations.json and then manifest.json.

Every begin() and end() starts a new generation of the recorder. An interaction reads it when it starts, so a capture that finishes after its test ended is dropped.

In jsdom, Testing Library runs render, rerender and fireEvent inside act(), so the DOM is committed when they return. Their frames are taken right away, without settling, before the test's next line can change the DOM. So are user-event frames, because user-event runs each call through the asyncWrapper that Testing Library configures, which drains a macrotask before the call resolves. The setup file knows Testing Library is loaded through a check that @describe-me/react/testing-library publishes on globalThis when it loads. Without that check, user-event frames settle.

Open the step frame of this test in the browser-mode example. Writing stories lists which frames are taken, and which tests are left out.

Intercepting interactions #

Your tests stay as they are. describe-me reaches into them in two ways, both set up by the plugin.

The render redirect

The plugin's resolveId hook runs before other plugins (enforce: 'pre'). When a module imports vitest-browser-react in browser mode, or @testing-library/react in a DOM environment, it resolves @describe-me/react or @describe-me/react/testing-library instead. Imports from the adapter's own files keep the original, because the adapter wraps it. The hook knows them by the root of the installed adapter package.

When the adapter cannot be resolved, the import fails. Falling back to the original module would look like success and record nothing. In jsdom every test would also stay mounted, because Testing Library's own cleanup is off.

describe-me: cannot resolve @describe-me/react (the recording replacement for vitest-browser-react). Install @describe-me/react.

In browser mode the plugin also lists the adapter and the original module in optimizeDeps.include. Vite's dependency scanner runs before resolveId and never sees the adapter. Finding it mid-run makes Vite optimize again and reload, and the test ends up with two copies of the bundled dependencies ("Vitest failed to find the runner"). Listing both up front keeps one optimization pass.

Patches at the source

Interactions do not go through the adapter, so the setup file wraps them where they are defined, once per test file:

Each wrapper runs the original through record-action.ts: take a held-back render frame, run the interaction, report what it did to hover and focus with recorder.noteGesture(), then capture. Reported after the interaction, a gesture never shows in the render frame before it, and one that throws changes nothing. The wrapper counts how deep the calls are nested in the current test, so a gesture that delegates to another records one frame, for the outermost call. Every call reports its gesture, so the outermost one is reported last. userEvent.click() delegates to the locator's click(), and the direct API of user-event delegates to an instance. A marker on each wrapper keeps a second patch from wrapping it again.

In jsdom, fireEvent is not patched. The adapter exports its own, which records one frame per call. Its enter and over events report a hover with recorder.noteGesture() too, and its leave and out events the end of one, after the event and before its frame. Writing stories lists the methods that record, and What the plugin changes lists every other change to your config.

What a frame holds #

A frame (Frame in @describe-me/core/types) holds:

id
f0, f1 and so on, in the order the test asked for them.
kind
render, action, step or end.
label
What the timeline shows, such as click(menuitem "Rename").
at
When the snapshot was taken, in ms since the test began, on real timers.
meta
Render frames carry the props they were rendered with.
snapshot
The whole document, serialized to JSON text.

rrweb-snapshot serializes the document into a tree of nodes, with the text of every stylesheet inlined as _cssText. It never sees document.adoptedStyleSheets, which Lit and other web component libraries use. So materialize-adopted-styles.ts copies their rules into a temporary <style> for the length of each capture. It does this for the document, not for shadow roots.

Right before it serializes, the recorder reads which elements are in :hover, :focus, :focus-visible and :focus-within. In browser mode the browser decides, and in a DOM environment the gestures the adapters reported, as Environments explains. serialize-document.ts finds the nodes of those elements through rrweb's mirror and writes their states into the serialized tree as data-describe-me-states, such as focus focus-visible focus-within. The page never gets the attribute, so neither the test nor a component's MutationObserver sees a change.

rrweb numbers the nodes from a counter that only cleanupSnapshot() resets. serialize-document.ts calls it before every capture, so the ids start at 1 each time and identical DOM gives identical text. That is what lets the reporter store it once.

Before it stores a snapshot, the reporter removes what names the machine that recorded it. Every <script> goes, wherever it sits, because the viewer never runs one. Vite marks each <style> it injects with data-vite-dev-id, the absolute path of the file. That path becomes relative to the root, such as src/styles.css, or starts at the last node_modules/ for a file outside the root. Any other file outside the root loses the attribute. Values that the tests put into a frame, such as props, labels and the text they render, stay as they are.

A frame has no layout and no screenshot. The viewer's browser lays the DOM out again from its styles, at the width you pick.

The output directory #

The reporter writes everything the viewer shows into one directory, .describe-me/ unless outDir says otherwise:

.describe-me/
  manifest.json
  foundations.json
  snapshots/<hash>.json
  styles/<hash>.css
  assets/<hash>.<ext>

Every file but manifest.json and foundations.json is named after the first 16 hex characters of a SHA-1 of its content. Identical content is written once, however many frames, tests and runs produce it.

For every frame, in this order, the reporter:

  1. Copies the project files the snapshot points at into assets/. rrweb made their URLs absolute against the test page, whose server is gone after the run. A URL that names a file in the project root or its public/ folder becomes describe-me-asset: and the stored name. A CSS file is copied with the fonts, images and stylesheets it points at.
  2. Moves every stylesheet of 256 characters or more to styles/. The sheet is cut between top-level rules into chunks of about 4 KB, and the content of the rules decides where a chunk ends. So a sheet that grows from test to test shares most of its chunks. The snapshot keeps describe-me-style:<hash>+<hash>….
  3. Removes every <script> and writes data-vite-dev-id relative to the root, as What a frame holds says.
  4. Writes the snapshot to snapshots/, named after the hash of the cleaned text.

Assets come first, so the stored stylesheets point at the copied files too.

After every run, garbage collection deletes the snapshots no frame points at, then the style chunks no kept snapshot refers to, then the assets that nothing kept refers to: no snapshot, chunk, preview head or kept CSS file. Folders inside the three directories are left alone.

Then the reporter writes foundations.json from every kept snapshot, those of skipped tests included. It holds the design systems that package.json and components.json name, the recorded stylesheets with custom properties on a root-like selector or with @font-face, the project CSS files the frames load, the Tailwind 4 theme and the tokens of the theme module. A stored sheet keeps its describe-me-style: reference. The file is absent when the run found no token, no problem and no Material UI, and with foundations: false. describe-me build copies it and keeps its chunk references in step when it vendors fonts. The viewer fetches it only when Foundations opens.

A filtered run, such as vitest run Button.test.tsx, starts from the previous manifest. Modules whose file is gone or no longer recorded are dropped, the rest stay. A test skipped in the run keeps its previous entry. Every module, kept or new, gets its sidebar group from this run's options.

describe-me build copies the directory into the site as __data/, writes an id of the project as root in the copy of the manifest and downloads web fonts into the copy. The directory itself never changes. Styles and fonts says which files are copied, Vendoring fonts how fonts are downloaded, and Performance how large the output gets.

The manifest #

manifest.json is the index of the directory: interface Manifest in @describe-me/core/types. The reporter writes it at the end of every run. These are its fields:

Field Type Absent What it holds
version 1 never The format of the manifest. Every release so far writes 1.
generator ManifestGenerator before 0.5.1 The package and version that wrote the manifest, such as @describe-me/vitest and its version. Also absent when the reporter cannot read its own package.json. Versions explains what the viewer does with it.
generatedAt string never When the run ended, as an ISO 8601 date. The viewer repaints only when it changes.
root string never The Vitest root of the run, as an absolute path. Module ids and component files are relative to it. In the copy that describe-me build writes into a site, it is an id of the project instead: the first 16 hex characters of a SHA-1 of the path. The viewer keeps the collapsed rows and the sidebar width per root, so two sites on one origin keep theirs apart.
configFile string before 0.6, and when the run had no config file The Vitest config file of the run, relative to root, with / separators, such as vitest.config.ts. It always comes from this run. The live preview loads the same file.
project string before 0.10 The name of the project: the name of the nearest package.json at or above the root, else the root directory's name. The viewer's header and tab title show it; without it the header shows the describe(me) wordmark.
modules ManifestModule[] never The recorded test files, sorted by path, each with its sidebar group when it has one, its tests and their frames. Their shape is below the table.
sidebar { groups: string[][] } before 0.12, and without groups The sidebar groups that hold a test file, directly or in a subgroup, each as its path from the outermost group, such as ["Components", "Inputs"]. They are in sidebar order: depth first, each group before its subgroups. Also absent when no file has a group.
components Record<string, ComponentDoc> never The props of the components the tests rendered, read from their TypeScript types and keyed by the name the adapter gave each one. A component whose props cannot be read is missing here, and the diagnostics name it.
assetsMissing string[] before 0.4 Paths that snapshots, the preview head or copied CSS files point at but that do not exist, so they will not load in the viewer. They are root-relative inside the root, such as /fonts/gone.woff2. A path outside the root starts at its last node_modules/, such as node_modules/pkg/logo.svg, or else is relative to the root, such as ../shared/gone.woff2. Only what this run found.
head string no previewHead The preview head HTML, with project files as describe-me-asset: URLs.
fontsMissing FontMissing[] before 0.5 Font families that frames use but nothing loads, most tests first. Each names the first test that misses it. A missing list counts as empty.
remoteStylesheets RemoteStylesheet[] before 0.5 Hosts that frames load stylesheets from, most frames first. A missing list counts as empty.
setupWarnings string[] before 0.5.1 Problems the plugin or the reporter found in the project's setup, one sentence each, such as a sidebar group that gets no test file. They come from this run only, never from the previous manifest. Absent when there are none, which counts as empty.
foundations ManifestFoundations before 0.9 What foundations.json next to the manifest holds: the ids of the design systems, how many distinct token names the token sheets, the Tailwind theme and the theme module have together, and the problems while collecting, one sentence each. Also absent with foundations: false and when the run found no token, no problem and no Material UI.

The nested shapes, from the outside in:

manifest.json, one test of the browser-mode example, shortened

{
  "id": "src/Menu.test.tsx",
  "group": ["Components"],
  "tests": [
    {
      "id": "d5b51b54fed9",
      "vitestId": "99171413_0_1",
      "name": "selects an item and reports it",
      "path": ["Menu"],
      "fullName": "Menu > selects an item and reports it",
      "state": "passed",
      "component": { "name": "Menu", "file": "src/Menu.tsx", "props": { … } },
      "frames": [
        { "id": "f0", "kind": "render", … },
        {
          "id": "f1",
          "kind": "step",
          "label": "open the menu · click(button)",
          "at": 49,
          "snapshot": "snapshots/4c0bbe491dd8a500.json"
        },
        { "id": "f2", "kind": "action", … }
      ]
    }
  ]
}

ManifestDiagnostics is not stored. manifestDiagnostics() computes it from the manifest, for the reporter's summary and the viewer's header. Troubleshooting explains each key.

How the viewer replays a frame #

The viewer is a static page that fetches __data/manifest.json. It hides tests that recorded nothing but a closing frame, which older versions still wrote.

Under describe-me dev, the viewer's Vite config serves the data directory as __data/ and watches it. When manifest.json changes, it sends a describe-me:update event through Vite's HMR socket, and the viewer fetches the manifest again. A static site has no such socket, so the viewer polls every 60 seconds instead. Either way it repaints only when generatedAt changed.

To show a frame, the viewer:

  1. Fetches the snapshot and makes every describe-me-asset: URL absolute against __data/assets/. The frame replays in an iframe that has no base URL of its own.
  2. Joins each stored stylesheet back from its chunks in __data/styles/. A chunk that fails to load is tried once more after half a second. If it still fails, the sheet keeps a gap and counts as incomplete, and the frame loads again the next time it is shown.
  3. Has rrweb rebuild the DOM into a fresh iframe, sandboxed with allow-same-origin only, so no script ever runs. A :where(:root) { color-scheme: light dark } rule goes first in its <head>, then the preview head, so the captured styles win ties. The rule gives the page the iframe's color scheme, which comes from the background switch, and under auto from the page or the viewer theme.
  4. In a frame with states, gives each marked element the classes :hover, :focus, :focus-visible and :focus-within of the states it had. Each rule that names a state is rewritten to match the class as well, nested rules included: .btn:hover becomes .btn:is(:hover, .\:hover), in place, so :not(:focus-visible) reads the class too. The rewrite leaves a state after a pseudo-element as it is, as in ::part(label):focus-visible. A rule with a hover after a pseudo-element never gets that far: rrweb's own copy of it, such as ::-webkit-scrollbar-thumb.\:hover, is invalid, so the browser drops the whole rule while rrweb builds the frame, as before 0.12. The real states still match, so your own pointer over the frame hovers it. Sheets from another origin stay as they are. The browser's default focus ring comes first in <head>, in a cascade layer that every page rule beats. A frame without states replays as rrweb builds it.
  5. Builds the new frame in a hidden slot next to the visible one, and swaps it in once its stylesheets and fonts have loaded, or after 200 ms. The stage never goes blank.

Viewer and CLI explains how to host the static site.

The live preview #

A frame is DOM without scripts. The live preview mounts the component of one test for real instead, the way the test's arrange phase set it up: its props, its vi.fn() mocks, its wrapper and whatever its hooks prepared. Nothing after the first render runs, so every mount is a fresh start, and the component stays mounted. describe-me dev starts the preview, describe-me build --live builds it into the static site, and Live in the viewer frames its page.

createLiveServer() loads your config with DESCRIBE_ME_LIVE set and starts a Vite dev server, which serves the live page and the shims. The viewer frames the page. The page imports the test file, runs its arrange phase, mounts the component at the first render and stops the test. It posts its status, size, actions and errors to the viewer's origin. In Node createLiveServer() @describe-me/vitest/live loads your config with DESCRIBE_ME_LIVE=1 Vite dev server your Vite, your config mode test, localhost only describeMe() in live mode serves the page and shims Live page · live.html Test file, imported for real vitest and render are shims Arrange phase beforeAll → beforeEach → body First render mounts in the test's wrapper, then stops the test Live component a fresh start: nothing after the render runs Viewer · another origin iframe live.html?file&path &occurrence&origin Messages status · size action · error to its origin only postMessage
The page runs the test until its first render and no further. The component stays mounted and live, as the arrange phase set it up.

The server

createLiveServer() starts a second Vite dev server next to Vitest, on your own Vite and your own Vitest config, in mode test. It listens on localhost only. Without a configFile it picks the file Vitest would pick, the first vitest.config.*, then vite.config.*. The page is live.html under base.

import { createLiveServer } from '@describe-me/vitest/live'

const server = await createLiveServer({ root: process.cwd(), configFile: 'vitest.config.ts' })

console.log(`${server.base}live.html`)
await server.close()

It sets DESCRIBE_ME_LIVE=1 while it loads the config, and only then. describeMe() reads the variable when the config calls it, and returns its live mode: no setup file of its own, reporter, provide or recording redirect, but the live page, the shims below and the dependencies to bundle before the first page loads. Vitest never sets the variable, so a test run records as before.

Both modes expose api.live.protocol and api.live.enabled. When the config has no describeMe(), or one whose protocol is not the server's, the server closes and throws. The message says that all describe-me packages must be on the same version, as the recorder's guard does. With live: false, enabled is false: live mode adds nothing, and the server closes and throws an error whose code is DESCRIBE_ME_LIVE_OFF. describe-me dev then runs the viewer without Live.

The server does not watch the config file: a restart would run it again without the variable, in recording mode. After an edit to the config, start a new server. Edits to components and tests reach the page through Vite as usual.

The static live preview

buildLive() builds the same page into static files. describe-me build --live calls it with the test files of the manifest and __live/ in the site as the output directory, then writes __live.json next to the viewer.

import { buildLive } from '@describe-me/vitest/live'

const { protocol } = await buildLive({
  root: process.cwd(),
  configFile: 'vitest.config.ts',
  outDir: 'describe-me-dist/__live',
  testFiles: ['src/Button.test.tsx', 'src/Menu.test.tsx'],
})

An import in a project file that does not load in the browser, such as node:fs or a package that is not installed, gets a stand-in that throws when it runs. The build warns and goes on:

describe-me: live-fixtures/node-only.test.tsx imports node:fs, which does not load in the browser: it is a Node module

A test file that imports it reports import-failed, and a setup file setup-failed. This is stricter than dev: a file that imports a Node module fails as a whole, even when its arrange phase never calls it.

A URL built as a string from the host root, such as '/logo.svg', points at the root of the host, not at the site. Import the file instead, so the bundler gives it a relative URL.

What the page imports

The page imports the test file for real. Live mode swaps these imports:

Import What the page gets
vitest describe, it and the hooks register as in Vitest, with every modifier, aroundEach and aroundAll included. test.extend keeps its fixtures. vi.fn() and vi.spyOn() post each call as an action. vi.mock and vi.doMock post a notice. The rest of vi does nothing and returns vi. expect and every other name is inert: it takes any call and any property. A package that imports vitest, such as @testing-library/jest-dom/vitest, gets the shim too, because the optimizer leaves vitest out of its bundles.
vitest-browser-react @describe-me/react/live, in browser mode.
@testing-library/react @describe-me/react/live-testing-library, in jsdom projects. The rest of Testing Library stays real.
vitest/browser Inert names, and the same for @vitest/browser/context.
@describe-me/vitest A step() that runs its body and records nothing, and an inert recorder.

The arrange phase

  1. Globals With test.globals, the shims go on globalThis before the test file imports.
  2. Setup files The page imports test.setupFiles, or live.setupFiles when the config sets it, one at a time in their order, under the same shims. Their hooks register on the root suite before the test file's. One that does not load ends the page with setup-failed.
  3. Import The page imports the test file, then calls the describe factories the way Vitest collects them: depth first, in order.
  4. Find It finds the test by its suite path and name, and by its occurrence when the name repeats, counted like the stable test id.
  5. Run It runs the beforeAll hooks from the root down, then the beforeEach hooks, then the body. The aroundAll hooks of each suite wrap its beforeAll hooks and everything below, and the aroundEach hooks of the path wrap the beforeEach hooks and the body, the first one outermost. Before each aroundEach hook, beforeEach hook and the body, the page computes the test.extend fixtures it destructures and the auto ones, each once. each and for rows get their names by the rules of the installed Vitest.
  6. First render The render names each unnamed vi.fn() after its prop, mounts with createRoot in the test's wrapper, reports mounted and throws a stop signal that ends the test. Assertions, interactions and afterEach never run.

A render of an empty fragment, such as render(<></>), mounts nothing and waits: the component mounts at the test's rerender. renderHook stops without a mount, so the page reports no-render. A test that does not reach its first render within its own timeout, or test.testTimeout, reports failed.

The page and its messages

<base>live.html?file=<module id>&path=<JSON array>&occurrence=<n>&origin=<viewer origin>

Under describe-me dev, base is the server's, such as http://localhost:5173/__describe-me/. On a static site it is __live/ next to the viewer. file is the module id of the manifest, a path relative to the root. The page builds the URL of the test file on its own origin and imports it only when that URL names exactly this file, so a file cannot load code from another origin, not even with a tab or a newline that the URL parser drops. path holds the suite names, outermost first, then the test name. occurrence counts a repeated name from 1, its default.

The page imports the test file by its absolute URL, so it loads once. Vite adds ?import to a path from the root, and the self-import that @vitejs/plugin-react adds to a file with a component would load the file a second time, with every hook and test twice. A static build imports it from the map of lazy imports instead, and a file the map does not hold reports import-failed.

The page posts its messages to window.parent, each with source: 'describe-me-live'. It posts only in a frame, only when origin is exactly the origin of an http: or https: URL, and only to that origin. Another site that frames the page learns nothing. In any case the page sets its status on <html data-describe-me-live>, and writes any status but mounted on the page.

Message Fields When
status status, detail? Once, when the page knows how it ended up. The first status is final.
background html, body Just before the status, and whenever either changes: the computed background-color of <html> and of <body>, so the viewer can paint its canvas to match. A viewer of an earlier version ignores it.
size height After the status, and whenever the content changes: the bottom of the lowest painted element, plus a margin.
action name, args On every call of a mock, under its name, such as onSelect with "Rename".
error message When the live component throws, or a promise rejects unhandled.
notice message For each vi.mock or vi.doMock call, once per page: it names the module the page loads for real. The viewer shows it in the inspector.

The viewer posts one message to the page, with source: 'describe-me-viewer'. It carries the stage's dark variant.

Message Fields When
dark-variant root When the frame loads, and whenever bg, the viewer theme under auto or the variant changes. root holds the class and the data-* attribute of the dark variant under bg=dark, or under auto when it resolves to dark, or is null.

The page takes the message only from its parent window, from the origin its URL names. It applies only one class name and one data-* attribute to <html>, and only once it has a status. A request that comes earlier waits for it. The page takes back only what it applied, so a class the test set itself stays. A page of an earlier version ignores the message, so the live protocol stays 1.

Status What happened
mounted The first render mounted the component.
no-render The test ended without a render: it called renderHook or skip(), or rendered nothing.
not-found The URL is not valid, or the file has no such test.
import-failed The test file did not import, or one of its suites threw.
setup-failed A setup file did not load, link or run its top level. The detail starts with the file.
failed The arrange phase threw, or did not reach the first render in time.

What the page does not do:

Versions and compatibility #

The reporter reads its own package.json and writes its name and version to generator. When the major and minor version there differ from the viewer's own, the viewer shows a banner under its header:

Written by @describe-me/vitest <version>. This viewer is describe-me <version>, so frames may not show correctly. Install every describe-me package at the same version.

A manifest without generator was written before 0.5.1. This viewer reads all of those, so it shows no banner for them.

In the test, two copies of @describe-me/core can meet: Vite bundles the adapter with its own copy, while a linked workspace serves the setup file's copy from source. So the recorder lives on globalThis, one per page, and carries a protocol number. A copy of core that finds a recorder with another number, or with none, throws, and every test file fails with:

describe-me: all describe-me packages must be on the same version. Another copy of @describe-me/core created the test recorder (protocol …, expected …). Install @describe-me/core, @describe-me/react, @describe-me/vitest and describe-me at one version.

Data written by older versions keeps working in this viewer:

It does not work the other way round. Style references came with 0.5.0, and a 0.4 viewer takes them for CSS text, so frames written since show there without their styles. Upgrading and One version for all packages say how to keep the packages together.