executable-stories-vitest 7.0.1 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "executable-stories-vitest",
3
- "version": "7.0.1",
3
+ "version": "8.0.0",
4
4
  "description": "TS-first story/given/when/then helpers for Vitest with Markdown user-story doc generation.",
5
5
  "author": "Jag Reehal <jag@jagreehal.com>",
6
6
  "homepage": "https://github.com/jagreehal/executable-stories#readme",
@@ -32,21 +32,23 @@
32
32
  },
33
33
  "files": [
34
34
  "dist",
35
- "README.md"
35
+ "skills",
36
+ "README.md",
37
+ "bin"
36
38
  ],
37
39
  "peerDependencies": {
38
- "vitest": ">=4.0.18",
39
- "executable-stories-formatters": "^0.6.1"
40
+ "vitest": ">=4.1.0",
41
+ "executable-stories-formatters": "^0.7.0"
40
42
  },
41
43
  "devDependencies": {
42
44
  "@opentelemetry/api": "^1.9.0",
43
- "@types/node": "^25.3.2",
45
+ "@types/node": "^25.5.0",
44
46
  "@types/picomatch": "^4.0.2",
45
47
  "tsup": "^8.5.1",
46
48
  "typescript": "^5.9.3",
47
49
  "vitest": "^4.0.18",
48
50
  "eslint-config-executable-stories": "0.2.0",
49
- "executable-stories-formatters": "0.6.1"
51
+ "executable-stories-formatters": "0.7.0"
50
52
  },
51
53
  "keywords": [
52
54
  "vitest",
@@ -64,6 +66,9 @@
64
66
  "fast-glob": "^3.3.3",
65
67
  "picomatch": "^4.0.3"
66
68
  },
