executable-stories-jest 2.0.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.
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/reporter.ts
31
+ var reporter_exports = {};
32
+ __export(reporter_exports, {
33
+ default: () => StoryReporter
34
+ });
35
+ module.exports = __toCommonJS(reporter_exports);
36
+ var fs = __toESM(require("fs"), 1);
37
+ var path = __toESM(require("path"), 1);
38
+ var import_fast_glob = __toESM(require("fast-glob"), 1);
39
+ var import_executable_stories_formatters = require("executable-stories-formatters");
40
+ function toRelativePosix(absolutePath, projectRoot) {
41
+ return path.relative(projectRoot, absolutePath).split(path.sep).join("/");
42
+ }
43
+ var StoryReporter = class {
44
+ options;
45
+ startTime = 0;
46
+ packageVersion;
47
+ gitSha;
48
+ constructor(_globalConfig, reporterOptions = {}) {
49
+ this.options = reporterOptions;
50
+ }
51
+ /** Get the output directory for story JSON files */
52
+ getOutputDir() {
53
+ const baseDir = process.env.JEST_STORY_DOCS_DIR ?? ".jest-executable-stories";
54
+ return path.resolve(process.cwd(), baseDir);
55
+ }
56
+ /** Reset run artifacts (clear previous story files) */
57
+ resetRunArtifacts() {
58
+ try {
59
+ fs.rmSync(this.getOutputDir(), { recursive: true, force: true });
60
+ } catch {
61
+ }
62
+ }
63
+ /** Read story reports written by story.init() during test execution */
64
+ readStoryReports() {
65
+ const outputDir = this.getOutputDir();
66
+ if (!fs.existsSync(outputDir)) return [];
67
+ const files = import_fast_glob.default.sync("**/*.json", {
68
+ cwd: outputDir,
69
+ onlyFiles: true,
70
+ absolute: true
71
+ });
72
+ const reports = [];
73
+ for (const file of files) {
74
+ try {
75
+ const raw = fs.readFileSync(file, "utf8");
76
+ const parsed = JSON.parse(raw);
77
+ if (!parsed?.testFilePath || !Array.isArray(parsed.scenarios)) continue;
78
+ reports.push(parsed);
79
+ } catch {
80
+ }
81
+ }
82
+ return reports;
83
+ }
84
+ onRunStart() {
85
+ this.resetRunArtifacts();
86
+ this.startTime = Date.now();
87
+ const root = process.cwd();
88
+ const includeMetadata = this.options.markdown?.includeMetadata ?? true;
89
+ if (includeMetadata) {
90
+ this.packageVersion = (0, import_executable_stories_formatters.readPackageVersion)(root);
91
+ this.gitSha = (0, import_executable_stories_formatters.readGitSha)(root);
92
+ }
93
+ }
94
+ async onRunComplete(_testContexts, results) {
95
+ const root = process.cwd();
96
+ const fileResults = /* @__PURE__ */ new Map();
97
+ for (const testFileResult of results.testResults) {
98
+ fileResults.set(testFileResult.testFilePath, testFileResult);
99
+ }
100
+ const reports = this.readStoryReports();
101
+ const rawTestCases = [];
102
+ for (const report of reports) {
103
+ const fileResult = fileResults.get(report.testFilePath);
104
+ const sourceFile = toRelativePosix(report.testFilePath, root);
105
+ for (const meta of report.scenarios) {
106
+ if (!meta?.scenario) continue;
107
+ const matchingTest = fileResult?.testResults.find((test) => {
108
+ const expectedFullName = meta.suitePath ? [...meta.suitePath, meta.scenario].join(" > ") : meta.scenario;
109
+ return test.fullName === expectedFullName;
110
+ });
111
+ const statusMap = {
112
+ passed: "pass",
113
+ failed: "fail",
114
+ pending: "pending",
115
+ todo: "pending"
116
+ };
117
+ const status = matchingTest ? statusMap[matchingTest.status] ?? "unknown" : "pass";
118
+ const rawAttachments = (meta._attachments ?? []).map((a) => ({
119
+ name: a.name,
120
+ mediaType: a.mediaType,
121
+ path: a.path,
122
+ body: a.body,
123
+ encoding: a.encoding,
124
+ charset: a.charset,
125
+ fileName: a.fileName,
126
+ stepIndex: a.stepIndex,
127
+ stepId: a.stepId
128
+ }));
129
+ const stepEvents = meta.steps.filter((s) => s.durationMs !== void 0).map((s, i) => ({
130
+ index: i,
131
+ title: s.text,
132
+ durationMs: s.durationMs
133
+ }));
134
+ rawTestCases.push({
135
+ title: meta.scenario,
136
+ titlePath: meta.suitePath ? [...meta.suitePath, meta.scenario] : [meta.scenario],
137
+ story: meta,
138
+ sourceFile,
139
+ sourceLine: Math.max(1, meta.sourceOrder ?? 1),
140
+ status,
141
+ durationMs: matchingTest?.duration ?? 0,
142
+ error: matchingTest?.failureMessages?.length ? { message: matchingTest.failureMessages.join("\n") } : void 0,
143
+ attachments: rawAttachments.length > 0 ? rawAttachments : void 0,
144
+ stepEvents: stepEvents.length > 0 ? stepEvents : void 0,
145
+ retry: 0,
146
+ retries: 0
147
+ });
148
+ }
149
+ }
150
+ if (rawTestCases.length === 0) return;
151
+ const rawRun = {
152
+ testCases: rawTestCases,
153
+ startedAtMs: this.startTime,
154
+ finishedAtMs: Date.now(),
155
+ projectRoot: root,
156
+ packageVersion: this.packageVersion,
157
+ gitSha: this.gitSha,
158
+ ci: (0, import_executable_stories_formatters.detectCI)()
159
+ };
160
+ const rawRunPath = this.options.rawRunPath;
161
+ if (rawRunPath) {
162
+ const absolutePath = path.isAbsolute(rawRunPath) ? rawRunPath : path.join(root, rawRunPath);
163
+ const dir = path.dirname(absolutePath);
164
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
165
+ const payload = { schemaVersion: 1, ...rawRun };
166
+ fs.writeFileSync(absolutePath, JSON.stringify(payload, null, 2), "utf8");
167
+ }
168
+ const canonicalRun = (0, import_executable_stories_formatters.canonicalizeRun)(rawRun);
169
+ const generator = new import_executable_stories_formatters.ReportGenerator(this.options);
170
+ try {
171
+ await generator.generate(canonicalRun);
172
+ } catch (err) {
173
+ console.error("Failed to generate reports:", err);
174
+ }
175
+ }
176
+ };
177
+ //# sourceMappingURL=reporter.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reporter.ts"],"sourcesContent":["/**\n * Jest reporter for executable-stories.\n * Generates reports using the executable-stories-formatters package.\n *\n * Uses file-based communication: story.init() writes JSON files to\n * .jest-executable-stories/ which this reporter reads.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport fg from \"fast-glob\";\nimport type { StoryMeta } from \"executable-stories-formatters\";\n\n// Import from formatters package\nimport {\n ReportGenerator,\n canonicalizeRun,\n readGitSha,\n readPackageVersion,\n detectCI,\n type RawRun,\n type RawTestCase,\n type RawAttachment,\n type RawStepEvent,\n type FormatterOptions,\n} from \"executable-stories-formatters\";\n\n// Re-export types from formatters for convenience\nexport type {\n OutputFormat,\n OutputMode,\n ColocatedStyle,\n OutputRule,\n FormatterOptions,\n} from \"executable-stories-formatters\";\n\n// ============================================================================\n// Reporter Options (delegates to FormatterOptions)\n// ============================================================================\n\nexport interface StoryReporterOptions extends FormatterOptions {\n /** If set, write raw run JSON (schemaVersion 1) to this path for use with the executable-stories CLI/binary */\n rawRunPath?: string;\n}\n\n// ============================================================================\n// Internal Types\n// ============================================================================\n\n/** Shape of JSON files written by story.init() */\ninterface StoryFileReport {\n testFilePath: string;\n scenarios: (StoryMeta & {\n _attachments?: Array<{\n name: string;\n mediaType: string;\n path?: string;\n body?: string;\n encoding?: \"BASE64\" | \"IDENTITY\";\n charset?: string;\n fileName?: string;\n stepIndex?: number;\n stepId?: string;\n }>;\n })[];\n}\n\ninterface JestTestResult {\n testFilePath: string;\n testResults: Array<{\n fullName: string;\n status: \"passed\" | \"failed\" | \"pending\" | \"todo\";\n duration?: number;\n failureMessages?: string[];\n }>;\n}\n\ninterface JestAggregatedResult {\n testResults: JestTestResult[];\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Convert path to relative posix format.\n */\nfunction toRelativePosix(absolutePath: string, projectRoot: string): string {\n return path.relative(projectRoot, absolutePath).split(path.sep).join(\"/\");\n}\n\n// ============================================================================\n// Reporter Implementation\n// ============================================================================\n\nexport default class StoryReporter {\n private options: StoryReporterOptions;\n private startTime = 0;\n private packageVersion: string | undefined;\n private gitSha: string | undefined;\n\n constructor(_globalConfig: unknown, reporterOptions: StoryReporterOptions = {}) {\n this.options = reporterOptions;\n }\n\n /** Get the output directory for story JSON files */\n private getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n }\n\n /** Reset run artifacts (clear previous story files) */\n private resetRunArtifacts(): void {\n try {\n fs.rmSync(this.getOutputDir(), { recursive: true, force: true });\n } catch {\n // Ignore cleanup errors\n }\n }\n\n /** Read story reports written by story.init() during test execution */\n private readStoryReports(): StoryFileReport[] {\n const outputDir = this.getOutputDir();\n if (!fs.existsSync(outputDir)) return [];\n const files = fg.sync(\"**/*.json\", {\n cwd: outputDir,\n onlyFiles: true,\n absolute: true,\n });\n const reports: StoryFileReport[] = [];\n for (const file of files) {\n try {\n const raw = fs.readFileSync(file, \"utf8\");\n const parsed = JSON.parse(raw) as StoryFileReport;\n if (!parsed?.testFilePath || !Array.isArray(parsed.scenarios)) continue;\n reports.push(parsed);\n } catch {\n // Ignore unreadable or malformed files\n }\n }\n return reports;\n }\n\n onRunStart(): void {\n this.resetRunArtifacts();\n this.startTime = Date.now();\n const root = process.cwd();\n const includeMetadata = this.options.markdown?.includeMetadata ?? true;\n if (includeMetadata) {\n this.packageVersion = readPackageVersion(root);\n this.gitSha = readGitSha(root);\n }\n }\n\n async onRunComplete(\n _testContexts: Set<unknown>,\n results: JestAggregatedResult\n ): Promise<void> {\n const root = process.cwd();\n\n // Build map of test results by file for status lookup\n const fileResults = new Map<string, JestTestResult>();\n for (const testFileResult of results.testResults) {\n fileResults.set(testFileResult.testFilePath, testFileResult);\n }\n\n // Read story data from JSON files written by story.init()\n const reports = this.readStoryReports();\n\n // Collect test cases\n const rawTestCases: RawTestCase[] = [];\n\n for (const report of reports) {\n const fileResult = fileResults.get(report.testFilePath);\n const sourceFile = toRelativePosix(report.testFilePath, root);\n\n for (const meta of report.scenarios) {\n if (!meta?.scenario) continue;\n\n // Find matching test result\n const matchingTest = fileResult?.testResults.find((test) => {\n const expectedFullName = meta.suitePath\n ? [...meta.suitePath, meta.scenario].join(\" > \")\n : meta.scenario;\n return test.fullName === expectedFullName;\n });\n\n // Map Jest status to raw status\n const statusMap: Record<string, RawTestCase[\"status\"]> = {\n passed: \"pass\",\n failed: \"fail\",\n pending: \"pending\",\n todo: \"pending\",\n };\n\n const status = matchingTest\n ? statusMap[matchingTest.status] ?? \"unknown\"\n : \"pass\";\n\n // Map attachments\n const rawAttachments: RawAttachment[] = (meta._attachments ?? []).map((a) => ({\n name: a.name,\n mediaType: a.mediaType,\n path: a.path,\n body: a.body,\n encoding: a.encoding,\n charset: a.charset,\n fileName: a.fileName,\n stepIndex: a.stepIndex,\n stepId: a.stepId,\n }));\n\n // Extract step events (timing)\n const stepEvents: RawStepEvent[] = meta.steps\n .filter((s: { durationMs?: number }) => s.durationMs !== undefined)\n .map((s: { durationMs?: number; text: string }, i: number) => ({\n index: i,\n title: s.text,\n durationMs: s.durationMs,\n }));\n\n rawTestCases.push({\n title: meta.scenario,\n titlePath: meta.suitePath\n ? [...meta.suitePath, meta.scenario]\n : [meta.scenario],\n story: meta,\n sourceFile,\n sourceLine: Math.max(1, meta.sourceOrder ?? 1),\n status,\n durationMs: matchingTest?.duration ?? 0,\n error: matchingTest?.failureMessages?.length\n ? { message: matchingTest.failureMessages.join(\"\\n\") }\n : undefined,\n attachments: rawAttachments.length > 0 ? rawAttachments : undefined,\n stepEvents: stepEvents.length > 0 ? stepEvents : undefined,\n retry: 0,\n retries: 0,\n });\n }\n }\n\n if (rawTestCases.length === 0) return;\n\n // Build RawRun\n const rawRun: RawRun = {\n testCases: rawTestCases,\n startedAtMs: this.startTime,\n finishedAtMs: Date.now(),\n projectRoot: root,\n packageVersion: this.packageVersion,\n gitSha: this.gitSha,\n ci: detectCI(),\n };\n\n // Optionally write raw run JSON for CLI/binary consumption\n const rawRunPath = this.options.rawRunPath;\n if (rawRunPath) {\n const absolutePath = path.isAbsolute(rawRunPath)\n ? rawRunPath\n : path.join(root, rawRunPath);\n const dir = path.dirname(absolutePath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n const payload = { schemaVersion: 1, ...rawRun };\n fs.writeFileSync(absolutePath, JSON.stringify(payload, null, 2), \"utf8\");\n }\n\n // Canonicalize\n const canonicalRun = canonicalizeRun(rawRun);\n\n // Generate reports\n const generator = new ReportGenerator(this.options);\n try {\n await generator.generate(canonicalRun);\n } catch (err) {\n console.error(\"Failed to generate reports:\", err);\n }\n }\n}\n"],"mappings":"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","names":["fg"]}
@@ -0,0 +1,44 @@
1
+ import { FormatterOptions } from 'executable-stories-formatters';
2
+ export { ColocatedStyle, FormatterOptions, OutputFormat, OutputMode, OutputRule } from 'executable-stories-formatters';
3
+
4
+ /**
5
+ * Jest reporter for executable-stories.
6
+ * Generates reports using the executable-stories-formatters package.
7
+ *
8
+ * Uses file-based communication: story.init() writes JSON files to
9
+ * .jest-executable-stories/ which this reporter reads.
10
+ */
11
+
12
+ interface StoryReporterOptions extends FormatterOptions {
13
+ /** If set, write raw run JSON (schemaVersion 1) to this path for use with the executable-stories CLI/binary */
14
+ rawRunPath?: string;
15
+ }
16
+ interface JestTestResult {
17
+ testFilePath: string;
18
+ testResults: Array<{
19
+ fullName: string;
20
+ status: "passed" | "failed" | "pending" | "todo";
21
+ duration?: number;
22
+ failureMessages?: string[];
23
+ }>;
24
+ }
25
+ interface JestAggregatedResult {
26
+ testResults: JestTestResult[];
27
+ }
28
+ declare class StoryReporter {
29
+ private options;
30
+ private startTime;
31
+ private packageVersion;
32
+ private gitSha;
33
+ constructor(_globalConfig: unknown, reporterOptions?: StoryReporterOptions);
34
+ /** Get the output directory for story JSON files */
35
+ private getOutputDir;
36
+ /** Reset run artifacts (clear previous story files) */
37
+ private resetRunArtifacts;
38
+ /** Read story reports written by story.init() during test execution */
39
+ private readStoryReports;
40
+ onRunStart(): void;
41
+ onRunComplete(_testContexts: Set<unknown>, results: JestAggregatedResult): Promise<void>;
42
+ }
43
+
44
+ export { type StoryReporterOptions, StoryReporter as default };
@@ -0,0 +1,44 @@
1
+ import { FormatterOptions } from 'executable-stories-formatters';
2
+ export { ColocatedStyle, FormatterOptions, OutputFormat, OutputMode, OutputRule } from 'executable-stories-formatters';
3
+
4
+ /**
5
+ * Jest reporter for executable-stories.
6
+ * Generates reports using the executable-stories-formatters package.
7
+ *
8
+ * Uses file-based communication: story.init() writes JSON files to
9
+ * .jest-executable-stories/ which this reporter reads.
10
+ */
11
+
12
+ interface StoryReporterOptions extends FormatterOptions {
13
+ /** If set, write raw run JSON (schemaVersion 1) to this path for use with the executable-stories CLI/binary */
14
+ rawRunPath?: string;
15
+ }
16
+ interface JestTestResult {
17
+ testFilePath: string;
18
+ testResults: Array<{
19
+ fullName: string;
20
+ status: "passed" | "failed" | "pending" | "todo";
21
+ duration?: number;
22
+ failureMessages?: string[];
23
+ }>;
24
+ }
25
+ interface JestAggregatedResult {
26
+ testResults: JestTestResult[];
27
+ }
28
+ declare class StoryReporter {
29
+ private options;
30
+ private startTime;
31
+ private packageVersion;
32
+ private gitSha;
33
+ constructor(_globalConfig: unknown, reporterOptions?: StoryReporterOptions);
34
+ /** Get the output directory for story JSON files */
35
+ private getOutputDir;
36
+ /** Reset run artifacts (clear previous story files) */
37
+ private resetRunArtifacts;
38
+ /** Read story reports written by story.init() during test execution */
39
+ private readStoryReports;
40
+ onRunStart(): void;
41
+ onRunComplete(_testContexts: Set<unknown>, results: JestAggregatedResult): Promise<void>;
42
+ }
43
+
44
+ export { type StoryReporterOptions, StoryReporter as default };
@@ -0,0 +1,152 @@
1
+ // src/reporter.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import fg from "fast-glob";
5
+ import {
6
+ ReportGenerator,
7
+ canonicalizeRun,
8
+ readGitSha,
9
+ readPackageVersion,
10
+ detectCI
11
+ } from "executable-stories-formatters";
12
+ function toRelativePosix(absolutePath, projectRoot) {
13
+ return path.relative(projectRoot, absolutePath).split(path.sep).join("/");
14
+ }
15
+ var StoryReporter = class {
16
+ options;
17
+ startTime = 0;
18
+ packageVersion;
19
+ gitSha;
20
+ constructor(_globalConfig, reporterOptions = {}) {
21
+ this.options = reporterOptions;
22
+ }
23
+ /** Get the output directory for story JSON files */
24
+ getOutputDir() {
25
+ const baseDir = process.env.JEST_STORY_DOCS_DIR ?? ".jest-executable-stories";
26
+ return path.resolve(process.cwd(), baseDir);
27
+ }
28
+ /** Reset run artifacts (clear previous story files) */
29
+ resetRunArtifacts() {
30
+ try {
31
+ fs.rmSync(this.getOutputDir(), { recursive: true, force: true });
32
+ } catch {
33
+ }
34
+ }
35
+ /** Read story reports written by story.init() during test execution */
36
+ readStoryReports() {
37
+ const outputDir = this.getOutputDir();
38
+ if (!fs.existsSync(outputDir)) return [];
39
+ const files = fg.sync("**/*.json", {
40
+ cwd: outputDir,
41
+ onlyFiles: true,
42
+ absolute: true
43
+ });
44
+ const reports = [];
45
+ for (const file of files) {
46
+ try {
47
+ const raw = fs.readFileSync(file, "utf8");
48
+ const parsed = JSON.parse(raw);
49
+ if (!parsed?.testFilePath || !Array.isArray(parsed.scenarios)) continue;
50
+ reports.push(parsed);
51
+ } catch {
52
+ }
53
+ }
54
+ return reports;
55
+ }
56
+ onRunStart() {
57
+ this.resetRunArtifacts();
58
+ this.startTime = Date.now();
59
+ const root = process.cwd();
60
+ const includeMetadata = this.options.markdown?.includeMetadata ?? true;
61
+ if (includeMetadata) {
62
+ this.packageVersion = readPackageVersion(root);
63
+ this.gitSha = readGitSha(root);
64
+ }
65
+ }
66
+ async onRunComplete(_testContexts, results) {
67
+ const root = process.cwd();
68
+ const fileResults = /* @__PURE__ */ new Map();
69
+ for (const testFileResult of results.testResults) {
70
+ fileResults.set(testFileResult.testFilePath, testFileResult);
71
+ }
72
+ const reports = this.readStoryReports();
73
+ const rawTestCases = [];
74
+ for (const report of reports) {
75
+ const fileResult = fileResults.get(report.testFilePath);
76
+ const sourceFile = toRelativePosix(report.testFilePath, root);
77
+ for (const meta of report.scenarios) {
78
+ if (!meta?.scenario) continue;
79
+ const matchingTest = fileResult?.testResults.find((test) => {
80
+ const expectedFullName = meta.suitePath ? [...meta.suitePath, meta.scenario].join(" > ") : meta.scenario;
81
+ return test.fullName === expectedFullName;
82
+ });
83
+ const statusMap = {
84
+ passed: "pass",
85
+ failed: "fail",
86
+ pending: "pending",
87
+ todo: "pending"
88
+ };
89
+ const status = matchingTest ? statusMap[matchingTest.status] ?? "unknown" : "pass";
90
+ const rawAttachments = (meta._attachments ?? []).map((a) => ({
91
+ name: a.name,
92
+ mediaType: a.mediaType,
93
+ path: a.path,
94
+ body: a.body,
95
+ encoding: a.encoding,
96
+ charset: a.charset,
97
+ fileName: a.fileName,
98
+ stepIndex: a.stepIndex,
99
+ stepId: a.stepId
100
+ }));
101
+ const stepEvents = meta.steps.filter((s) => s.durationMs !== void 0).map((s, i) => ({
102
+ index: i,
103
+ title: s.text,
104
+ durationMs: s.durationMs
105
+ }));
106
+ rawTestCases.push({
107
+ title: meta.scenario,
108
+ titlePath: meta.suitePath ? [...meta.suitePath, meta.scenario] : [meta.scenario],
109
+ story: meta,
110
+ sourceFile,
111
+ sourceLine: Math.max(1, meta.sourceOrder ?? 1),
112
+ status,
113
+ durationMs: matchingTest?.duration ?? 0,
114
+ error: matchingTest?.failureMessages?.length ? { message: matchingTest.failureMessages.join("\n") } : void 0,
115
+ attachments: rawAttachments.length > 0 ? rawAttachments : void 0,
116
+ stepEvents: stepEvents.length > 0 ? stepEvents : void 0,
117
+ retry: 0,
118
+ retries: 0
119
+ });
120
+ }
121
+ }
122
+ if (rawTestCases.length === 0) return;
123
+ const rawRun = {
124
+ testCases: rawTestCases,
125
+ startedAtMs: this.startTime,
126
+ finishedAtMs: Date.now(),
127
+ projectRoot: root,
128
+ packageVersion: this.packageVersion,
129
+ gitSha: this.gitSha,
130
+ ci: detectCI()
131
+ };
132
+ const rawRunPath = this.options.rawRunPath;
133
+ if (rawRunPath) {
134
+ const absolutePath = path.isAbsolute(rawRunPath) ? rawRunPath : path.join(root, rawRunPath);
135
+ const dir = path.dirname(absolutePath);
136
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
137
+ const payload = { schemaVersion: 1, ...rawRun };
138
+ fs.writeFileSync(absolutePath, JSON.stringify(payload, null, 2), "utf8");
139
+ }
140
+ const canonicalRun = canonicalizeRun(rawRun);
141
+ const generator = new ReportGenerator(this.options);
142
+ try {
143
+ await generator.generate(canonicalRun);
144
+ } catch (err) {
145
+ console.error("Failed to generate reports:", err);
146
+ }
147
+ }
148
+ };
149
+ export {
150
+ StoryReporter as default
151
+ };
152
+ //# sourceMappingURL=reporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reporter.ts"],"sourcesContent":["/**\n * Jest reporter for executable-stories.\n * Generates reports using the executable-stories-formatters package.\n *\n * Uses file-based communication: story.init() writes JSON files to\n * .jest-executable-stories/ which this reporter reads.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport fg from \"fast-glob\";\nimport type { StoryMeta } from \"executable-stories-formatters\";\n\n// Import from formatters package\nimport {\n ReportGenerator,\n canonicalizeRun,\n readGitSha,\n readPackageVersion,\n detectCI,\n type RawRun,\n type RawTestCase,\n type RawAttachment,\n type RawStepEvent,\n type FormatterOptions,\n} from \"executable-stories-formatters\";\n\n// Re-export types from formatters for convenience\nexport type {\n OutputFormat,\n OutputMode,\n ColocatedStyle,\n OutputRule,\n FormatterOptions,\n} from \"executable-stories-formatters\";\n\n// ============================================================================\n// Reporter Options (delegates to FormatterOptions)\n// ============================================================================\n\nexport interface StoryReporterOptions extends FormatterOptions {\n /** If set, write raw run JSON (schemaVersion 1) to this path for use with the executable-stories CLI/binary */\n rawRunPath?: string;\n}\n\n// ============================================================================\n// Internal Types\n// ============================================================================\n\n/** Shape of JSON files written by story.init() */\ninterface StoryFileReport {\n testFilePath: string;\n scenarios: (StoryMeta & {\n _attachments?: Array<{\n name: string;\n mediaType: string;\n path?: string;\n body?: string;\n encoding?: \"BASE64\" | \"IDENTITY\";\n charset?: string;\n fileName?: string;\n stepIndex?: number;\n stepId?: string;\n }>;\n })[];\n}\n\ninterface JestTestResult {\n testFilePath: string;\n testResults: Array<{\n fullName: string;\n status: \"passed\" | \"failed\" | \"pending\" | \"todo\";\n duration?: number;\n failureMessages?: string[];\n }>;\n}\n\ninterface JestAggregatedResult {\n testResults: JestTestResult[];\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Convert path to relative posix format.\n */\nfunction toRelativePosix(absolutePath: string, projectRoot: string): string {\n return path.relative(projectRoot, absolutePath).split(path.sep).join(\"/\");\n}\n\n// ============================================================================\n// Reporter Implementation\n// ============================================================================\n\nexport default class StoryReporter {\n private options: StoryReporterOptions;\n private startTime = 0;\n private packageVersion: string | undefined;\n private gitSha: string | undefined;\n\n constructor(_globalConfig: unknown, reporterOptions: StoryReporterOptions = {}) {\n this.options = reporterOptions;\n }\n\n /** Get the output directory for story JSON files */\n private getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n }\n\n /** Reset run artifacts (clear previous story files) */\n private resetRunArtifacts(): void {\n try {\n fs.rmSync(this.getOutputDir(), { recursive: true, force: true });\n } catch {\n // Ignore cleanup errors\n }\n }\n\n /** Read story reports written by story.init() during test execution */\n private readStoryReports(): StoryFileReport[] {\n const outputDir = this.getOutputDir();\n if (!fs.existsSync(outputDir)) return [];\n const files = fg.sync(\"**/*.json\", {\n cwd: outputDir,\n onlyFiles: true,\n absolute: true,\n });\n const reports: StoryFileReport[] = [];\n for (const file of files) {\n try {\n const raw = fs.readFileSync(file, \"utf8\");\n const parsed = JSON.parse(raw) as StoryFileReport;\n if (!parsed?.testFilePath || !Array.isArray(parsed.scenarios)) continue;\n reports.push(parsed);\n } catch {\n // Ignore unreadable or malformed files\n }\n }\n return reports;\n }\n\n onRunStart(): void {\n this.resetRunArtifacts();\n this.startTime = Date.now();\n const root = process.cwd();\n const includeMetadata = this.options.markdown?.includeMetadata ?? true;\n if (includeMetadata) {\n this.packageVersion = readPackageVersion(root);\n this.gitSha = readGitSha(root);\n }\n }\n\n async onRunComplete(\n _testContexts: Set<unknown>,\n results: JestAggregatedResult\n ): Promise<void> {\n const root = process.cwd();\n\n // Build map of test results by file for status lookup\n const fileResults = new Map<string, JestTestResult>();\n for (const testFileResult of results.testResults) {\n fileResults.set(testFileResult.testFilePath, testFileResult);\n }\n\n // Read story data from JSON files written by story.init()\n const reports = this.readStoryReports();\n\n // Collect test cases\n const rawTestCases: RawTestCase[] = [];\n\n for (const report of reports) {\n const fileResult = fileResults.get(report.testFilePath);\n const sourceFile = toRelativePosix(report.testFilePath, root);\n\n for (const meta of report.scenarios) {\n if (!meta?.scenario) continue;\n\n // Find matching test result\n const matchingTest = fileResult?.testResults.find((test) => {\n const expectedFullName = meta.suitePath\n ? [...meta.suitePath, meta.scenario].join(\" > \")\n : meta.scenario;\n return test.fullName === expectedFullName;\n });\n\n // Map Jest status to raw status\n const statusMap: Record<string, RawTestCase[\"status\"]> = {\n passed: \"pass\",\n failed: \"fail\",\n pending: \"pending\",\n todo: \"pending\",\n };\n\n const status = matchingTest\n ? statusMap[matchingTest.status] ?? \"unknown\"\n : \"pass\";\n\n // Map attachments\n const rawAttachments: RawAttachment[] = (meta._attachments ?? []).map((a) => ({\n name: a.name,\n mediaType: a.mediaType,\n path: a.path,\n body: a.body,\n encoding: a.encoding,\n charset: a.charset,\n fileName: a.fileName,\n stepIndex: a.stepIndex,\n stepId: a.stepId,\n }));\n\n // Extract step events (timing)\n const stepEvents: RawStepEvent[] = meta.steps\n .filter((s: { durationMs?: number }) => s.durationMs !== undefined)\n .map((s: { durationMs?: number; text: string }, i: number) => ({\n index: i,\n title: s.text,\n durationMs: s.durationMs,\n }));\n\n rawTestCases.push({\n title: meta.scenario,\n titlePath: meta.suitePath\n ? [...meta.suitePath, meta.scenario]\n : [meta.scenario],\n story: meta,\n sourceFile,\n sourceLine: Math.max(1, meta.sourceOrder ?? 1),\n status,\n durationMs: matchingTest?.duration ?? 0,\n error: matchingTest?.failureMessages?.length\n ? { message: matchingTest.failureMessages.join(\"\\n\") }\n : undefined,\n attachments: rawAttachments.length > 0 ? rawAttachments : undefined,\n stepEvents: stepEvents.length > 0 ? stepEvents : undefined,\n retry: 0,\n retries: 0,\n });\n }\n }\n\n if (rawTestCases.length === 0) return;\n\n // Build RawRun\n const rawRun: RawRun = {\n testCases: rawTestCases,\n startedAtMs: this.startTime,\n finishedAtMs: Date.now(),\n projectRoot: root,\n packageVersion: this.packageVersion,\n gitSha: this.gitSha,\n ci: detectCI(),\n };\n\n // Optionally write raw run JSON for CLI/binary consumption\n const rawRunPath = this.options.rawRunPath;\n if (rawRunPath) {\n const absolutePath = path.isAbsolute(rawRunPath)\n ? rawRunPath\n : path.join(root, rawRunPath);\n const dir = path.dirname(absolutePath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n const payload = { schemaVersion: 1, ...rawRun };\n fs.writeFileSync(absolutePath, JSON.stringify(payload, null, 2), \"utf8\");\n }\n\n // Canonicalize\n const canonicalRun = canonicalizeRun(rawRun);\n\n // Generate reports\n const generator = new ReportGenerator(this.options);\n try {\n await generator.generate(canonicalRun);\n } catch (err) {\n console.error(\"Failed to generate reports:\", err);\n }\n }\n}\n"],"mappings":"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","names":[]}