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/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";
@@ -214,8 +215,41 @@ function init(task, options) {
214
215
  attachments: [],
215
216
  activeTimers: /* @__PURE__ */ new Map(),
216
217
  timerCounter: 0,
217
- traceUrlTemplate
218
+ traceUrlTemplate,
219
+ pendingStep: null
218
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;
219
253
  }
220
254
  function createStepMarker(keyword) {
221
255
  function stepMarker(text, docsOrBody) {
@@ -234,8 +268,13 @@ function createStepMarker(keyword) {
234
268
  docs: stepDocs,
235
269
  ...isCallback ? { wrapped: true } : {}
236
270
  };
271
+ flushPendingAssertions(ctx);
237
272
  ctx.meta.steps.push(step);
238
273
  ctx.currentStep = step;
274
+ if (!isCallback) {
275
+ ctx.pendingStep = step;
276
+ ctx.pendingFrom = readAssertionCount();
277
+ }
239
278
  syncMetaToTask();
240
279
  if (isChildrenArray) {
241
280
  const children = docsOrBody;
@@ -255,27 +294,32 @@ function createStepMarker(keyword) {
255
294
  if (!isCallback) return;
256
295
  const body = docsOrBody;
257
296
  const start = performance.now();
297
+ const assertionsBefore = readAssertionCount();
258
298
  try {
259
299
  const result = body();
260
300
  if (result instanceof Promise) {
261
301
  return result.then(
262
302
  (val) => {
263
303
  step.durationMs = performance.now() - start;
304
+ recordAssertions(step, assertionsBefore);
264
305
  syncMetaToTask();
265
306
  return val;
266
307
  },
267
308
  (err) => {
268
309
  step.durationMs = performance.now() - start;
310
+ recordAssertions(step, assertionsBefore);
269
311
  syncMetaToTask();
270
312
  throw err;
271
313
  }
272
314
  );
273
315
  }
274
316
  step.durationMs = performance.now() - start;
317
+ recordAssertions(step, assertionsBefore);
275
318
  syncMetaToTask();
276
319
  return result;
277
320
  } catch (err) {
278
321
  step.durationMs = performance.now() - start;
322
+ recordAssertions(step, assertionsBefore);
279
323
  syncMetaToTask();
280
324
  throw err;
281
325
  }
@@ -664,6 +708,11 @@ var StoryReporter = class {
664
708
  startTime = 0;
665
709
  packageVersion;
666
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;
667
716
  coverageByFile = {};
668
717
  constructor(options = {}) {
669
718
  this.options = options;
@@ -672,6 +721,9 @@ var StoryReporter = class {
672
721
  this.ctx = ctx;
673
722
  this.startTime = Date.now();
674
723
  const root = ctx.config?.root ?? process.cwd();
724
+ if (ctx.config) {
725
+ this.runScope = ctx.config.testNamePattern != null ? "filtered" : "full";
726
+ }
675
727
  const includeMetadata = this.options.markdown?.includeMetadata ?? true;
676
728
  if (includeMetadata) {
677
729
  this.packageVersion = readPackageVersion(root);
@@ -688,6 +740,8 @@ var StoryReporter = class {
688
740
  if (reason === "interrupted") return;
689
741
  const root = this.ctx?.config?.root ?? process.cwd();
690
742
  const rawTestCases = this.collectTestCases(testModules, root);
743
+ const coveredSourceFiles = this.collectCoveredSourceFiles(testModules, root);
744
+ const incompleteSourceFiles = this.collectIncompleteSourceFiles(testModules, root);
691
745
  const features = collectFeatures(testModules, root);
692
746
  const rawRun = {
693
747
  testCases: rawTestCases,
@@ -697,7 +751,10 @@ var StoryReporter = class {
697
751
  projectRoot: root,
698
752
  packageVersion: this.packageVersion,
699
753
  gitSha: this.gitSha,
700
- ci: detectCI()
754
+ ci: detectCI(),
755
+ ...this.runScope ? { runScope: this.runScope } : {},
756
+ ...coveredSourceFiles.length > 0 ? { coveredSourceFiles } : {},
757
+ ...incompleteSourceFiles.length > 0 ? { incompleteSourceFiles } : {}
701
758
  };
702
759
  const rawRunPath = this.options.rawRunPath;
703
760
  if (rawRunPath) {
@@ -774,6 +831,58 @@ var StoryReporter = class {
774
831
  /**
775
832
  * Collect test cases from Vitest test modules.
776
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
+ }
777
886
  collectTestCases(testModules, root) {
778
887
  const testCases = [];
779
888
  for (const mod of testModules) {