executable-stories-vitest 8.7.0 → 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 +111 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +111 -2
- package/dist/index.js.map +1 -1
- package/dist/reporter.cjs +66 -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 +66 -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");
|
|
@@ -254,8 +255,41 @@ function init(task, options) {
|
|
|
254
255
|
attachments: [],
|
|
255
256
|
activeTimers: /* @__PURE__ */ new Map(),
|
|
256
257
|
timerCounter: 0,
|
|
257
|
-
traceUrlTemplate
|
|
258
|
+
traceUrlTemplate,
|
|
259
|
+
pendingStep: null
|
|
258
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;
|
|
259
293
|
}
|
|
260
294
|
function createStepMarker(keyword) {
|
|
261
295
|
function stepMarker(text, docsOrBody) {
|
|
@@ -274,8 +308,13 @@ function createStepMarker(keyword) {
|
|
|
274
308
|
docs: stepDocs,
|
|
275
309
|
...isCallback ? { wrapped: true } : {}
|
|
276
310
|
};
|
|
311
|
+
flushPendingAssertions(ctx);
|
|
277
312
|
ctx.meta.steps.push(step);
|
|
278
313
|
ctx.currentStep = step;
|
|
314
|
+
if (!isCallback) {
|
|
315
|
+
ctx.pendingStep = step;
|
|
316
|
+
ctx.pendingFrom = readAssertionCount();
|
|
317
|
+
}
|
|
279
318
|
syncMetaToTask();
|
|
280
319
|
if (isChildrenArray) {
|
|
281
320
|
const children = docsOrBody;
|
|
@@ -295,27 +334,32 @@ function createStepMarker(keyword) {
|
|
|
295
334
|
if (!isCallback) return;
|
|
296
335
|
const body = docsOrBody;
|
|
297
336
|
const start = performance.now();
|
|
337
|
+
const assertionsBefore = readAssertionCount();
|
|
298
338
|
try {
|
|
299
339
|
const result = body();
|
|
300
340
|
if (result instanceof Promise) {
|
|
301
341
|
return result.then(
|
|
302
342
|
(val) => {
|
|
303
343
|
step.durationMs = performance.now() - start;
|
|
344
|
+
recordAssertions(step, assertionsBefore);
|
|
304
345
|
syncMetaToTask();
|
|
305
346
|
return val;
|
|
306
347
|
},
|
|
307
348
|
(err) => {
|
|
308
349
|
step.durationMs = performance.now() - start;
|
|
350
|
+
recordAssertions(step, assertionsBefore);
|
|
309
351
|
syncMetaToTask();
|
|
310
352
|
throw err;
|
|
311
353
|
}
|
|
312
354
|
);
|
|
313
355
|
}
|
|
314
356
|
step.durationMs = performance.now() - start;
|
|
357
|
+
recordAssertions(step, assertionsBefore);
|
|
315
358
|
syncMetaToTask();
|
|
316
359
|
return result;
|
|
317
360
|
} catch (err) {
|
|
318
361
|
step.durationMs = performance.now() - start;
|
|
362
|
+
recordAssertions(step, assertionsBefore);
|
|
319
363
|
syncMetaToTask();
|
|
320
364
|
throw err;
|
|
321
365
|
}
|
|
@@ -693,6 +737,11 @@ var StoryReporter = class {
|
|
|
693
737
|
startTime = 0;
|
|
694
738
|
packageVersion;
|
|
695
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;
|
|
696
745
|
coverageByFile = {};
|
|
697
746
|
constructor(options = {}) {
|
|
698
747
|
this.options = options;
|
|
@@ -701,6 +750,9 @@ var StoryReporter = class {
|
|
|
701
750
|
this.ctx = ctx;
|
|
702
751
|
this.startTime = Date.now();
|
|
703
752
|
const root = ctx.config?.root ?? process.cwd();
|
|
753
|
+
if (ctx.config) {
|
|
754
|
+
this.runScope = ctx.config.testNamePattern != null ? "filtered" : "full";
|
|
755
|
+
}
|
|
704
756
|
const includeMetadata = this.options.markdown?.includeMetadata ?? true;
|
|
705
757
|
if (includeMetadata) {
|
|
706
758
|
this.packageVersion = (0, import_executable_stories_formatters.readPackageVersion)(root);
|
|
@@ -717,6 +769,8 @@ var StoryReporter = class {
|
|
|
717
769
|
if (reason === "interrupted") return;
|
|
718
770
|
const root = this.ctx?.config?.root ?? process.cwd();
|
|
719
771
|
const rawTestCases = this.collectTestCases(testModules, root);
|
|
772
|
+
const coveredSourceFiles = this.collectCoveredSourceFiles(testModules, root);
|
|
773
|
+
const incompleteSourceFiles = this.collectIncompleteSourceFiles(testModules, root);
|
|
720
774
|
const features = collectFeatures(testModules, root);
|
|
721
775
|
const rawRun = {
|
|
722
776
|
testCases: rawTestCases,
|
|
@@ -726,7 +780,10 @@ var StoryReporter = class {
|
|
|
726
780
|
projectRoot: root,
|
|
727
781
|
packageVersion: this.packageVersion,
|
|
728
782
|
gitSha: this.gitSha,
|
|
729
|
-
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 } : {}
|
|
730
787
|
};
|
|
731
788
|
const rawRunPath = this.options.rawRunPath;
|
|
732
789
|
if (rawRunPath) {
|
|
@@ -803,6 +860,58 @@ var StoryReporter = class {
|
|
|
803
860
|
/**
|
|
804
861
|
* Collect test cases from Vitest test modules.
|
|
805
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
|
+
}
|
|
806
915
|
collectTestCases(testModules, root) {
|
|
807
916
|
const testCases = [];
|
|
808
917
|
for (const mod of testModules) {
|