69
+ "bin": {
70
+ "intent": "./bin/intent.js"
71
+ },
67
72
  "scripts": {
68
73
  "build": "tsup",
69
74
  "type-check": "tsc --noEmit",
@@ -0,0 +1,142 @@
1
+ ---
2
+ name: vitest-converting-tests
3
+ description: >
4
+ Incrementally adopt executable-stories in Vitest. Add story.init(task) and
5
+ step markers to existing it/test blocks. Use doc.story for framework-native
6
+ tests. File naming .story.test.ts. No rewrite needed — progressive
7
+ enhancement of existing tests.
8
+ type: lifecycle
9
+ library: executable-stories-vitest
10
+ library_version: "7.0.1"
11
+ requires:
12
+ - vitest-story-api
13
+ sources:
14
+ - "jagreehal/executable-stories:apps/docs-site/src/content/docs/guides/converting-vitest.md"
15
+ ---
16
+
17
+ This skill builds on vitest-story-api. Read vitest-story-api first.
18
+
19
+ # Converting Existing Vitest Tests
20
+
21
+ ## Setup
22
+
23
+ Install the package:
24
+
25
+ ```bash
26
+ npm install -D executable-stories-vitest executable-stories-formatters
27
+ ```
28
+
29
+ ## Core Patterns
30
+
31
+ ### Step 1: Rename the file
32
+
33
+ ```
34
+ # Before
35
+ test/calculator.test.ts
36
+
37
+ # After
38
+ test/calculator.story.test.ts
39
+ ```
40
+
41
+ The reporter filters for `.story.test.ts` files.
42
+
43
+ ### Step 2: Add story.init() and step markers
44
+
45
+ ```typescript
46
+ // Before
47
+ import { describe, expect, it } from "vitest";
48
+
49
+ describe("Calculator", () => {
50
+ it("adds two numbers", () => {
51
+ const result = add(2, 3);
52
+ expect(result).toBe(5);
53
+ });
54
+ });
55
+ ```
56
+
57
+ ```typescript
58
+ // After
59
+ import { describe, expect, it } from "vitest";
60
+ import { story } from "executable-stories-vitest";
61
+
62
+ describe("Calculator", () => {
63
+ it("adds two numbers", ({ task }) => {
64
+ story.init(task);
65
+
66
+ story.given("two numbers 2 and 3");
67
+ const a = 2, b = 3;
68
+
69
+ story.when("they are added");
70
+ const result = add(a, b);
71
+
72
+ story.then("the result is 5");
73
+ expect(result).toBe(5);
74
+ });
75
+ });
76
+ ```
77
+
78
+ Key change: add `({ task })` to the callback parameter.
79
+
80
+ ### Step 3: Minimal story (test name only)
81
+
82
+ If you want a test to appear in docs without step markers:
83
+
84
+ ```typescript
85
+ it("subtracts two numbers", ({ task }) => {
86
+ story.init(task);
87
+ // Test runs normally, appears in report with test name as scenario title
88
+ expect(subtract(10, 4)).toBe(6);
89
+ });
90
+ ```
91
+
92
+ ### Step 4: Add doc entries for richer output
93
+
94
+ ```typescript
95
+ it("handles division by zero", ({ task }) => {
96
+ story.init(task, { tags: ["edge-case"] });
97
+
98
+ story.given("a divisor of zero");
99
+ story.note("This tests the error handling path");
100
+
101
+ story.when("division is attempted");
102
+ const fn = () => divide(10, 0);
103
+
104
+ story.then("an error is thrown");
105
+ expect(fn).toThrow("Division by zero");
106
+ });
107
+ ```
108
+
109
+ ### Progressive adoption
110
+
111
+ Convert tests one file at a time. Non-story tests continue working normally. The reporter only processes files matching `*.story.test.ts` with `story.init()` calls.
112
+
113
+ ## Common Mistakes
114
+
115
+ ### HIGH Forgetting ({ task }) in callback
116
+
117
+ Wrong:
118
+
119
+ ```typescript
120
+ it("my test", () => {
121
+ story.init(task); // ReferenceError: task is not defined
122
+ });
123
+ ```
124
+
125
+ Correct:
126
+
127
+ ```typescript
128
+ it("my test", ({ task }) => {
129
+ story.init(task);
130
+ });
131
+ ```
132
+
133
+ Vitest passes the test context as the first callback argument. You must destructure `task` from it.
134
+
135
+ Source: packages/executable-stories-vitest/src/story-api.ts
136
+
137
+ ### MEDIUM Using wrong file extension
138
+
139
+ Wrong: `calculator.story.spec.ts` (Playwright convention)
140
+ Correct: `calculator.story.test.ts` (Vitest convention)
141
+
142
+ Source: CLAUDE.md — file naming conventions
@@ -0,0 +1,201 @@
1
+ ---
2
+ name: vitest-reporter-setup
3
+ description: >
4
+ Configure StoryReporter in vitest.config.ts for executable-stories-vitest.
5
+ Import from executable-stories-vitest/reporter subpath. OutputConfig with
6
+ formats, outputDir, outputName. Output modes: aggregated, colocated
7
+ (mirrored/adjacent). Markdown, HTML, JUnit, Cucumber JSON options.
8
+ GitHub Actions summary. rawRunPath for CLI consumption.
9
+ type: core
10
+ library: executable-stories-vitest
11
+ library_version: "7.0.1"
12
+ sources:
13
+ - "jagreehal/executable-stories:packages/executable-stories-vitest/src/reporter.ts"
14
+ - "jagreehal/executable-stories:apps/docs-site/src/content/docs/vitest/vitest-config.md"
15
+ ---
16
+
17
+ # executable-stories-vitest — Reporter Setup
18
+
19
+ ## Setup
20
+
21
+ ```typescript
22
+ // vitest.config.ts
23
+ import { defineConfig } from "vitest/config";
24
+ import { StoryReporter } from "executable-stories-vitest/reporter";
25
+
26
+ export default defineConfig({
27
+ test: {
28
+ reporters: [
29
+ "default",
30
+ new StoryReporter({
31
+ formats: ["markdown", "html"],
32
+ outputDir: "docs",
33
+ outputName: "user-stories",
34
+ }),
35
+ ],
36
+ },
37
+ });
38
+ ```
39
+
40
+ Peer dependency: `executable-stories-formatters` must be installed.
41
+
42
+ ## Core Patterns
43
+
44
+ ### Output modes
45
+
46
+ ```typescript
47
+ // Aggregated (default) — one file per format
48
+ new StoryReporter({
49
+ formats: ["markdown"],
50
+ outputDir: "docs",
51
+ outputName: "user-stories",
52
+ output: { mode: "aggregated" },
53
+ })
54
+ // → docs/user-stories.md
55
+
56
+ // Colocated mirrored — one file per source, directory mirrored
57
+ new StoryReporter({
58
+ formats: ["markdown"],
59
+ outputDir: "docs",
60
+ output: { mode: "colocated", colocatedStyle: "mirrored" },
61
+ })
62
+ // test/auth/login.story.test.ts → docs/test/auth/login.story.md
63
+
64
+ // Colocated adjacent — written next to the test file
65
+ new StoryReporter({
66
+ formats: ["markdown"],
67
+ output: { mode: "colocated", colocatedStyle: "adjacent" },
68
+ })
69
+ // test/auth/login.story.test.ts → test/auth/login.story.md
70
+ ```
71
+
72
+ ### Format-specific options
73
+
74
+ ```typescript
75
+ new StoryReporter({
76
+ formats: ["markdown", "html", "junit", "cucumber-json"],
77
+ outputDir: "reports",
78
+ markdown: {
79
+ title: "User Stories",
80
+ includeStatusIcons: true,
81
+ includeErrors: true,
82
+ includeMetadata: true,
83
+ sortScenarios: "source",
84
+ ticketUrlTemplate: "https://jira.example.com/browse/{ticket}",
85
+ },
86
+ html: {
87
+ title: "Test Report",
88
+ darkMode: true,
89
+ searchable: true,
90
+ startCollapsed: false,
91
+ embedScreenshots: true,
92
+ },
93
+ junit: {
94
+ suiteName: "My Test Suite",
95
+ includeOutput: true,
96
+ },
97
+ cucumberJson: { pretty: true },
98
+ })
99
+ ```
100
+
101
+ ### Pattern-based output rules
102
+
103
+ ```typescript
104
+ new StoryReporter({
105
+ formats: ["markdown"],
106
+ output: {
107
+ mode: "aggregated",
108
+ rules: [
109
+ {
110
+ match: "test/api/**",
111
+ mode: "colocated",
112
+ colocatedStyle: "adjacent",
113
+ formats: ["markdown", "html"],
114
+ },
115
+ {
116
+ match: "test/e2e/**",
117
+ outputDir: "docs/e2e",
118
+ outputName: "e2e-stories",
119
+ },
120
+ ],
121
+ },
122
+ })
123
+ ```
124
+
125
+ ### Raw run output for CLI
126
+
127
+ ```typescript
128
+ new StoryReporter({
129
+ formats: ["markdown"],
130
+ rawRunPath: "reports/raw-run.json",
131
+ enableGithubActionsSummary: true,
132
+ })
133
+ ```
134
+
135
+ ## Common Mistakes
136
+
137
+ ### HIGH Importing StoryReporter from main package entry
138
+
139
+ Wrong:
140
+
141
+ ```typescript
142
+ import { StoryReporter } from "executable-stories-vitest";
143
+ ```
144
+
145
+ Correct:
146
+
147
+ ```typescript
148
+ import { StoryReporter } from "executable-stories-vitest/reporter";
149
+ ```
150
+
151
+ The main entry exports a guard class that throws at construction time. The real `StoryReporter` lives at the `/reporter` subpath to keep heavy formatter dependencies out of test code.
152
+
153
+ Source: packages/executable-stories-vitest/src/index.ts
154
+
155
+ ### HIGH Passing a string instead of OutputConfig object
156
+
157
+ Wrong:
158
+
159
+ ```typescript
160
+ new StoryReporter({ output: "docs/user-stories.md" })
161
+ ```
162
+
163
+ Correct:
164
+
165
+ ```typescript
166
+ new StoryReporter({
167
+ formats: ["markdown"],
168
+ outputDir: "docs",
169
+ outputName: "user-stories",
170
+ })
171
+ ```
172
+
173
+ The `output` property expects an `OutputConfig` object with `mode`, `colocatedStyle`, and `rules`. A string is silently treated as an object with all `undefined` fields, falling back to defaults.
174
+
175
+ Source: packages/executable-stories-vitest/src/reporter.ts
176
+
177
+ ### MEDIUM Default format is cucumber-json, not markdown
178
+
179
+ Wrong assumption:
180
+
181
+ ```typescript
182
+ // Expecting markdown output
183
+ new StoryReporter({ outputDir: "docs" })
184
+ // → docs/test-results.cucumber.json (not .md)
185
+ ```
186
+
187
+ Correct:
188
+
189
+ ```typescript
190
+ new StoryReporter({
191
+ formats: ["markdown"],
192
+ outputDir: "docs",
193
+ })
194
+ ```
195
+
196
+ The default format is `["cucumber-json"]`, not `["markdown"]`. Always specify `formats` explicitly.
197
+
198
+ Source: packages/executable-stories-vitest/src/reporter.ts
199
+
200
+ See also: vitest-story-api/SKILL.md — Stories need the reporter to produce output
201
+ See also: formatters-cli/SKILL.md — Reporter produces RawRun that CLI consumes
@@ -0,0 +1,243 @@
1
+ ---
2
+ name: vitest-story-api
3
+ description: >
4
+ Write BDD stories in Vitest using executable-stories-vitest. Callback-only
5
+ API: story.init(task) with ({ task }) destructuring. Steps: given, when,
6
+ then, and, but. Doc entries: json, kv, code, table, link, section, mermaid,
7
+ note, tag, screenshot, custom. Auto-And keyword conversion. No top-level
8
+ then export. Aliases: arrange, act, assert. Inline docs via second argument.
9
+ type: core
10
+ library: executable-stories-vitest
11
+ library_version: "7.0.1"
12
+ sources:
13
+ - "jagreehal/executable-stories:packages/executable-stories-vitest/src/story-api.ts"
14
+ - "jagreehal/executable-stories:apps/docs-site/src/content/docs/vitest/vitest-story-api.md"
15
+ ---
16
+
17
+ # executable-stories-vitest — Story API
18
+
19
+ ## Setup
20
+
21
+ ```typescript
22
+ import { describe, expect, it } from "vitest";
23
+ import { story } from "executable-stories-vitest";
24
+
25
+ describe("Cart checkout", () => {
26
+ it("applies discount code", ({ task }) => {
27
+ story.init(task, { tags: ["checkout"], ticket: "CART-42" });
28
+
29
+ story.given("a cart with items totaling $100");
30
+ const cart = createCart([{ name: "Shirt", price: 100 }]);
31
+
32
+ story.when("a 20% discount code is applied");
33
+ applyDiscount(cart, "SAVE20");
34
+
35
+ story.then("the total is $80");
36
+ expect(cart.total).toBe(80);
37
+
38
+ story.and("the discount is shown in the summary");
39
+ expect(cart.discounts).toHaveLength(1);
40
+ });
41
+ });
42
+ ```
43
+
44
+ File naming: `*.story.test.ts` or `*.story.spec.ts`.
45
+
46
+ ## Core Patterns
47
+
48
+ ### Step markers with Auto-And conversion
49
+
50
+ First call to `given()`, `when()`, or `then()` renders the keyword as-is. Subsequent calls to the same keyword in the same story auto-convert to "And". Explicit `and()` always renders "And". Explicit `but()` always renders "But" and never auto-converts.
51
+
52
+ ```typescript
53
+ it("blocks suspended user login", ({ task }) => {
54
+ story.init(task);
55
+
56
+ story.given("the user account exists"); // renders "Given"
57
+ story.given("the account is suspended"); // renders "And" (auto-converted)
58
+ story.when("the user submits valid credentials");
59
+ story.then("the user sees an error message");
60
+ story.but("the user is not logged in"); // renders "But" (always)
61
+ story.but("no session is created"); // renders "But" (always)
62
+ });
63
+ ```
64
+
65
+ ### Doc entries attached to steps
66
+
67
+ ```typescript
68
+ it("processes payment", ({ task }) => {
69
+ story.init(task);
70
+
71
+ story.given("a valid payment request");
72
+ story.json({ label: "Request payload", value: { amount: 50, currency: "USD" } });
73
+ story.kv({ label: "Gateway", value: "stripe" });
74
+
75
+ story.when("the payment is submitted");
76
+ story.code({ label: "Response", content: '{ "status": "ok" }', lang: "json" });
77
+
78
+ story.then("the order is confirmed");
79
+ story.table({
80
+ label: "Order summary",
81
+ columns: ["Item", "Qty", "Price"],
82
+ rows: [["Widget", "2", "$25"]],
83
+ });
84
+ story.link({ label: "API docs", url: "https://docs.example.com/payments" });
85
+ story.note("Payment processed in sandbox mode");
86
+ });
87
+ ```
88
+
89
+ ### Inline docs via second argument
90
+
91
+ ```typescript
92
+ story.given("valid credentials", {
93
+ json: { label: "Credentials", value: { user: "alice", role: "admin" } },
94
+ note: "Password masked for security",
95
+ });
96
+ ```
97
+
98
+ ### Step wrappers with timing
99
+
100
+ ```typescript
101
+ it("fetches user profile", ({ task }) => {
102
+ story.init(task);
103
+
104
+ story.given("a registered user");
105
+ const userId = "user-123";
106
+
107
+ const profile = await story.fn("When", "the profile is fetched", async () => {
108
+ return fetchProfile(userId);
109
+ });
110
+
111
+ await story.expect("the profile contains the correct name", () => {
112
+ expect(profile.name).toBe("Alice");
113
+ });
114
+ });
115
+ ```
116
+
117
+ ## Common Mistakes
118
+
119
+ ### CRITICAL Missing task argument in story.init()
120
+
121
+ Wrong:
122
+
123
+ ```typescript
124
+ it("my test", () => {
125
+ story.init();
126
+ story.given("something");
127
+ });
128
+ ```
129
+
130
+ Correct:
131
+
132
+ ```typescript
133
+ it("my test", ({ task }) => {
134
+ story.init(task);
135
+ story.given("something");
136
+ });
137
+ ```
138
+
139
+ Without `task`, story metadata is not linked to the test and the reporter cannot capture it. The `task` object comes from Vitest's test callback destructuring.
140
+
141
+ Source: packages/executable-stories-vitest/src/story-api.ts
142
+
143
+ ### CRITICAL Importing top-level then from the package
144
+
145
+ Wrong:
146
+
147
+ ```typescript
148
+ import { story, then } from "executable-stories-vitest";
149
+ ```
150
+
151
+ Correct:
152
+
153
+ ```typescript
154
+ import { story } from "executable-stories-vitest";
155
+
156
+ it("my test", ({ task }) => {
157
+ story.init(task);
158
+ story.then("result is correct");
159
+ });
160
+ ```
161
+
162
+ A top-level `then` export would make the module namespace thenable. Tools using `await import("executable-stories-vitest")` would treat the module as a Promise and invoke `then` unexpectedly. All step functions exist only on the `story` object.
163
+
164
+ Source: CLAUDE.md — "Vitest: do not export top-level then"
165
+
166
+ ### HIGH Expecting but() to auto-convert to And
167
+
168
+ Wrong:
169
+
170
+ ```typescript
171
+ story.then("the user sees a success message");
172
+ story.but("no email is sent"); // Developer expects this to render "And"
173
+ ```
174
+
175
+ Correct:
176
+
177
+ ```typescript
178
+ // but() always renders "But" — this is intentional for negative/contrast intent
179
+ story.then("the user sees a success message");
180
+ story.but("no email is sent"); // Renders "But" (correct for contrast)
181
+
182
+ // Use and() if you want "And"
183
+ story.then("the user sees a success message");
184
+ story.and("no email is sent"); // Renders "And"
185
+ ```
186
+
187
+ `but()` expresses negative intent or contrast and never auto-converts. This matches Gherkin semantics.
188
+
189
+ Source: packages/executable-stories-vitest/src/story-api.ts
190
+
191
+ ### HIGH Calling steps before story.init()
192
+
193
+ Wrong:
194
+
195
+ ```typescript
196
+ it("my test", ({ task }) => {
197
+ story.given("something");
198
+ story.init(task);
199
+ });
200
+ ```
201
+
202
+ Correct:
203
+
204
+ ```typescript
205
+ it("my test", ({ task }) => {
206
+ story.init(task);
207
+ story.given("something");
208
+ });
209
+ ```
210
+
211
+ Steps called before `init()` are silently dropped because no story context exists yet.
212
+
213
+ Source: packages/eslint-plugin-executable-stories-vitest/src/rules/require-init-before-steps.ts
214
+
215
+ See also: vitest-reporter-setup/SKILL.md — Stories need a reporter to produce output
216
+ See also: eslint-vitest-rules/SKILL.md — ESLint enforces correct story.init() usage
217
+
218
+ ## Parameterized Scenarios (Scenario Outline equivalent)
219
+
220
+ Use Vitest's `it.each` with `story()` to produce one scenario per data row — the framework-native replacement for Cucumber's Scenario Outline + Examples.
221
+
222
+ ```ts
223
+ import { story } from "executable-stories-vitest";
224
+
225
+ const cases = [
226
+ { input: 1, expected: 2 },
227
+ { input: 2, expected: 4 },
228
+ { input: 3, expected: 6 },
229
+ ];
230
+
231
+ describe("Doubling", () => {
232
+ it.each(cases)("doubles $input to $expected", ({ input, expected }) => {
233
+ story(`Doubles ${input} to ${expected}`, (s) => {
234
+ s.given(`the input is ${input}`);
235
+ s.when("the doubler runs");
236
+ s.then(`the result is ${expected}`);
237
+ expect(input * 2).toBe(expected);
238
+ });
239
+ });
240
+ });
241
+ ```
242
+
243
+ Each iteration produces a separate scenario in the generated report. Use interpolated titles so each scenario has a distinct, descriptive name.