executable-stories-vitest 8.1.15 → 8.1.17

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.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { DocEntry, StepKeyword } from 'executable-stories-formatters';
2
2
  export { ColocatedStyle, DocEntry, DocPhase, FormatterOptions, NormalizedTicket, OutputFormat, OutputMode, OutputRule, STORY_META_KEY, StepKeyword, StepMode, StoryMeta, StoryStep } from 'executable-stories-formatters';
3
- export { StoryReporterOptions } from './reporter.cjs';
3
+ export { StoryReporterOptions, StoryReporterProtocol, VitestContext, createStoryReporter } from './reporter.cjs';
4
4
  import 'vitest/node';
5
5
 
6
6
  /**
@@ -501,19 +501,21 @@ type Story = typeof story;
501
501
  * });
502
502
  * ```
503
503
  *
504
- * In vitest.config, import StoryReporter from "executable-stories-vitest/reporter":
504
+ * In vitest.config, use createStoryReporter factory from "executable-stories-vitest/reporter":
505
505
  *
506
506
  * @example
507
507
  * ```ts
508
508
  * import { defineConfig } from "vitest/config";
509
- * import { StoryReporter } from "executable-stories-vitest/reporter";
509
+ * import { createStoryReporter } from "executable-stories-vitest/reporter";
510
510
  *
511
511
  * export default defineConfig({
512
512
  * test: {
513
- * reporters: ["default", new StoryReporter()],
513
+ * reporters: ["default", createStoryReporter()],
514
514
  * },
515
515
  * });
516
516
  * ```
517
+ *
518
+ * The factory provides type-safe reporter creation with no type casts.
517
519
  */
518
520
 
519
521
  /** @internal Guard: throws if used. Import StoryReporter from "executable-stories-vitest/reporter" in vitest.config. */
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { DocEntry, StepKeyword } from 'executable-stories-formatters';
2
2
  export { ColocatedStyle, DocEntry, DocPhase, FormatterOptions, NormalizedTicket, OutputFormat, OutputMode, OutputRule, STORY_META_KEY, StepKeyword, StepMode, StoryMeta, StoryStep } from 'executable-stories-formatters';
3
- export { StoryReporterOptions } from './reporter.js';
3
+ export { StoryReporterOptions, StoryReporterProtocol, VitestContext, createStoryReporter } from './reporter.js';
4
4
  import 'vitest/node';
5
5
 
6
6
  /**
@@ -501,19 +501,21 @@ type Story = typeof story;
501
501
  * });
502
502
  * ```
503
503
  *
504
- * In vitest.config, import StoryReporter from "executable-stories-vitest/reporter":
504
+ * In vitest.config, use createStoryReporter factory from "executable-stories-vitest/reporter":
505
505
  *
506
506
  * @example
507
507
  * ```ts
508
508
  * import { defineConfig } from "vitest/config";
509
- * import { StoryReporter } from "executable-stories-vitest/reporter";
509
+ * import { createStoryReporter } from "executable-stories-vitest/reporter";
510
510
  *
511
511
  * export default defineConfig({
512
512
  * test: {
513
- * reporters: ["default", new StoryReporter()],
513
+ * reporters: ["default", createStoryReporter()],
514
514
  * },
515
515
  * });
516
516
  * ```
517
+ *
518
+ * The factory provides type-safe reporter creation with no type casts.
517
519
  */
518
520
 
519
521
  /** @internal Guard: throws if used. Import StoryReporter from "executable-stories-vitest/reporter" in vitest.config. */
package/dist/index.js CHANGED
@@ -27,17 +27,17 @@ function looksLikeFilePath(name) {
27
27
  return false;
28
28
  }
29
29
  function extractSuitePath(task) {
30
- const path = [];
30
+ const path2 = [];
31
31
  const fileName = task.file?.name;
32
32
  let current = task.suite;
33
33
  while (current) {
34
34
  const name = current.name;
35
35
  if (name && name.trim() !== "" && name !== "<root>" && name !== fileName && !looksLikeFilePath(name)) {
36
- path.unshift(name);
36
+ path2.unshift(name);
37
37
  }
38
38
  current = current.suite;
39
39
  }
40
- return path.length > 0 ? path : void 0;
40
+ return path2.length > 0 ? path2 : void 0;
41
41
  }
