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/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/story-api.ts
|
|
2
|
+
import { expect, onTestFinished } from "vitest";
|
|
2
3
|
import { createRequire } from "module";
|
|
3
4
|
import { tryGetActiveOtelContext, resolveTraceUrl } from "executable-stories-core/utils/otel-detect";
|
|
4
5
|
import { buildHtmlDocEntry } from "executable-stories-core/utils/doc-builders";
|
|
@@ -162,6 +163,17 @@ function convertStoryDocsToEntries(docs) {
|
|
|
162
163
|
}
|
|
163
164
|
return entries;
|
|
164
165
|
}
|
|
166
|
+
function feature(input) {
|
|
167
|
+
declaredFeature = { input };
|
|
168
|
+
}
|
|
169
|
+
var declaredFeature = null;
|
|
170
|
+
function attachFeature(task) {
|
|
171
|
+
if (!declaredFeature) return;
|
|
172
|
+
const file = task.file?.name;
|
|
173
|
+
declaredFeature.file ??= file;
|
|
174
|
+
if (declaredFeature.file !== file) return;
|
|
175
|
+
task.meta.storyFeature = { ...declaredFeature.input, sourceFile: file };
|
|
176
|
+
}
|
|
165
177
|
function init(task, options) {
|
|
166
178
|
const meta = {
|
|
167
179
|
scenario: task.name,
|
|
@@ -194,6 +206,7 @@ function init(task, options) {
|
|
|
194
206
|
}
|
|
195
207
|
}
|
|
196
208
|
task.meta.story = meta;
|
|
209
|
+
attachFeature(task);
|
|
197
210
|
activeContext = {
|
|
198
211
|
meta,
|
|
199
212
|
currentStep: null,
|
|
@@ -202,8 +215,41 @@ function init(task, options) {
|
|
|
202
215
|
attachments: [],
|
|
203
216
|
activeTimers: /* @__PURE__ */ new Map(),
|
|
204
217
|
timerCounter: 0,
|
|
205
|
-
traceUrlTemplate
|
|
218
|
+
traceUrlTemplate,
|
|
219
|
+
pendingStep: null
|
|
206
220
|
};
|
|
221
|
+
const ctx = activeContext;
|
|
222
|
+
try {
|
|
223
|
+
onTestFinished(() => {
|
|
224
|
+
flushPendingAssertions(ctx);
|
|
225
|
+
task.meta.story = ctx.meta;
|
|
226
|
+
});
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function flushPendingAssertions(ctx) {
|
|
231
|
+
const step = ctx.pendingStep;
|
|
232
|
+
const before = ctx.pendingFrom;
|
|
233
|
+
ctx.pendingStep = null;
|
|
234
|
+
ctx.pendingFrom = void 0;
|
|
235
|
+
if (!step || before === void 0) return;
|
|
236
|
+
const after = readAssertionCount();
|
|
237
|
+
if (after === void 0) return;
|
|
238
|
+
step.assertions = after - before;
|
|
239
|
+
}
|
|
240
|
+
function readAssertionCount() {
|
|
241
|
+
try {
|
|
242
|
+
const state2 = expect.getState?.();
|
|
243
|
+
return typeof state2?.assertionCalls === "number" ? state2.assertionCalls : void 0;
|
|
244
|
+
} catch {
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function recordAssertions(step, before) {
|
|
249
|
+
if (before === void 0) return;
|
|
250
|
+
const after = readAssertionCount();
|
|
251
|
+
if (after === void 0) return;
|
|
252
|
+
step.assertions = after - before;
|
|
207
253
|
}
|
|
208
254
|
function createStepMarker(keyword) {
|
|
209
255
|
function stepMarker(text, docsOrBody) {
|
|
@@ -222,8 +268,13 @@ function createStepMarker(keyword) {
|
|
|
222
268
|
docs: stepDocs,
|
|
223
269
|
...isCallback ? { wrapped: true } : {}
|
|
224
270
|
};
|
|
271
|
+
flushPendingAssertions(ctx);
|
|
225
272
|
ctx.meta.steps.push(step);
|
|
226
273
|
ctx.currentStep = step;
|
|
274
|
+
if (!isCallback) {
|
|
275
|
+
ctx.pendingStep = step;
|
|
276
|
+
ctx.pendingFrom = readAssertionCount();
|
|
277
|
+
}
|
|
227
278
|
syncMetaToTask();
|
|
228
279
|
if (isChildrenArray) {
|
|
229
280
|
const children = docsOrBody;
|
|
@@ -243,27 +294,32 @@ function createStepMarker(keyword) {
|
|
|
243
294
|
if (!isCallback) return;
|
|
244
295
|
const body = docsOrBody;
|
|
245
296
|
const start = performance.now();
|
|
297
|
+
const assertionsBefore = readAssertionCount();
|
|
246
298
|
try {
|
|
247
299
|
const result = body();
|
|
248
300
|
if (result instanceof Promise) {
|
|
249
301
|
return result.then(
|
|
250
302
|
(val) => {
|
|
251
303
|
step.durationMs = performance.now() - start;
|
|
304
|
+
recordAssertions(step, assertionsBefore);
|
|
252
305
|
syncMetaToTask();
|
|
253
306
|
return val;
|
|
254
307
|
},
|
|
255
308
|
(err) => {
|
|
256
309
|
step.durationMs = performance.now() - start;
|
|
310
|
+
recordAssertions(step, assertionsBefore);
|
|
257
311
|
syncMetaToTask();
|
|
258
312
|
throw err;
|
|
259
313
|
}
|
|
260
314
|
);
|
|
261
315
|
}
|
|
262
316
|
step.durationMs = performance.now() - start;
|
|
317
|
+
recordAssertions(step, assertionsBefore);
|
|
263
318
|
syncMetaToTask();
|
|
264
319
|
return result;
|
|
265
320
|
} catch (err) {
|
|
266
321
|
step.durationMs = performance.now() - start;
|
|
322
|
+
recordAssertions(step, assertionsBefore);
|
|
267
323
|
syncMetaToTask();
|
|
268
324
|
throw err;
|
|
269
325
|
}
|
|
@@ -508,6 +564,7 @@ function attachSpans(spans, options) {
|
|
|
508
564
|
var story = {
|
|
509
565
|
// Core
|
|
510
566
|
init,
|
|
567
|
+
feature,
|
|
511
568
|
// BDD step markers
|
|
512
569
|
given: createStepMarker("Given"),
|
|
513
570
|
when: createStepMarker("When"),
|
|
@@ -651,6 +708,11 @@ var StoryReporter = class {
|
|
|
651
708
|
startTime = 0;
|
|
652
709
|
packageVersion;
|
|
653
710
|
gitSha;
|
|
711
|
+
/**
|
|
712
|
+
* Left unknown until onInit sees a config. Claiming full coverage without
|
|
713
|
+
* having looked would let a later merge retire scenarios on a guess.
|
|
714
|
+
*/
|
|
715
|
+
runScope;
|
|
654
716
|
coverageByFile = {};
|
|
655
717
|
constructor(options = {}) {
|
|
656
718
|
this.options = options;
|
|
@@ -659,6 +721,9 @@ var StoryReporter = class {
|
|
|
659
721
|
this.ctx = ctx;
|
|
660
722
|
this.startTime = Date.now();
|
|
661
723
|
const root = ctx.config?.root ?? process.cwd();
|
|
724
|
+
if (ctx.config) {
|
|
725
|
+
this.runScope = ctx.config.testNamePattern != null ? "filtered" : "full";
|
|
726
|
+
}
|
|
662
727
|
const includeMetadata = this.options.markdown?.includeMetadata ?? true;
|
|
663
728
|
if (includeMetadata) {
|
|
664
729
|
this.packageVersion = readPackageVersion(root);
|
|
@@ -675,14 +740,21 @@ var StoryReporter = class {
|
|
|
675
740
|
if (reason === "interrupted") return;
|
|
676
741
|
const root = this.ctx?.config?.root ?? process.cwd();
|
|
677
742
|
const rawTestCases = this.collectTestCases(testModules, root);
|
|
743
|
+
const coveredSourceFiles = this.collectCoveredSourceFiles(testModules, root);
|
|
744
|
+
const incompleteSourceFiles = this.collectIncompleteSourceFiles(testModules, root);
|
|
745
|
+
const features = collectFeatures(testModules, root);
|
|
678
746
|
const rawRun = {
|
|
679
747
|
testCases: rawTestCases,
|
|
748
|
+
...features.length > 0 ? { features } : {},
|
|
680
749
|
startedAtMs: this.startTime,
|
|
681
750
|
finishedAtMs: Date.now(),
|
|
682
751
|
projectRoot: root,
|
|
683
752
|
packageVersion: this.packageVersion,
|
|
684
753
|
gitSha: this.gitSha,
|
|
685
|
-
ci: detectCI()
|
|
754
|
+
ci: detectCI(),
|
|
755
|
+
...this.runScope ? { runScope: this.runScope } : {},
|
|
756
|
+
...coveredSourceFiles.length > 0 ? { coveredSourceFiles } : {},
|
|
757
|
+
...incompleteSourceFiles.length > 0 ? { incompleteSourceFiles } : {}
|
|
686
758
|
};
|
|
687
759
|
const rawRunPath = this.options.rawRunPath;
|
|
688
760
|
if (rawRunPath) {
|
|
@@ -759,6 +831,58 @@ var StoryReporter = class {
|
|
|
759
831
|
/**
|
|
760
832
|
* Collect test cases from Vitest test modules.
|
|
761
833
|
*/
|
|
834
|
+
/**
|
|
835
|
+
* Every module vitest ran, whether or not it produced a story.
|
|
836
|
+
*
|
|
837
|
+
* The collected test cases only name modules that produced something, so a
|
|
838
|
+
* file whose last story was deleted would otherwise look like a file that did
|
|
839
|
+
* not run and keep its old scenarios for good.
|
|
840
|
+
*/
|
|
841
|
+
collectCoveredSourceFiles(testModules, root) {
|
|
842
|
+
const files = /* @__PURE__ */ new Set();
|
|
843
|
+
for (const mod of testModules) {
|
|
844
|
+
const moduleId = mod.moduleId ?? mod.relativeModuleId ?? "";
|
|
845
|
+
if (!moduleId) continue;
|
|
846
|
+
const absolute = path.isAbsolute(moduleId) ? moduleId : path.resolve(root, moduleId);
|
|
847
|
+
files.add(toRelativePosix(absolute, root));
|
|
848
|
+
}
|
|
849
|
+
return [...files].sort();
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Files whose scenarios could not be collected in full.
|
|
853
|
+
*
|
|
854
|
+
* A module that errored, or a test that failed without ever declaring a
|
|
855
|
+
* story, is missing scenarios because the run broke. Treating that as
|
|
856
|
+
* deletion would throw away documentation at the exact moment the suite is
|
|
857
|
+
* unhealthy. Checked per test, not per file: one healthy story in a module
|
|
858
|
+
* does not vouch for its broken siblings.
|
|
859
|
+
*/
|
|
860
|
+
collectIncompleteSourceFiles(testModules, root) {
|
|
861
|
+
const incomplete = /* @__PURE__ */ new Set();
|
|
862
|
+
for (const mod of testModules) {
|
|
863
|
+
const moduleId = mod.moduleId ?? mod.relativeModuleId ?? "";
|
|
864
|
+
if (!moduleId) continue;
|
|
865
|
+
const absolute = path.isAbsolute(moduleId) ? moduleId : path.resolve(root, moduleId);
|
|
866
|
+
const sourceFile = toRelativePosix(absolute, root);
|
|
867
|
+
const errors = mod.errors?.() ?? [];
|
|
868
|
+
const collection = mod.children;
|
|
869
|
+
if (errors.length > 0 || !collection) {
|
|
870
|
+
const state2 = mod.state?.();
|
|
871
|
+
if (errors.length > 0 || state2 === "failed") incomplete.add(sourceFile);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
for (const test of collection.allTests()) {
|
|
875
|
+
const meta = this.getStoryMeta(test);
|
|
876
|
+
if (meta?.scenario && Array.isArray(meta.steps)) continue;
|
|
877
|
+
if (test.options?.mode === "todo") continue;
|
|
878
|
+
if (test.result?.()?.state === "failed") {
|
|
879
|
+
incomplete.add(sourceFile);
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return [...incomplete].sort();
|
|
885
|
+
}
|
|
762
886
|
collectTestCases(testModules, root) {
|
|
763
887
|
const testCases = [];
|
|
764
888
|
for (const mod of testModules) {
|
|
@@ -880,6 +1004,24 @@ var StoryReporter = class {
|
|
|
880
1004
|
function createStoryReporter(options) {
|
|
881
1005
|
return new StoryReporter(options);
|
|
882
1006
|
}
|
|
1007
|
+
function collectFeatures(testModules, root) {
|
|
1008
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1009
|
+
for (const mod of testModules) {
|
|
1010
|
+
const collection = mod.children;
|
|
1011
|
+
if (!collection) continue;
|
|
1012
|
+
const moduleId = mod.moduleId ?? mod.relativeModuleId ?? "";
|
|
1013
|
+
const absoluteModuleId = path.isAbsolute(moduleId) ? moduleId : path.resolve(root, moduleId);
|
|
1014
|
+
const sourceFile = toRelativePosix(absoluteModuleId, root);
|
|
1015
|
+
for (const test of collection.allTests()) {
|
|
1016
|
+
const declared = test.meta()?.storyFeature;
|
|
1017
|
+
if (declared?.title) {
|
|
1018
|
+
byFile.set(sourceFile, { ...declared, sourceFile });
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return [...byFile.values()];
|
|
1024
|
+
}
|
|
883
1025
|
|
|
884
1026
|
// src/index.ts
|
|
885
1027
|
var STORY_REPORTER_GUARD_MSG = 'Do not import StoryReporter from "executable-stories-vitest". In vitest.config, import it from "executable-stories-vitest/reporter".';
|