executable-stories-vitest 8.1.16 → 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.cjs +309 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +309 -5
- package/dist/index.js.map +1 -1
- package/dist/reporter.cjs +6 -1
- package/dist/reporter.cjs.map +1 -1
- package/dist/reporter.d.cts +10 -2
- package/dist/reporter.d.ts +10 -2
- package/dist/reporter.js +4 -0
- package/dist/reporter.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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 {
|
|
509
|
+
* import { createStoryReporter } from "executable-stories-vitest/reporter";
|
|
510
510
|
*
|
|
511
511
|
* export default defineConfig({
|
|
512
512
|
* test: {
|
|
513
|
-
* reporters: ["default",
|
|
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,
|
|
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 {
|
|
509
|
+
* import { createStoryReporter } from "executable-stories-vitest/reporter";
|
|
510
510
|
*
|
|
511
511
|
* export default defineConfig({
|
|
512
512
|
* test: {
|
|
513
|
-
* reporters: ["default",
|
|
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
|
|
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
|
-
|
|
36
|
+
path2.unshift(name);
|
|
37
37
|
}
|
|
38
38
|
current = current.suite;
|
|
39
39
|
}
|
|
40
|
-
return
|
|
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
|
|
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
|