executable-stories-vitest 8.6.6 → 8.8.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/README.md +12 -0
- package/dist/index.cjs +144 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -1
- package/dist/index.d.ts +49 -1
- package/dist/index.js +144 -2
- package/dist/index.js.map +1 -1
- package/dist/reporter.cjs +86 -1
- package/dist/reporter.cjs.map +1 -1
- package/dist/reporter.d.cts +28 -0
- package/dist/reporter.d.ts +28 -0
- package/dist/reporter.js +86 -1
- package/dist/reporter.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -153,6 +153,18 @@ story.init(task, {
|
|
|
153
153
|
|
|
154
154
|
## Developer experience
|
|
155
155
|
|
|
156
|
+
The reporter also maintains canonical state under `<outputDir>/by-file/`, one JSON
|
|
157
|
+
report per test source. Documentation formats render that accumulated suite; JUnit,
|
|
158
|
+
Cucumber, and release manifests contain only the current execution. Vitest name filters
|
|
159
|
+
are detected automatically. A full run may retire scenarios no longer reported, while a
|
|
160
|
+
filtered or incompletely collected file preserves its earlier scenarios and emits a
|
|
161
|
+
warning instead of deleting documentation.
|
|
162
|
+
|
|
163
|
+
Vitest's live assertion counter is attributed to steps. Marker-style assertions are
|
|
164
|
+
assigned to the current step until the next step or test end, and `story.expect` measures
|
|
165
|
+
its own body. An observable Then/And/But claim with zero assertions is marked in Markdown
|
|
166
|
+
and HTML and grades `none` in Evidence Review.
|
|
167
|
+
|
|
156
168
|
- **API:** Steps are on the `story` object: `story.given`, `story.when`, `story.then`, `story.and`, `story.but`. There are no top-level `given`/`when`/`then` exports (to avoid `then` being treated as a thenable on the package namespace).
|
|
157
169
|
- **Modifiers:** Use Vitest’s `.skip`, `.only`, `.todo`, `.fails`, `.concurrent` on step calls when needed (e.g. `story.then.skip('...')`). Use `story.skip` / `story.only` for scenario-level modifiers.
|
|
158
170
|
- **Attach story to a plain `it()`:** Call `story.init(task)` inside a normal `it('...', ({ task }) => { ... })` so that test appears in generated docs. Vitest does not export top-level step helpers or `doc`.
|
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(src_exports, {
|
|
|
38
38
|
module.exports = __toCommonJS(src_exports);
|
|
39
39
|
|
|
40
40
|
// src/story-api.ts
|
|
41
|
+
var import_vitest = require("vitest");
|
|
41
42
|
var import_node_module = require("module");
|
|
42
43
|
var import_otel_detect = require("executable-stories-core/utils/otel-detect");
|
|
43
44
|
var import_doc_builders = require("executable-stories-core/utils/doc-builders");
|
|
@@ -202,6 +203,17 @@ function convertStoryDocsToEntries(docs) {
|
|
|
202
203
|
}
|
|
203
204
|
return entries;
|
|
204
205
|
}
|
|
206
|
+
function feature(input) {
|
|
207
|
+
declaredFeature = { input };
|
|
208
|
+
}
|
|
209
|
+
var declaredFeature = null;
|
|
210
|
+
function attachFeature(task) {
|
|
211
|
+
if (!declaredFeature) return;
|
|
212
|
+
const file = task.file?.name;
|
|
213
|
+
declaredFeature.file ??= file;
|
|
214
|
+
if (declaredFeature.file !== file) return;
|
|
215
|
+
task.meta.storyFeature = { ...declaredFeature.input, sourceFile: file };
|
|
216
|
+
}
|
|
205
217
|
function init(task, options) {
|
|
206
218
|
const meta = {
|
|
207
219
|
scenario: task.name,
|
|
@@ -234,6 +246,7 @@ function init(task, options) {
|
|
|
234
246
|
}
|
|
235
247
|
}
|
|
236
248
|
task.meta.story = meta;
|
|
249
|
+
attachFeature(task);
|
|
237
250
|
activeContext = {
|
|
238
251
|
meta,
|
|
239
252
|
currentStep: null,
|
|
@@ -242,8 +255,41 @@ function init(task, options) {
|
|
|
242
255
|
attachments: [],
|
|
243
256
|
activeTimers: /* @__PURE__ */ new Map(),
|
|
244
257
|
timerCounter: 0,
|
|
245
|
-
traceUrlTemplate
|
|
258
|
+
traceUrlTemplate,
|
|
259
|
+
pendingStep: null
|
|
246
260
|
};
|
|
261
|
+
const ctx = activeContext;
|
|
262
|
+
try {
|
|
263
|
+
(0, import_vitest.onTestFinished)(() => {
|
|
264
|
+
flushPendingAssertions(ctx);
|
|
265
|
+
task.meta.story = ctx.meta;
|
|
266
|
+
});
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function flushPendingAssertions(ctx) {
|
|
271
|
+
const step = ctx.pendingStep;
|
|
272
|
+
const before = ctx.pendingFrom;
|
|
273
|
+
ctx.pendingStep = null;
|
|
274
|
+
ctx.pendingFrom = void 0;
|
|
275
|
+
if (!step || before === void 0) return;
|
|
276
|
+
const after = readAssertionCount();
|
|
277
|
+
if (after === void 0) return;
|
|
278
|
+
step.assertions = after - before;
|
|
279
|
+
}
|
|
280
|
+
function readAssertionCount() {
|
|
281
|
+
try {
|
|
282
|
+
const state2 = import_vitest.expect.getState?.();
|
|
283
|
+
return typeof state2?.assertionCalls === "number" ? state2.assertionCalls : void 0;
|
|
284
|
+
} catch {
|
|
285
|
+
return void 0;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function recordAssertions(step, before) {
|
|
289
|
+
if (before === void 0) return;
|
|
290
|
+
const after = readAssertionCount();
|
|
291
|
+
if (after === void 0) return;
|
|
292
|
+
step.assertions = after - before;
|
|
247
293
|
}
|
|
248
294
|
function createStepMarker(keyword) {
|
|
249
295
|
function stepMarker(text, docsOrBody) {
|
|
@@ -262,8 +308,13 @@ function createStepMarker(keyword) {
|
|
|
262
308
|
docs: stepDocs,
|
|
263
309
|
...isCallback ? { wrapped: true } : {}
|
|
264
310
|
};
|
|
311
|
+
flushPendingAssertions(ctx);
|
|
265
312
|
ctx.meta.steps.push(step);
|
|
266
313
|
ctx.currentStep = step;
|
|
314
|
+
if (!isCallback) {
|
|
315
|
+
ctx.pendingStep = step;
|
|
316
|
+
ctx.pendingFrom = readAssertionCount();
|
|
317
|
+
}
|
|
267
318
|
syncMetaToTask();
|
|
268
319
|
if (isChildrenArray) {
|
|
269
320
|
const children = docsOrBody;
|
|
@@ -283,27 +334,32 @@ function createStepMarker(keyword) {
|
|
|
283
334
|
if (!isCallback) return;
|
|
284
335
|
const body = docsOrBody;
|
|
285
336
|
const start = performance.now();
|
|
337
|
+
const assertionsBefore = readAssertionCount();
|
|
286
338
|
try {
|
|
287
339
|
const result = body();
|
|
288
340
|
if (result instanceof Promise) {
|
|
289
341
|
return result.then(
|
|
290
342
|
(val) => {
|
|
291
343
|
step.durationMs = performance.now() - start;
|
|
344
|
+
recordAssertions(step, assertionsBefore);
|
|
292
345
|
syncMetaToTask();
|
|
293
346
|
return val;
|
|
294
347
|
},
|
|
295
348
|
(err) => {
|
|
296
349
|
step.durationMs = performance.now() - start;
|
|
350
|
+
recordAssertions(step, assertionsBefore);
|
|
297
351
|
syncMetaToTask();
|
|
298
352
|
throw err;
|
|
299
353
|
}
|
|
300
354
|
);
|
|
301
355
|
}
|
|
302
356
|
step.durationMs = performance.now() - start;
|
|
357
|
+
recordAssertions(step, assertionsBefore);
|
|
303
358
|
syncMetaToTask();
|
|
304
359
|
return result;
|
|
305
360
|
} catch (err) {
|
|
306
361
|
step.durationMs = performance.now() - start;
|
|
362
|
+
recordAssertions(step, assertionsBefore);
|
|
307
363
|
syncMetaToTask();
|
|
308
364
|
throw err;
|
|
309
365
|
}
|
|
@@ -548,6 +604,7 @@ function attachSpans(spans, options) {
|
|
|
548
604
|
var story = {
|
|
549
605
|
// Core
|
|
550
606
|
init,
|
|
607
|
+
feature,
|
|
551
608
|
// BDD step markers
|
|
552
609
|
given: createStepMarker("Given"),
|
|
553
610
|
when: createStepMarker("When"),
|
|
@@ -680,6 +737,11 @@ var StoryReporter = class {
|
|
|
680
737
|
startTime = 0;
|
|
681
738
|
packageVersion;
|
|
682
739
|
gitSha;
|
|
740
|
+
/**
|
|
741
|
+
* Left unknown until onInit sees a config. Claiming full coverage without
|
|
742
|
+
* having looked would let a later merge retire scenarios on a guess.
|
|
743
|
+
*/
|
|
744
|
+
runScope;
|
|
683
745
|
coverageByFile = {};
|
|
684
746
|
constructor(options = {}) {
|
|
685
747
|
this.options = options;
|
|
@@ -688,6 +750,9 @@ var StoryReporter = class {
|
|
|
688
750
|
this.ctx = ctx;
|
|
689
751
|
this.startTime = Date.now();
|
|
690
752
|
const root = ctx.config?.root ?? process.cwd();
|
|
753
|
+
if (ctx.config) {
|
|
754
|
+
this.runScope = ctx.config.testNamePattern != null ? "filtered" : "full";
|
|
755
|
+
}
|
|
691
756
|
const includeMetadata = this.options.markdown?.includeMetadata ?? true;
|
|
692
757
|
if (includeMetadata) {
|
|
693
758
|
this.packageVersion = (0, import_executable_stories_formatters.readPackageVersion)(root);
|
|
@@ -704,14 +769,21 @@ var StoryReporter = class {
|
|
|
704
769
|
if (reason === "interrupted") return;
|
|
705
770
|
const root = this.ctx?.config?.root ?? process.cwd();
|
|
706
771
|
const rawTestCases = this.collectTestCases(testModules, root);
|
|
772
|
+
const coveredSourceFiles = this.collectCoveredSourceFiles(testModules, root);
|
|
773
|
+
const incompleteSourceFiles = this.collectIncompleteSourceFiles(testModules, root);
|
|
774
|
+
const features = collectFeatures(testModules, root);
|
|
707
775
|
const rawRun = {
|
|
708
776
|
testCases: rawTestCases,
|
|
777
|
+
...features.length > 0 ? { features } : {},
|
|
709
778
|
startedAtMs: this.startTime,
|
|
710
779
|
finishedAtMs: Date.now(),
|
|
711
780
|
projectRoot: root,
|
|
712
781
|
packageVersion: this.packageVersion,
|
|
713
782
|
gitSha: this.gitSha,
|
|
714
|
-
ci: (0, import_executable_stories_formatters.detectCI)()
|
|
783
|
+
ci: (0, import_executable_stories_formatters.detectCI)(),
|
|
784
|
+
...this.runScope ? { runScope: this.runScope } : {},
|
|
785
|
+
...coveredSourceFiles.length > 0 ? { coveredSourceFiles } : {},
|
|
786
|
+
...incompleteSourceFiles.length > 0 ? { incompleteSourceFiles } : {}
|
|
715
787
|
};
|
|
716
788
|
const rawRunPath = this.options.rawRunPath;
|
|
717
789
|
if (rawRunPath) {
|
|
@@ -788,6 +860,58 @@ var StoryReporter = class {
|
|
|
788
860
|
/**
|
|
789
861
|
* Collect test cases from Vitest test modules.
|
|
790
862
|
*/
|
|
863
|
+
/**
|
|
864
|
+
* Every module vitest ran, whether or not it produced a story.
|
|
865
|
+
*
|
|
866
|
+
* The collected test cases only name modules that produced something, so a
|
|
867
|
+
* file whose last story was deleted would otherwise look like a file that did
|
|
868
|
+
* not run and keep its old scenarios for good.
|
|
869
|
+
*/
|
|
870
|
+
collectCoveredSourceFiles(testModules, root) {
|
|
871
|
+
const files = /* @__PURE__ */ new Set();
|
|
872
|
+
for (const mod of testModules) {
|
|
873
|
+
const moduleId = mod.moduleId ?? mod.relativeModuleId ?? "";
|
|
874
|
+
if (!moduleId) continue;
|
|
875
|
+
const absolute = path.isAbsolute(moduleId) ? moduleId : path.resolve(root, moduleId);
|
|
876
|
+
files.add(toRelativePosix(absolute, root));
|
|
877
|
+
}
|
|
878
|
+
return [...files].sort();
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Files whose scenarios could not be collected in full.
|
|
882
|
+
*
|
|
883
|
+
* A module that errored, or a test that failed without ever declaring a
|
|
884
|
+
* story, is missing scenarios because the run broke. Treating that as
|
|
885
|
+
* deletion would throw away documentation at the exact moment the suite is
|
|
886
|
+
* unhealthy. Checked per test, not per file: one healthy story in a module
|
|
887
|
+
* does not vouch for its broken siblings.
|
|
888
|
+
*/
|
|
889
|
+
collectIncompleteSourceFiles(testModules, root) {
|
|
890
|
+
const incomplete = /* @__PURE__ */ new Set();
|
|
891
|
+
for (const mod of testModules) {
|
|
892
|
+
const moduleId = mod.moduleId ?? mod.relativeModuleId ?? "";
|
|
893
|
+
if (!moduleId) continue;
|
|
894
|
+
const absolute = path.isAbsolute(moduleId) ? moduleId : path.resolve(root, moduleId);
|
|
895
|
+
const sourceFile = toRelativePosix(absolute, root);
|
|
896
|
+
const errors = mod.errors?.() ?? [];
|
|
897
|
+
const collection = mod.children;
|
|
898
|
+
if (errors.length > 0 || !collection) {
|
|
899
|
+
const state2 = mod.state?.();
|
|
900
|
+
if (errors.length > 0 || state2 === "failed") incomplete.add(sourceFile);
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
for (const test of collection.allTests()) {
|
|
904
|
+
const meta = this.getStoryMeta(test);
|
|
905
|
+
if (meta?.scenario && Array.isArray(meta.steps)) continue;
|
|
906
|
+
if (test.options?.mode === "todo") continue;
|
|
907
|
+
if (test.result?.()?.state === "failed") {
|
|
908
|
+
incomplete.add(sourceFile);
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return [...incomplete].sort();
|
|
914
|
+
}
|
|
791
915
|
collectTestCases(testModules, root) {
|
|
792
916
|
const testCases = [];
|
|
793
917
|
for (const mod of testModules) {
|
|
@@ -909,6 +1033,24 @@ var StoryReporter = class {
|
|
|
909
1033
|
function createStoryReporter(options) {
|
|
910
1034
|
return new StoryReporter(options);
|
|
911
1035
|
}
|
|
1036
|
+
function collectFeatures(testModules, root) {
|
|
1037
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1038
|
+
for (const mod of testModules) {
|
|
1039
|
+
const collection = mod.children;
|
|
1040
|
+
if (!collection) continue;
|
|
1041
|
+
const moduleId = mod.moduleId ?? mod.relativeModuleId ?? "";
|
|
1042
|
+
const absoluteModuleId = path.isAbsolute(moduleId) ? moduleId : path.resolve(root, moduleId);
|
|
1043
|
+
const sourceFile = toRelativePosix(absoluteModuleId, root);
|
|
1044
|
+
for (const test of collection.allTests()) {
|
|
1045
|
+
const declared = test.meta()?.storyFeature;
|
|
1046
|
+
if (declared?.title) {
|
|
1047
|
+
byFile.set(sourceFile, { ...declared, sourceFile });
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
return [...byFile.values()];
|
|
1053
|
+
}
|
|
912
1054
|
|
|
913
1055
|
// src/index.ts
|
|
914
1056
|
var STORY_REPORTER_GUARD_MSG = 'Do not import StoryReporter from "executable-stories-vitest". In vitest.config, import it from "executable-stories-vitest/reporter".';
|