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/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".';