Skip to content

Vitest story & doc API

Vitest uses a native describe/it pattern: call story.init(task) at the start of each test, then use story.given, story.when, story.then, and doc methods on the story object. Vitest intentionally does not export top-level step helpers because a top-level then export can break tooling that treats module namespaces as thenables.

Initializes a story for the current test. Must be called at the start of each test that wants documentation.

ItemDescription
taskThe Vitest task from it('...', ({ task }) => { ... }).
optionsOptional StoryOptions: tags, ticket, meta, traceUrlTemplate.
Exampleit('adds two numbers', ({ task }) => { story.init(task); story.given('...'); ... });

Example with options:

it('admin deletes user', ({ task }) => {
story.init(task, {
tags: ['admin', 'destructive'],
ticket: 'JIRA-456',
});
story.given('the admin is logged in');
story.when('the admin deletes the user');
story.then('the user is removed');
});

Step markers (story.given, story.when, story.then, story.and, story.but)

Section titled “Step markers (story.given, story.when, story.then, story.and, story.but)”

Use the story object to mark steps. Each step can take optional inline docs as a second argument.

MethodRenders asDescription
story.given(text, docs?)Given / AndFirst given → “Given”; subsequent in same story → “And”.
story.when(text, docs?)When / AndSame auto-And rule for repeated when.
story.then(text, docs?)Then / AndSame for then.
story.and(text, docs?)AndAlways “And” (never auto-converted).
story.but(text, docs?)ButAlways “But” (negative intent).

Keyword resolution: The first given in a story renders as “Given”; any further given in the same story renders as “And”. The same rule applies to repeated when and then. Using story.and() or story.but() explicitly never changes: and always renders “And”, but always renders “But” (for negative or contrasting intent).

Example:

import { story } from 'executable-stories-vitest';
import { describe, expect, it } from 'vitest';
describe('Calculator', () => {
it('adds two numbers', ({ task }) => {
story.init(task);
story.given('two numbers 5 and 3');
const a = 5,
b = 3;
story.when('I add them together');
const result = a + b;
story.then('the result is 8');
expect(result).toBe(8);
});
});

Pass a StoryDocs object as the second argument to any step to attach notes, JSON, tables, etc. to that step:

story.given('valid credentials', {
json: {
label: 'Credentials',
value: { email: '[email protected]', password: '***' },
},
note: 'Password is masked for security',
});
story.then('the user is logged in', {
table: {
label: 'Session',
columns: ['key', 'value'],
rows: [['userId', '123']],
},
});

Supported keys: note, tag, kv, code, json, table, link, section, mermaid, screenshot, custom. Same shapes as the standalone doc methods below.

AliasMaps to
story.arrange, story.setup, story.contextgiven
story.act, story.execute, story.actionwhen
story.assert, story.verifythen

Attach rich documentation to the current step (or story-level if called before any step). All take an options object except note and tag.

MethodDescriptionExample
story.note(text)Free-text note.story.note("But guest checkout is enabled");
story.tag(name | names)Tag(s).story.tag(["wip"]);
story.kv(options)Key-value.story.kv({ label: "Version", value: "1.0" });
story.code(options)Code block.story.code({ label: "Invoice", content: "<xml>...</xml>", lang: "xml" });
story.json(options)JSON block.story.json({ label: "Payload", value: { id: 1 } });
story.table(options)Markdown table.story.table({ label: "Users", columns: ["email"], rows: [["[email protected]"]] });
story.link(options)Link.story.link({ label: "Spec", url: "https://..." });
story.section(options)Section with markdown.story.section({ title: "Notes", markdown: "**Bold**" });
story.mermaid(options)Mermaid diagram.story.mermaid({ code: "graph LR; A-->B" });
story.screenshot(options)Screenshot reference.story.screenshot({ path: "screen.png" });
story.custom(options)Custom entry (use customRenderers in reporter).story.custom({ type: "myType", data });

Example:

it('login with credentials', ({ task }) => {
story.init(task);
story.given('the user has valid credentials');
story.note('Credentials are loaded from fixtures.');
story.when('the user submits the login form');
story.json({ label: 'Request', value: { email: '[email protected]' } });
story.then('the user is logged in');
});
OptionTypeDefaultDescription
tagsstring[]Tags for filtering and categorizing (e.g. ["smoke", "auth"]).
ticketstring | string[]Ticket/issue reference(s) for requirements traceability.
metaRecord<string, unknown>Arbitrary user-defined metadata.
traceUrlTemplatestringTrace URL template using {traceId}. Also supported via OTEL_TRACE_URL_TEMPLATE.

Import the reporter from the /reporter subpath in your config (do not import from the main package in vitest.config.ts):

import { StoryReporter } from 'executable-stories-vitest/reporter';
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
reporters: ['default', new StoryReporter()],
},
});

See Vitest reporter options for all options.

Exported from executable-stories-vitest: story, StoryMeta, StoryStep, DocEntry, StepKeyword, StoryDocs, StoryOptions, VitestTask, VitestSuite, StepMode, DocPhase, STORY_META_KEY.

Reporter types come from executable-stories-vitest/reporter, including StoryReporterOptions, OutputFormat, OutputMode, ColocatedStyle, OutputRule, and FormatterOptions.