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

Examples

The repository has four example projects with plain component tests. Two are React projects, one in Vitest browser mode and one in jsdom. Two run on a design system: shadcn/ui on Tailwind in browser mode, and Material UI in jsdom. Their viewers show what describe-me makes of those tests.

Open the viewers #

Note. The viewers are part of this site. They fetch their data, so they do not open from disk, as Hosting the static site explains. Run them locally builds them on your machine.

Every viewer is built with --live, so a test opens in Live. Try it on these tests:

React in browser mode #

The project runs Vitest in browser mode with Playwright and Chromium, set up as in Getting started. Its tests import render from vitest-browser-react and interact through Locator methods and userEvent from vitest/browser. Nothing in them is written for describe-me apart from two calls to step().

examples/react-browser/src/Counter.test.tsx

import { render } from 'vitest-browser-react'
import { step } from '@describe-me/vitest'
import { Counter } from './Counter'

it('does not go below the minimum', async () => {
  const screen = await render(<Counter initial={1} />)
  await step('go down to the minimum', async () => {
    await screen.getByRole('button', { name: 'decrement' }).click()
  })
  // …assertions
})
Component What its tests show
Counter Every .click() on a Locator is a frame, so the second click shows the count at 2. step() names a frame after what it does: go down to the minimum.
Chip .click() on the remove button of two chips that a component in the test file keeps in state, with the click as its own frame. The test is documented under the first chip.
SearchBox .fill() on a Locator, and userEvent.type and userEvent.keyboard, which type the query and submit it.
Menu userEvent.click opens the menu and userEvent.keyboard('{Escape}') closes it. In the second test a step() names the frame that opens the menu.
Button Variants, sizes and states. A rerender is a frame of its own: large, then small. userEvent.click with force: true clicks a loading button that swallows the click. Frames show hover and focus: .hover() on a ghost button, the ring of userEvent.tab(), and neither after .unhover() or after a click, although the click leaves the pointer on the button. The loading button loses its hover after a double click, a triple click, .wheel() and .dropTo() as well.
Postcard An imported image on a stylesheet url() background. The viewer serves both files long after the test ran. The caption is set in Lora, which previewHead loads.
StyledText Four lines, styled by an inline style, a <style> tag, a CSSOM insertRule and adoptedStyleSheets. All four colours survive.

The components import their CSS like app code does: styles.css, Chip.css and postcard.css. Every frame carries the styles the page had, see what is captured. previewHead in vitest.config.ts loads Inter from Google Fonts and Lora from a local fonts.css, the way an app shell would. See Fonts.

Naming.test.tsx renders components inside a context provider, from a test-local memo, inside a fragment, inside Suspense and after a rerender from an empty fragment. Each test is documented under the component it rendered, not the wrapper around it. One test, two environments lists the cases, and Naming explains the rules.

React in jsdom #

The project runs Vitest in jsdom with Testing Library and globals: true, set up as in Getting started. Its tests import render from @testing-library/react or from a custom render in src/test-utils.tsx. render is synchronous, and most tests have no await. Recording adds no act warnings, see jsdom.

The custom render wraps every test in a styled-components ThemeProvider and GlobalStyles, as many design systems do. It imports @testing-library/react itself, so its tests record without a change.

examples/react-jsdom/src/test-utils.tsx

function AllTheProviders({ children }: { children: ReactNode }) {
  return (
    <ThemeProvider theme={theme}>
      <GlobalStyles />
      {children}
    </ThemeProvider>
  )
}

