Skip to content

Your first story

A story is a normal test with three extra lines in it. You keep your framework’s describe, your assertions, and your runner. The steps you mark become the documentation.

Create src/login.story.test.ts:

import { story } from 'executable-stories-vitest';
import { describe, expect, it } from 'vitest';
describe('Login', () => {
it('user logs in successfully', ({ task }) => {
story.init(task);
story.given('the user is on the login page');
story.when('the user submits valid credentials');
story.then('the user should see the dashboard');
expect(true).toBe(true); // replace with real assertions
});
});

Call story.init(task) first, passing task from the test callback. Everything after it is a step.

Terminal window
pnpm vitest run

Whichever adapter you used, the Markdown comes out the same:

### ✅ User logs in successfully
- **Given** the user is on the login page
- **When** the user submits valid credentials
- **Then** the user should see the dashboard

A skipped, failed, or planned step is marked as such, so the document reports the run rather than describing an intention.

Render the same run as HTML and you get a searchable report:

Report Outputwhat this code generatesOpen live report ↗
The generated HTML report with Given/When/Then steps, a run summary, and search and status filters
Every adapter produces this same report. Click to open the live one.

Repeat given, when, or then anywhere in a story and the second occurrence onward renders as And. Write what you mean and let the report read like prose:

story.given('the user account exists');
story.given('the account is suspended'); // renders as And
story.when('the user submits valid credentials');
story.then('the user should see an error message');
story.but('the user should not be logged in'); // always But

and() and but() never convert. Use but() when the contrast is the point, exactly as Gherkin does.

Steps carry the narrative. Doc entries carry the evidence: the payload you sent, the table you checked, the diagram of the flow.

it('user logs in with valid credentials', ({ task }) => {
story.init(task, { tags: ['auth', 'smoke'], ticket: 'AUTH-42' });
story.given('the user has a registered account');
story.and('the user is on the login page');
story.json({
label: 'Credentials',
value: { email: 'user@example.com', password: 'secret' },
});
story.when('the user submits valid credentials');
story.then('the user should receive an auth token');
story.table({
label: 'Token fields',
columns: ['Field', 'Type', 'Description'],
rows: [
['token', 'string', 'JWT bearer token'],
['expiresIn', 'number', 'Seconds until expiry'],
],
});
story.note('Token rotation is handled automatically on refresh.');
});

Every adapter offers the same doc kinds: json, kv, code, table, link, section, mermaid, screenshot, note, tag, and custom. Argument shapes follow each language’s conventions.