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 #
What each part does, one level below the overview:
-
@describe-me/vitest/pluginruns once, when Vitest loads its config. It picks the environment, adds the setup file and the reporter, redirects therenderimport, and handsinclude,excludeandrenderFrameto the setup file through Vitest'sprovide. -
@describe-me/reactwraps therenderyour tests call. It takes the render frame and names the component and its props. -
@describe-me/vitest/setuporsetup-domruns in every test file. It patches the interactions at their source and registers the hooks that begin and end each recording. -
@describe-me/coreholds the recorder, one per page onglobalThis. It waits for the framework to settle, serializes the DOM with rrweb-snapshot and keeps the frames in the order the test asked for them. -
@describe-me/vitest/reporterruns in Node. It reads each test's frames fromtask.meta, stores them by content hash and writesmanifest.json. -
describe-mereads that directory.describe-me devserves it and follows each run,describe-me buildcopies it into a static site.
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.
task.meta to Node.
-
Before the test The setup file's
beforeEachcallsrecorder.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. -
Render The adapter calls the original
render, names the component and its props withsetComponent(), and callscapture('render'). TherenderFrameoption decides when that frame is taken: right away, before the next interaction, or once a loader is gone. See Render frame timing. -
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 withnoteGesture(), andcapture('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 forrequestAnimationFrame, 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/reactskips the settle. -
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). -
After the test The
afterEachhook callsflush(), which waits for the captures still in flight and takes a held-back render frame. Unless it took one, the hook callscapture('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 intotask.meta.describeMeonly when there are frames. Thenteardown()lets the adapters unmount. -
In Node When a test file ends, the reporter's
onTestModuleEndbuilds the entry of each recorded test and writes its snapshots. When the run ends,onTestRunEndreads the props of the components, deletes the files no frame needs, writesfoundations.jsonand thenmanifest.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:
-
In browser mode,
patch-locators.tsaskspage.elementLocator()for a locator, walks up its prototype chain and wraps each action method on the prototype that defines it. Soscreen.getByRole('button').click()records just likeuserEvent.click(). -
patch-user-event.tswraps the methods ofuserEventfromvitest/browserthat do not go through a locator, such as the keyboard ones. -
In jsdom,
patch-testing-library-user-event.tsimports@testing-library/user-eventwhen it is installed. It wraps the direct API and every instance thatuserEvent.setup()returns.
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:
idf0,f1and so on, in the order the test asked for them.kindrender,action,steporend.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:
-
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 itspublic/folder becomesdescribe-me-asset:and the stored name. A CSS file is copied with the fonts, images and stylesheets it points at. -
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 keepsdescribe-me-style:<hash>+<hash>…. -
Removes every
<script>and writesdata-vite-dev-idrelative to the root, as What a frame holds says. -
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:
-
A module (
ManifestModule) has anid, the test file's path from the root, itsgroupand itstests. Thegroupis the file's sidebar group, outermost first, absent for a file without one and before 0.12. -
A test (
ManifestTest) has anid: the first 12 hex characters of a SHA-1 of the module path, the suite path, the name and, for a repeated name, its occurrence. It stays the same across runs and machines.vitestIdis Vitest's own id in the run that recorded it, absent before 0.5. Thenname,path(the suite names, outermost first),fullName,tags(its Vitest tags, own and inherited, when it has any, absent before 0.7),state,duration,errors,component(name, props and source file) andframes. In the message and stack of each of theerrors, a path under the root or under a folder above it is relative to the root, such assrc/Button.test.tsx:9:52, unless that folder sits right under/, as/homedoes. -
A frame (
ManifestFrame) is a frame whosesnapshotis the path of its file, such assnapshots/4c0bbe491dd8a500.json. -
A component (
ComponentDoc) has aname, afileand itsprops. Each prop (PropDoc) has its name, its type as TypeScript prints it, whether it is required, its default, its kind, its literal values and its JSDoc. Writing stories shows how the viewer uses them for coverage.
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:
-
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. -
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. -
Has rrweb rebuild the DOM into a fresh iframe, sandboxed with
allow-same-originonly, 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 underautofrom the page or the viewer theme. -
In a frame with states, gives each marked element the classes
:hover,:focus,:focus-visibleand:focus-withinof the states it had. Each rule that names a state is rewritten to match the class as well, nested rules included:.btn:hoverbecomes.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. - 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.
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'],
})
-
It loads the config like the server: your Vite, mode
test, andDESCRIBE_ME_LIVE=1only while the config loads. The checks of the protocol and oflive: falserun before anything is bundled, so withlive: falseit writes nothing and throws an error whosecodeisDESCRIBE_ME_LIVE_OFF. -
It bundles
live.htmlwith the preview head, the setup files and a lazy import of each test file, so each test file gets a chunk of its own. The base is relative, so the output works under any path, and yourpublic/is copied in. -
The output is not minified. Suite and test names come from
fn.name, as in Vitest, and a minifier would renamedescribe(Button, …)and the functions of%srows, so the page would not find tests that dev mounts. -
Your config's
buildsection is not used. Dev never reads it, and an app or library build there would break the page. -
Plugins run as on the dev server.
apply: 'serve'runs,apply: 'build'does not, and anapplyfunction getscommand: 'serve'. So a plugin split in two byapply, such as@tailwindcss/vite, generates the same CSS as in dev and in the tests, and production-only plugins such as compression or legacy bundles stay out. Hooks still seecommand: 'build', so React's Fast Refresh stays off. A plugin kept by itsapplyruns withouttransformIndexHtml, because Vite calls it without a server in a build. That is how@vitejs/plugin-react-swcbuilds without its refresh preamble. See no dev server for plugins. -
It builds with
NODE_ENV=development, so React andimport.meta.envbehave as underdescribe-me dev. -
File paths in the built code, such as the ones React's development JSX keeps for its
warnings, are relative to the root, such as
src/Button.test.tsx, as in a test'serrors.
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
-
Globals With
test.globals, the shims go onglobalThisbefore the test file imports. -
Setup files The page imports
test.setupFiles, orlive.setupFileswhen 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 withsetup-failed. -
Import The page imports the test file, then calls the
describefactories the way Vitest collects them: depth first, in order. - 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.
-
Run It runs the
beforeAllhooks from the root down, then thebeforeEachhooks, then the body. ThearoundAllhooks of each suite wrap itsbeforeAllhooks and everything below, and thearoundEachhooks of the path wrap thebeforeEachhooks and the body, the first one outermost. Before eacharoundEachhook,beforeEachhook and the body, the page computes thetest.extendfixtures it destructures and theautoones, each once.eachandforrows get their names by the rules of the installed Vitest. -
First render The render names each unnamed
vi.fn()after its prop, mounts withcreateRootin the test's wrapper, reportsmountedand throws a stop signal that ends the test. Assertions, interactions andafterEachnever 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:
-
vi.mockhas no effect, so the page loads the real module. It posts a notice instead. - Fake timers are skipped, so time is real. A toast that hides after 3 seconds hides after 3 seconds.
-
The teardown after
use()in a fixture, or afterrunTest()orrunSuite()in an around hook, never runs, and neither do the cleanups of the builder syntax.
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:
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:
- Fields added since 0.4 are optional, and the viewer counts a missing list as empty.
- Snapshots without style references load as they are.
-
Tests written before 0.5 carry Vitest's id. A link to one still opens it, through
vitestId. The next run gives every such test its stable id and keeps the old one asvitestId.
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.