function customRender(ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) {
  return render(ui, { wrapper: AllTheProviders, ...options })
}
Component What its tests show
Heading A synchronous render through the test utils. The heading takes its colour from the theme.
Text Body copy and small muted copy, through the test utils.
Button Exported from PolymorphicButton.tsx. With component="a" it renders as a link. It is an anonymous generic forwardRef, documented under its export.
Badge A styled-components pill in two tones.
Panel A plain CSS import, the most common case.
Disclosure fireEvent.click opens it in one frame. user-event works as a setup instance, user.click, and through the direct API, userEvent.click, with one frame per click either way. A step() names close it again. Hover follows user.hover until user.unhover, and fireEvent.mouseEnter and every other enter or over event until a leave or out event. user.tab shows the focus ring, which a hover keeps and a click takes away. user.keyboard and user.type bring it back, and user.dblClick, user.tripleClick and user.pointer take it away with the hover.
TextField user.type focuses the field, and its styles leave the focus ring to the browser, so the frame shows the default one. A click gives it to a multiline field and to a contenteditable element too, but not to one whose contenteditable is false.
AsyncGreeting With the default eager render frame, the render frame shows the loader and the closing frame shows the greeting. See render frames.
Toast The file turns on fake timers and calls cleanup in afterEach. A fireEvent inside a step() dismisses it. After the timers advance, the closing frame shows the empty status region.
Postcard The same postcard as in browser mode. Here Postcard.tsx imports Lora's fonts.css like any stylesheet, and previewHead loads only Inter.
StyledText The same four lines as in browser mode. The adoptedStyleSheets line keeps its default colour, because jsdom has no constructable stylesheets.

Naming.test.tsx has the five cases of browser mode with its own components, plus one that renders through the test utils: Badge under the test-utils wrapper is documented as Badge, not as the ThemeProvider around it. See styled-components for how its styles reach the viewer.

Three test files render nothing the viewer can show, so they have no entry in it. They run like any other test. See what is left out.

One test, two environments #

Some tests exist in both React examples. Open them side by side to see what the environment changes.

Case Browser mode jsdom What differs
A photo on a patterned card Postcard Postcard Both look the same. Lora comes from previewHead in browser mode and from an imported fonts.css in jsdom.
Four style techniques StyledText StyledText The adoptedStyleSheets line is orange in browser mode only. jsdom has no constructable stylesheets.
Inside a provider Button variant=secondary Badge tone=danger A React context provider in browser mode, the styled-components ThemeProvider in jsdom.
From a test-local memo Chip tone=accent Badge tone=info Nothing but the component. Both skip the memo the test file defines.
Inside a fragment Button size=sm Text size=small jsdom renders through the test-utils wrapper, browser mode without one.
Inside Suspense Counter initial=2 Panel title=Suspended Browser mode puts the component straight inside Suspense. jsdom wraps it in a test-local component and gives Suspense a fallback.
After a rerender from an empty fragment Button variant=ghost Badge tone=danger Browser mode awaits rerender, jsdom calls it synchronously. Both show an empty first frame.

Choosing an environment weighs browser mode against jsdom.

shadcn/ui on Tailwind #

The project is a Vite and React app with shadcn/ui on Tailwind 4, set up the way npx shadcn init sets up Vite: @tailwindcss/vite in the config, a components.json with the new-york style, and the components in src/components/ui/ as the CLI writes them. Its tests run in Vitest browser mode, like the React example, and render through vitest-browser-react.

src/index.css holds the design system. :root and .dark set the semantic tokens, such as --primary and --background. @theme adds a brand ramp next to Tailwind's default colors, and the primary color sits on it. @theme inline maps the tokens to utilities such as bg-primary. Inter comes from @fontsource-variable/inter. The setup file src/test-setup.ts imports index.css for every test file, as the app's entry would. The page paints the --background token, as the shadcn setup does. sidebar.groups mirrors src/components: the shadcn primitives sit under Components / ui, the app's own component next to them.

examples/shadcn/src/index.css

@import 'tailwindcss';
@import '@fontsource-variable/inter';

@custom-variant dark (&:is(.dark *));

@theme {
  --font-sans: 'Inter Variable', ui-sans-serif, system-ui, sans-serif;
  --color-brand-600: oklch(0.52 0.23 285);
  /* …the rest of the brand ramp, 50 to 950 */
}

:root {
  --background: oklch(1 0 0);
  --primary: oklch(0.52 0.23 285);
  /* …the other tokens */
}