42
42
  function normalizeTickets(ticket) {
43
43
  if (!ticket) return void 0;
@@ -479,9 +479,312 @@ var story = {
479
479
  // src/types.ts
480
480
  import { STORY_META_KEY } from "executable-stories-formatters";
481
481
 
482
+ // src/reporter.ts
483
+ import * as fs from "fs";
484
+ import * as path from "path";
485
+ import {
486
+ ReportGenerator,
487
+ canonicalizeRun,
488
+ readGitSha,
489
+ readPackageVersion,
490
+ detectCI,
491
+ sendNotifications,
492
+ toCIInfo,
493
+ loadHistory,
494
+ updateHistory,
495
+ saveHistory
496
+ } from "executable-stories-formatters";
497
+ function normalizeCoveragePayload(coverage) {
498
+ if (!coverage || typeof coverage !== "object" || Array.isArray(coverage))
499
+ return void 0;
500
+ const raw = coverage;
501
+ const data = {};
502
+ for (const [filePath, file] of Object.entries(raw)) {
503
+ if (file && typeof file === "object" && "s" in file && "f" in file && "b" in file) {
504
+ data[filePath] = file;
505
+ }
506
+ }
507
+ if (Object.keys(data).length === 0) return void 0;
508
+ return data;
509
+ }
510
+ function summarizeCoverage(data) {
511
+ let statementsTotal = 0;
512
+ let statementsCovered = 0;
513
+ let functionsTotal = 0;
514
+ let functionsCovered = 0;
515
+ let branchesTotal = 0;
516
+ let branchesCovered = 0;
517
+ let linesTotal = 0;
518
+ let linesCovered = 0;
519
+ let hasLines = false;
520
+ for (const file of Object.values(data)) {
521
+ for (const count of Object.values(file.s)) {
522
+ statementsTotal += 1;
523
+ if (count > 0) statementsCovered += 1;
524
+ }
525
+ for (const count of Object.values(file.f)) {
526
+ functionsTotal += 1;
527
+ if (count > 0) functionsCovered += 1;
528
+ }
529
+ for (const counts of Object.values(file.b)) {
530
+ for (const count of counts) {
531
+ branchesTotal += 1;
532
+ if (count > 0) branchesCovered += 1;
533
+ }
534
+ }
535
+ if (file.l) {
536
+ hasLines = true;
537
+ for (const count of Object.values(file.l)) {
538
+ linesTotal += 1;
539
+ if (count > 0) linesCovered += 1;
540
+ }
541
+ }
542
+ }
543
+ if (statementsTotal === 0 && functionsTotal === 0 && branchesTotal === 0 && !hasLines) {
544
+ return void 0;
545
+ }
546
+ const metric = (covered, total) => ({
547
+ total,
548
+ covered,
549
+ pct: total === 0 ? 100 : Math.round(covered / total * 100)
550
+ });
551
+ const summary = {
552
+ statements: metric(statementsCovered, statementsTotal),
553
+ branches: metric(branchesCovered, branchesTotal),
554
+ functions: metric(functionsCovered, functionsTotal)
555
+ };
556
+ if (hasLines) {
557
+ summary.lines = metric(linesCovered, linesTotal);
558
+ }
559
+ return summary;
560
+ }
561
+ function toCoverageSummary(data) {
562
+ if (!data) return void 0;
563
+ return {
564
+ statementsPct: data.statements.pct,
565
+ branchesPct: data.branches.pct,
566
+ functionsPct: data.functions.pct,
567
+ linesPct: data.lines?.pct
568
+ };
569
+ }
570
+ function toRelativePosix(absolutePath, projectRoot) {
571
+ return path.relative(projectRoot, absolutePath).split(path.sep).join("/");
572
+ }
573
+ var StoryReporter = class {
574
+ options;
575
+ ctx;
576
+ startTime = 0;
577
+ packageVersion;
578
+ gitSha;
579
+ coverageByFile = {};
580
+ constructor(options = {}) {
581
+ this.options = options;
582
+ }
583
+ onInit(ctx) {
584
+ this.ctx = ctx;
585
+ this.startTime = Date.now();
586
+ const root = ctx.config?.root ?? process.cwd();
587
+ const includeMetadata = this.options.markdown?.includeMetadata ?? true;
588
+ if (includeMetadata) {
589
+ this.packageVersion = readPackageVersion(root);
590
+ this.gitSha = readGitSha(root);
591
+ }
592
+ }
593
+ onCoverage(coverage) {
594
+ const data = normalizeCoveragePayload(coverage);
595
+ if (data) {
596
+ this.coverageByFile = { ...this.coverageByFile, ...data };
597
+ }
598
+ }
599
+ async onTestRunEnd(testModules, _unhandledErrors, reason) {
600
+ if (reason === "interrupted") return;
601
+ const root = this.ctx?.config?.root ?? process.cwd();
602
+ const rawTestCases = this.collectTestCases(testModules, root);
603
+ const rawRun = {
604
+ testCases: rawTestCases,
605
+ startedAtMs: this.startTime,
606
+ finishedAtMs: Date.now(),
607
+ projectRoot: root,
608
+ packageVersion: this.packageVersion,
609
+ gitSha: this.gitSha,
610
+ ci: detectCI()
611
+ };
612
+ const rawRunPath = this.options.rawRunPath;
613
+ if (rawRunPath) {
614
+ try {
615
+ const absolutePath = path.isAbsolute(rawRunPath) ? rawRunPath : path.join(root, rawRunPath);
616
+ const dir = path.dirname(absolutePath);
617
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
618
+ const payload = { schemaVersion: 1, ...rawRun };
619
+ fs.writeFileSync(absolutePath, JSON.stringify(payload, null, 2), "utf8");
620
+ } catch (err) {
621
+ console.error("Failed to write raw run JSON:", err);
622
+ }
623
+ }
624
+ const canonicalRun = canonicalizeRun(rawRun);
625
+ const coverageData = summarizeCoverage(this.coverageByFile);
626
+ if (coverageData) {
627
+ canonicalRun.coverage = toCoverageSummary(coverageData);
628
+ }
629
+ const generator = new ReportGenerator(this.options);
630
+ try {
631
+ const results = await generator.generate(canonicalRun);
632
+ const enableGithubSummary = this.options.enableGithubActionsSummary ?? true;
633
+ if (process.env.GITHUB_ACTIONS === "true" && enableGithubSummary) {
634
+ const markdownPaths = results.get("markdown") ?? [];
635
+ if (markdownPaths.length > 0) {
636
+ const firstPath = markdownPaths[0];
637
+ const content = fs.readFileSync(firstPath, "utf8");
638
+ await this.appendGithubSummary(content).catch(() => {
639
+ });
640
+ }
641
+ }
642
+ } catch (err) {
643
+ console.error("Failed to generate reports:", err);
644
+ }
645
+ try {
646
+ const histOpts = this.options.history;
647
+ if (histOpts?.filePath) {
648
+ const historyPath = path.isAbsolute(histOpts.filePath) ? histOpts.filePath : path.join(root, histOpts.filePath);
649
+ const store = loadHistory(
650
+ { filePath: historyPath },
651
+ {
652
+ readFile: (p) => {
653
+ try {
654
+ return fs.readFileSync(p, "utf8");
655
+ } catch {
656
+ return void 0;
657
+ }
658
+ },
659
+ logger: console
660
+ }
661
+ );
662
+ const updated = updateHistory({ store, run: canonicalRun, maxRuns: histOpts.maxRuns ?? 10 });
663
+ const dir = path.dirname(historyPath);
664
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
665
+ saveHistory(
666
+ { filePath: historyPath, store: updated },
667
+ { writeFile: (p, c) => fs.writeFileSync(p, c, "utf8") }
668
+ );
669
+ }
670
+ } catch (err) {
671
+ console.error("Failed to update history:", err);
672
+ }
673
+ try {
674
+ if (this.options.notification) {
675
+ await sendNotifications(
676
+ { run: canonicalRun, notification: this.options.notification },
677
+ { fetch: globalThis.fetch, logger: console, toCIInfo }
678
+ );
679
+ }
680
+ } catch (err) {
681
+ console.error("Failed to send notifications:", err);
682
+ }
683
+ }
684
+ /**
685
+ * Collect test cases from Vitest test modules.
686
+ */
687
+ collectTestCases(testModules, root) {
688
+ const testCases = [];
689
+ for (const mod of testModules) {
690
+ const collection = mod.children;
691
+ if (!collection) continue;
692
+ const moduleId = mod.moduleId ?? mod.relativeModuleId ?? "";
693
+ const absoluteModuleId = path.isAbsolute(moduleId) ? moduleId : path.resolve(root, moduleId);
694
+ const sourceFile = toRelativePosix(absoluteModuleId, root);
695
+ for (const test of collection.allTests()) {
696
+ const meta = this.getStoryMeta(test);
697
+ if (!meta?.scenario || !Array.isArray(meta.steps)) continue;
698
+ const result = test.result?.();
699
+ const state = result?.state ?? "pending";
700
+ const durationMs = typeof result?.duration === "number" ? result.duration : 0;
701
+ let errorMessage;
702
+ let errorStack;
703
+ if (state === "failed" && result) {
704
+ const errors = result.errors;
705
+ if (errors?.length) {
706
+ const err = errors[0];
707
+ errorMessage = err.message;
708
+ errorStack = err.stack;
709
+ }
710
+ }
711
+ const statusMap = {
712
+ passed: "pass",
713
+ failed: "fail",
714
+ skipped: "skip",
715
+ pending: "pending",
716
+ todo: "pending"
717
+ };
718
+ const taskMeta = test.meta();
719
+ const scopedAttachments = taskMeta?.storyAttachments ?? [];
720
+ const attachments = scopedAttachments.map((a) => ({
721
+ name: a.name,
722
+ mediaType: a.mediaType,
723
+ path: a.path,
724
+ body: a.body,
725
+ encoding: a.encoding,
726
+ charset: a.charset,
727
+ fileName: a.fileName,
728
+ stepIndex: a.stepIndex,
729
+ stepId: a.stepId
730
+ }));
731
+ const stepEvents = meta.steps.filter((s) => s.durationMs !== void 0).map((s, i) => ({
732
+ index: i,
733
+ title: s.text,
734
+ durationMs: s.durationMs
735
+ }));
736
+ const otelSpans = taskMeta?.otelSpans;
737
+ if (Array.isArray(otelSpans) && otelSpans.length > 0) {
738
+ const valid = otelSpans.filter(
739
+ (s) => s != null && typeof s === "object" && typeof s.spanId === "string" && typeof s.name === "string"
740
+ );
741
+ if (valid.length > 0) {
742
+ meta.otelSpans = valid;
743
+ }
744
+ }
745
+ const retryCount = result?.retryCount ?? 0;
746
+ const configuredRetries = Math.max(
747
+ retryCount,
748
+ test.retries ?? test.options?.retry ?? 0
749
+ );
750
+ testCases.push({
751
+ title: meta.scenario,
752
+ titlePath: meta.suitePath ? [...meta.suitePath, meta.scenario] : [meta.scenario],
753
+ story: meta,
754
+ sourceFile,
755
+ sourceLine: Math.max(1, meta.sourceOrder ?? 1),
756
+ status: statusMap[state] ?? "unknown",
757
+ durationMs,
758
+ error: errorMessage ? { message: errorMessage, stack: errorStack } : void 0,
759
+ attachments: attachments.length > 0 ? attachments : void 0,
760
+ stepEvents: stepEvents.length > 0 ? stepEvents : void 0,
761
+ retry: retryCount,
762
+ retries: configuredRetries
763
+ });
764
+ }
765
+ }
766
+ return testCases;
767
+ }
768
+ getStoryMeta(test) {
769
+ const meta = test.meta();
770
+ return meta?.["story"];
771
+ }
772
+ async appendGithubSummary(reportText) {
773
+ try {
774
+ const { summary } = await import("@actions/core");
775
+ summary.addRaw(reportText);
776
+ await summary.write();
777
+ } catch {
778
+ }
779
+ }
780
+ };
781
+ function createStoryReporter(options) {
782
+ return new StoryReporter(options);
783
+ }
784
+
482
785
  // src/index.ts
483
786
  var STORY_REPORTER_GUARD_MSG = 'Do not import StoryReporter from "executable-stories-vitest". In vitest.config, import it from "executable-stories-vitest/reporter".';
484
- var StoryReporter = class {
787
+ var StoryReporter2 = class {
485
788
  static __isGuard = true;
486
789
  constructor() {
487
790
  throw new Error(STORY_REPORTER_GUARD_MSG);
@@ -489,7 +792,8 @@ var StoryReporter = class {
489
792
  };
490
793
  export {
491
794
  STORY_META_KEY,
492
- StoryReporter,
795
+ StoryReporter2 as StoryReporter,
796
+ createStoryReporter,
493
797
  story
494
798
  };
495
799
  //# sourceMappingURL=index.js.map