.dark {
  --background: oklch(0.145 0 0);
  --primary: oklch(0.7 0.18 285);
  /* …the other tokens */
}
Component What its tests show
Button The default variant, outline with a lucide icon and destructive. The last test adds dark to the classes of <html> before render and removes it in onTestFinished, as an app's theme switch would. So every variant in the dark variant is dark in every frame. A ghost button on hover keeps its hover in the closing frame, with the background the utility paints there.
Card A header, content and a footer, with the border, radius and shadow of the tokens.
Alert The default alert and a destructive one with an icon.
Badge All six variants in a plain div, documented under the first Badge.
Input A placeholder, and a disabled input.
Switch .click() on the switch turns it on. The Switch renders before its Label, because a test is named after the first component it renders. See Naming.
SignupCard A product component built from Card, Label, Input and Button. A step named type the email wraps userEvent.fill, and a click on Request access calls onSubmit and swaps the card for a confirmation. Without an email, the click shows an error under the input and marks it aria-invalid.

Material UI #

The project runs Material UI with emotion in jsdom with Testing Library, like the React example. src/test-utils.tsx wraps every render in Material UI's ThemeProvider and CssBaseline, and imports Roboto from @fontsource/roboto. The page paints the theme's background.default, as CssBaseline does.

src/theme.ts builds the theme with createTheme(): a palette for the light and the dark scheme under colorSchemes, Roboto and heavier headings and buttons under typography, and rounder corners under shape. With cssVariables, Material UI writes both schemes as --mui-* custom properties, the dark one under prefers-color-scheme: dark. So a recorded frame shows its dark variant on the dark canvas, although jsdom has no matchMedia. disableCssColorScheme keeps color-scheme off the page, so the stage's auto background follows the viewer theme.

Two variants run the classic theme, without cssVariables, where the recorded CSS holds only the light scheme's colors. MUI_CLASSIC=1 pnpm test runs the same tests into node_modules/.describe-me-classic, so .describe-me/ keeps the default run. See MUI_CLASSIC. MUI_THEME=off pnpm test runs them without foundations.theme, as a plain Material UI project, into node_modules/.describe-me-no-theme, where Foundations shows only the setup hint. See MUI_THEME.

examples/mui/src/theme.ts

export const theme = createTheme({
  cssVariables: process.env.MUI_CLASSIC === '1' ? false : { disableCssColorScheme: true },
  colorSchemes: {
    light: { palette: { primary: { main: '#5b4bdb' }, secondary: { main: '#00897b' } } },
    dark: { palette: { primary: { main: '#a99cff' }, secondary: { main: '#4db6ac' } } },
  },
  typography: {
    fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
    button: { textTransform: 'none', fontWeight: 600 },
  },
  shape: { borderRadius: 10 },
})

Material UI's components live in node_modules, and describe-me documents the components of the project's own modules only. A test that rendered only Material UI components would have no props docs. So the tests render four product components built from them.

Component What its tests show
PlanCard A plan on an outlined card, and the popular plan with a secondary chip and a contained button in the primary color.
SignupForm user-event types the email and clicks Create account, which calls onSubmit. Without an @, the field flags the invalid email.
StatusBanner Every severity of Material UI's Alert, in a Stack.
StatusChip Every status as a colored chip.

Run them locally #

Without a clone, the starter in StackBlitz runs examples/stackblitz: three components, their tests and the viewer with Live and Foundations, in jsdom, on the published packages. Edit a test there and run npm run test:watch in a second terminal.

In a clone of the repository:

  1. Install pnpm install
  2. Build the packages pnpm build
  3. Install Chromium once, for browser mode: pnpm --filter react-browser exec playwright install chromium
  4. Run the browser-mode tests pnpm --filter react-browser test
  5. Run the jsdom tests pnpm --filter react-jsdom test
  6. Run the shadcn/ui tests in browser mode pnpm --filter shadcn test
  7. Run the Material UI tests in jsdom pnpm --filter mui test
  8. Build and serve the site pnpm site, then open http://localhost:6060/examples.html.

To work on one example, run pnpm dev inside it. It runs vitest and describe-me dev side by side, so the viewer follows the tests as they rerun. See the CLI. pnpm dev starts the live preview too. pnpm site builds every viewer with Live, and pnpm docs:build inside an example writes its static site to docs-dist/, without Live.