executable-stories-cypress 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.
- package/README.md +79 -0
- package/dist/index.cjs +334 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +173 -0
- package/dist/index.d.ts +173 -0
- package/dist/index.js +306 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.cjs +61 -0
- package/dist/plugin.cjs.map +1 -0
- package/dist/plugin.d.cts +33 -0
- package/dist/plugin.d.ts +33 -0
- package/dist/plugin.js +34 -0
- package/dist/plugin.js.map +1 -0
- package/dist/reporter.cjs +220 -0
- package/dist/reporter.cjs.map +1 -0
- package/dist/reporter.d.cts +84 -0
- package/dist/reporter.d.ts +84 -0
- package/dist/reporter.js +190 -0
- package/dist/reporter.js.map +1 -0
- package/dist/support.cjs +313 -0
- package/dist/support.cjs.map +1 -0
- package/dist/support.d.cts +2 -0
- package/dist/support.d.ts +2 -0
- package/dist/support.js +311 -0
- package/dist/support.js.map +1 -0
- package/package.json +61 -0
- package/reporter.cjs +2 -0
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/store.ts
|
|
2
|
+
function key(specRelative, titlePath) {
|
|
3
|
+
return `${specRelative}\0${titlePath.join("\0")}`;
|
|
4
|
+
}
|
|
5
|
+
var store = /* @__PURE__ */ new Map();
|
|
6
|
+
function recordMeta(payload) {
|
|
7
|
+
store.set(key(payload.specRelative, payload.titlePath), {
|
|
8
|
+
specRelative: payload.specRelative,
|
|
9
|
+
titlePath: payload.titlePath,
|
|
10
|
+
meta: payload.meta,
|
|
11
|
+
attachments: payload.attachments
|
|
12
|
+
});
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
function getMeta(specRelative, titlePath) {
|
|
16
|
+
return store.get(key(specRelative, titlePath))?.meta;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/plugin.ts
|
|
20
|
+
var TASK_NAME = "executableStories:recordMeta";
|
|
21
|
+
var GET_STORED_META_TASK = "executableStories:getStoredMeta";
|
|
22
|
+
function registerExecutableStoriesPlugin(on) {
|
|
23
|
+
on("task", {
|
|
24
|
+
[TASK_NAME]: (payload) => recordMeta(payload),
|
|
25
|
+
[GET_STORED_META_TASK]: (arg) => {
|
|
26
|
+
const { specRelative, titlePath } = arg;
|
|
27
|
+
return getMeta(specRelative, titlePath) ?? null;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
registerExecutableStoriesPlugin
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Node-only store for story meta sent from the browser via cy.task.\n * Plugin writes; reporter reads. Keyed by spec + titlePath for merging with run results.\n */\n\nimport type { StoryMeta, ScopedAttachment, RecordMetaPayload } from \"./types\";\n\nexport interface StoredMeta {\n specRelative: string;\n titlePath: string[];\n meta: StoryMeta;\n attachments?: ScopedAttachment[];\n}\n\nfunction key(specRelative: string, titlePath: string[]): string {\n return `${specRelative}\\0${titlePath.join(\"\\0\")}`;\n}\n\nconst store = new Map<string, StoredMeta>();\n\nexport function recordMeta(payload: RecordMetaPayload): null {\n store.set(key(payload.specRelative, payload.titlePath), {\n specRelative: payload.specRelative,\n titlePath: payload.titlePath,\n meta: payload.meta,\n attachments: payload.attachments,\n });\n return null;\n}\n\nexport function getAttachments(specRelative: string, titlePath: string[]): ScopedAttachment[] | undefined {\n return store.get(key(specRelative, titlePath))?.attachments;\n}\n\nexport function getMeta(specRelative: string, titlePath: string[]): StoryMeta | undefined {\n return store.get(key(specRelative, titlePath))?.meta;\n}\n\nexport function getAllMeta(): StoredMeta[] {\n return Array.from(store.values());\n}\n\nexport function clearStore(): void {\n store.clear();\n}\n","/**\n * Cypress Node plugin: registers the task that receives story meta from the browser.\n * Use in cypress.config.ts (or plugins file) so the reporter can merge meta with run results.\n *\n * @example\n * // cypress.config.ts\n * import { defineConfig } from 'cypress';\n * import { registerExecutableStoriesPlugin } from 'executable-stories-cypress/plugin';\n *\n * export default defineConfig({\n * e2e: {\n * setupNodeEvents(on) {\n * registerExecutableStoriesPlugin(on);\n * },\n * },\n * });\n */\n\nimport { recordMeta, getMeta } from \"./store\";\n\nconst TASK_NAME = \"executableStories:recordMeta\";\nconst GET_STORED_META_TASK = \"executableStories:getStoredMeta\";\n\n/** Minimal type for Cypress plugin `on` (task registration) */\nexport interface PluginEvents {\n (event: \"task\", tasks: Record<string, (arg: unknown) => unknown>): void;\n}\n\n/**\n * Register the executable-stories task on the Cypress `on` handler.\n * Call this from setupNodeEvents in your Cypress config.\n */\n/** Argument for getStoredMeta task (used by tests to assert on recorded meta). */\nexport interface GetStoredMetaArg {\n specRelative: string;\n titlePath: string[];\n}\n\nexport function registerExecutableStoriesPlugin(on: PluginEvents): void {\n on(\"task\", {\n [TASK_NAME]: (payload: unknown) => recordMeta(payload as import(\"./types\").RecordMetaPayload),\n [GET_STORED_META_TASK]: (arg: unknown) => {\n const { specRelative, titlePath } = arg as GetStoredMetaArg;\n return getMeta(specRelative, titlePath) ?? null;\n },\n });\n}\n"],"mappings":";AAcA,SAAS,IAAI,cAAsB,WAA6B;AAC9D,SAAO,GAAG,YAAY,KAAK,UAAU,KAAK,IAAI,CAAC;AACjD;AAEA,IAAM,QAAQ,oBAAI,IAAwB;AAEnC,SAAS,WAAW,SAAkC;AAC3D,QAAM,IAAI,IAAI,QAAQ,cAAc,QAAQ,SAAS,GAAG;AAAA,IACtD,cAAc,QAAQ;AAAA,IACtB,WAAW,QAAQ;AAAA,IACnB,MAAM,QAAQ;AAAA,IACd,aAAa,QAAQ;AAAA,EACvB,CAAC;AACD,SAAO;AACT;AAMO,SAAS,QAAQ,cAAsB,WAA4C;AACxF,SAAO,MAAM,IAAI,IAAI,cAAc,SAAS,CAAC,GAAG;AAClD;;;AChBA,IAAM,YAAY;AAClB,IAAM,uBAAuB;AAiBtB,SAAS,gCAAgC,IAAwB;AACtE,KAAG,QAAQ;AAAA,IACT,CAAC,SAAS,GAAG,CAAC,YAAqB,WAAW,OAA8C;AAAA,IAC5F,CAAC,oBAAoB,GAAG,CAAC,QAAiB;AACxC,YAAM,EAAE,cAAc,UAAU,IAAI;AACpC,aAAO,QAAQ,cAAc,SAAS,KAAK;AAAA,IAC7C;AAAA,EACF,CAAC;AACH;","names":[]}
|
|
@@ -0,0 +1,220 @@
|
|
|
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 key2 of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key2) && key2 !== except)
|
|
16
|
+
__defProp(to, key2, { get: () => from[key2], enumerable: !(desc = __getOwnPropDesc(from, key2)) || 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
|
+
buildRawRunFromCypressResult: () => buildRawRunFromCypressResult,
|
|
34
|
+
default: () => reporter_default,
|
|
35
|
+
generateReportsFromRawRun: () => generateReportsFromRawRun
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(reporter_exports);
|
|
38
|
+
var fs = __toESM(require("fs"), 1);
|
|
39
|
+
var path = __toESM(require("path"), 1);
|
|
40
|
+
var import_executable_stories_formatters = require("executable-stories-formatters");
|
|
41
|
+
|
|
42
|
+
// src/store.ts
|
|
43
|
+
function key(specRelative, titlePath) {
|
|
44
|
+
return `${specRelative}\0${titlePath.join("\0")}`;
|
|
45
|
+
}
|
|
46
|
+
var store = /* @__PURE__ */ new Map();
|
|
47
|
+
function getAttachments(specRelative, titlePath) {
|
|
48
|
+
return store.get(key(specRelative, titlePath))?.attachments;
|
|
49
|
+
}
|
|
50
|
+
function getMeta(specRelative, titlePath) {
|
|
51
|
+
return store.get(key(specRelative, titlePath))?.meta;
|
|
52
|
+
}
|
|
53
|
+
function getAllMeta() {
|
|
54
|
+
return Array.from(store.values());
|
|
55
|
+
}
|
|
56
|
+
function clearStore() {
|
|
57
|
+
store.clear();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/reporter.ts
|
|
61
|
+
function getTitlePath(test) {
|
|
62
|
+
const path2 = [];
|
|
63
|
+
let current = test;
|
|
64
|
+
while (current) {
|
|
65
|
+
const title = "title" in current ? current.title : "";
|
|
66
|
+
if (title) path2.unshift(title);
|
|
67
|
+
current = "parent" in current ? current.parent : void 0;
|
|
68
|
+
}
|
|
69
|
+
return path2;
|
|
70
|
+
}
|
|
71
|
+
function collectTests(suite) {
|
|
72
|
+
const tests = [...suite.tests];
|
|
73
|
+
for (const s of suite.suites) {
|
|
74
|
+
tests.push(...collectTests(s));
|
|
75
|
+
}
|
|
76
|
+
return tests;
|
|
77
|
+
}
|
|
78
|
+
function mapCypressStateToRaw(state) {
|
|
79
|
+
switch (state) {
|
|
80
|
+
case "passed":
|
|
81
|
+
return "pass";
|
|
82
|
+
case "failed":
|
|
83
|
+
return "fail";
|
|
84
|
+
case "pending":
|
|
85
|
+
return "skip";
|
|
86
|
+
default:
|
|
87
|
+
return "unknown";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
var DEFAULT_OPTIONS = {
|
|
91
|
+
projectRoot: process.cwd()
|
|
92
|
+
};
|
|
93
|
+
function createReporter(runner, options = {}) {
|
|
94
|
+
const reporterOpts = "reporterOptions" in options && options.reporterOptions ? options.reporterOptions : options;
|
|
95
|
+
const opts = { ...DEFAULT_OPTIONS, ...reporterOpts };
|
|
96
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
97
|
+
const specPath = opts.specPath ?? "unknown";
|
|
98
|
+
const startTime = runner.startTime ?? Date.now();
|
|
99
|
+
runner.suite && runner.on?.("end", () => {
|
|
100
|
+
const tests = collectTests(runner.suite);
|
|
101
|
+
const rawTestCases = [];
|
|
102
|
+
const effectiveSpecPath = specPath !== "unknown" ? specPath : (() => {
|
|
103
|
+
const all = getAllMeta();
|
|
104
|
+
return all.length > 0 ? all[0].specRelative : "unknown";
|
|
105
|
+
})();
|
|
106
|
+
for (const test of tests) {
|
|
107
|
+
const titlePath = getTitlePath(test);
|
|
108
|
+
const meta = getMeta(effectiveSpecPath, titlePath);
|
|
109
|
+
if (!meta) continue;
|
|
110
|
+
const status = mapCypressStateToRaw(test.state);
|
|
111
|
+
const storedAttachments = getAttachments(effectiveSpecPath, titlePath);
|
|
112
|
+
const attachments = storedAttachments?.map((a) => ({
|
|
113
|
+
name: a.name,
|
|
114
|
+
mediaType: a.mediaType,
|
|
115
|
+
path: a.path,
|
|
116
|
+
body: typeof a.body === "string" ? a.body : a.body instanceof Buffer ? a.body.toString("base64") : void 0,
|
|
117
|
+
encoding: a.encoding ?? (a.body instanceof Buffer ? "BASE64" : void 0),
|
|
118
|
+
charset: a.charset,
|
|
119
|
+
fileName: a.fileName,
|
|
120
|
+
stepIndex: a.stepIndex,
|
|
121
|
+
stepId: a.stepId
|
|
122
|
+
}));
|
|
123
|
+
const stepEvents = meta.steps.filter((s) => s.durationMs !== void 0).map((s, i) => ({
|
|
124
|
+
index: i,
|
|
125
|
+
title: s.text,
|
|
126
|
+
durationMs: s.durationMs
|
|
127
|
+
}));
|
|
128
|
+
rawTestCases.push({
|
|
129
|
+
title: meta.scenario,
|
|
130
|
+
titlePath: meta.suitePath ? [...meta.suitePath, meta.scenario] : [meta.scenario],
|
|
131
|
+
story: meta,
|
|
132
|
+
sourceFile: effectiveSpecPath,
|
|
133
|
+
sourceLine: 1,
|
|
134
|
+
status,
|
|
135
|
+
durationMs: test.duration,
|
|
136
|
+
error: test.err ? { message: test.err.message, stack: test.err.stack } : void 0,
|
|
137
|
+
attachments: attachments && attachments.length > 0 ? attachments : void 0,
|
|
138
|
+
stepEvents: stepEvents.length > 0 ? stepEvents : void 0,
|
|
139
|
+
retry: 0,
|
|
140
|
+
retries: 0
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (rawTestCases.length === 0) {
|
|
144
|
+
clearStore();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const includeMetadata = opts.markdown?.includeMetadata ?? true;
|
|
148
|
+
const rawRun = {
|
|
149
|
+
testCases: rawTestCases,
|
|
150
|
+
startedAtMs: startTime,
|
|
151
|
+
finishedAtMs: Date.now(),
|
|
152
|
+
projectRoot,
|
|
153
|
+
packageVersion: includeMetadata ? (0, import_executable_stories_formatters.readPackageVersion)(projectRoot) : void 0,
|
|
154
|
+
gitSha: includeMetadata ? (0, import_executable_stories_formatters.readGitSha)(projectRoot) : void 0,
|
|
155
|
+
ci: (0, import_executable_stories_formatters.detectCI)()
|
|
156
|
+
};
|
|
157
|
+
const rawRunPath = opts.rawRunPath;
|
|
158
|
+
if (rawRunPath) {
|
|
159
|
+
const absolutePath = path.isAbsolute(rawRunPath) ? rawRunPath : path.join(projectRoot, rawRunPath);
|
|
160
|
+
const dir = path.dirname(absolutePath);
|
|
161
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
162
|
+
fs.writeFileSync(absolutePath, JSON.stringify({ schemaVersion: 1, ...rawRun }, null, 2), "utf8");
|
|
163
|
+
}
|
|
164
|
+
const canonicalRun = (0, import_executable_stories_formatters.canonicalizeRun)(rawRun);
|
|
165
|
+
const generator = new import_executable_stories_formatters.ReportGenerator(opts);
|
|
166
|
+
generator.generate(canonicalRun).catch((err) => {
|
|
167
|
+
console.error("Failed to generate reports:", err);
|
|
168
|
+
});
|
|
169
|
+
clearStore();
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function buildRawRunFromCypressResult(result, options = {}) {
|
|
173
|
+
const projectRoot = options.projectRoot ?? result.config?.projectRoot ?? process.cwd();
|
|
174
|
+
const rawTestCases = [];
|
|
175
|
+
for (const run of result.runs ?? []) {
|
|
176
|
+
const specRelative = run.spec?.relative ?? "unknown";
|
|
177
|
+
for (const test of run.tests ?? []) {
|
|
178
|
+
const titlePath = test.title ?? [];
|
|
179
|
+
const meta = getMeta(specRelative, titlePath);
|
|
180
|
+
if (!meta) continue;
|
|
181
|
+
const lastAttempt = test.attempts?.length ? test.attempts[test.attempts.length - 1] : void 0;
|
|
182
|
+
const state = lastAttempt?.state ?? test.state;
|
|
183
|
+
const err = lastAttempt?.error ?? (test.displayError ? { message: test.displayError } : void 0);
|
|
184
|
+
const duration = lastAttempt?.duration ?? test.duration;
|
|
185
|
+
rawTestCases.push({
|
|
186
|
+
title: meta.scenario,
|
|
187
|
+
titlePath: meta.suitePath ? [...meta.suitePath, meta.scenario] : [meta.scenario],
|
|
188
|
+
story: meta,
|
|
189
|
+
sourceFile: specRelative,
|
|
190
|
+
sourceLine: 1,
|
|
191
|
+
status: mapCypressStateToRaw(state),
|
|
192
|
+
durationMs: duration,
|
|
193
|
+
error: err ? { message: err.message, stack: err.stack } : void 0
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const includeMetadata = options.markdown?.includeMetadata ?? true;
|
|
198
|
+
return {
|
|
199
|
+
testCases: rawTestCases,
|
|
200
|
+
startedAtMs: void 0,
|
|
201
|
+
finishedAtMs: Date.now(),
|
|
202
|
+
projectRoot,
|
|
203
|
+
packageVersion: includeMetadata ? (0, import_executable_stories_formatters.readPackageVersion)(projectRoot) : void 0,
|
|
204
|
+
gitSha: includeMetadata ? (0, import_executable_stories_formatters.readGitSha)(projectRoot) : void 0,
|
|
205
|
+
ci: (0, import_executable_stories_formatters.detectCI)()
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async function generateReportsFromRawRun(rawRun, options) {
|
|
209
|
+
if (rawRun.testCases.length === 0) return;
|
|
210
|
+
const canonicalRun = (0, import_executable_stories_formatters.canonicalizeRun)(rawRun);
|
|
211
|
+
const generator = new import_executable_stories_formatters.ReportGenerator(options);
|
|
212
|
+
await generator.generate(canonicalRun);
|
|
213
|
+
}
|
|
214
|
+
var reporter_default = createReporter;
|
|
215
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
216
|
+
0 && (module.exports = {
|
|
217
|
+
buildRawRunFromCypressResult,
|
|
218
|
+
generateReportsFromRawRun
|
|
219
|
+
});
|
|
220
|
+
//# sourceMappingURL=reporter.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/reporter.ts","../src/store.ts"],"sourcesContent":["/**\n * Cypress/Mocha reporter for executable-stories.\n * Builds RawRun (formatters schema) from run results + stored story meta, then generates reports.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { StoryMeta } from \"executable-stories-formatters\";\n\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 RawStatus,\n type FormatterOptions,\n} from \"executable-stories-formatters\";\n\nimport { getMeta, getAttachments, getAllMeta, clearStore } from \"./store\";\n\n// Re-export types from formatters\nexport type {\n OutputFormat,\n OutputMode,\n ColocatedStyle,\n OutputRule,\n FormatterOptions,\n} from \"executable-stories-formatters\";\n\n// ============================================================================\n// Reporter Options\n// ============================================================================\n\nexport interface StoryReporterOptions extends FormatterOptions {\n /** If set, write raw run JSON (schemaVersion 1) to this path for CLI/binary */\n rawRunPath?: string;\n /** Spec file path (relative to projectRoot) for this run. If omitted, inferred from stored meta when possible. */\n specPath?: string;\n /** Project root. Defaults to process.cwd(). */\n projectRoot?: string;\n}\n\n// ============================================================================\n// Mocha Runner / Test types (minimal for reporter)\n// ============================================================================\n\ninterface MochaSuite {\n title: string;\n suites: MochaSuite[];\n tests: MochaTest[];\n parent?: MochaSuite;\n}\n\ninterface MochaTest {\n title: string;\n state?: string;\n duration?: number;\n err?: Error & { message?: string; stack?: string };\n parent?: MochaSuite;\n}\n\ninterface MochaRunner {\n suite: MochaSuite;\n startTime?: number;\n on?(event: string, callback: () => void): void;\n}\n\nfunction getTitlePath(test: MochaTest): string[] {\n const path: string[] = [];\n let current: MochaSuite | MochaTest | undefined = test;\n while (current) {\n const title = \"title\" in current ? current.title : \"\";\n if (title) path.unshift(title);\n current = \"parent\" in current ? current.parent : undefined;\n }\n return path;\n}\n\nfunction collectTests(suite: MochaSuite): MochaTest[] {\n const tests = [...suite.tests];\n for (const s of suite.suites) {\n tests.push(...collectTests(s));\n }\n return tests;\n}\n\nfunction mapCypressStateToRaw(state: string | undefined): RawStatus {\n switch (state) {\n case \"passed\":\n return \"pass\";\n case \"failed\":\n return \"fail\";\n case \"pending\":\n return \"skip\";\n default:\n return \"unknown\";\n }\n}\n\n// ============================================================================\n// Reporter Implementation\n// ============================================================================\n\nconst DEFAULT_OPTIONS: Partial<StoryReporterOptions> = {\n projectRoot: process.cwd(),\n};\n\n/**\n * Create a Mocha reporter function that Cypress can use (--reporter path/to/reporter).\n * Cypress passes (runner, reporterOptions) with reporterOptions as the second argument directly.\n * Also supports options.reporterOptions for wrapped usage.\n */\nfunction createReporter(\n runner: MochaRunner,\n options: StoryReporterOptions | { reporterOptions?: StoryReporterOptions } = {}\n) {\n const reporterOpts =\n \"reporterOptions\" in options && options.reporterOptions\n ? options.reporterOptions\n : (options as StoryReporterOptions);\n const opts: StoryReporterOptions = { ...DEFAULT_OPTIONS, ...reporterOpts };\n const projectRoot = opts.projectRoot ?? process.cwd();\n const specPath = opts.specPath ?? \"unknown\";\n const startTime = runner.startTime ?? Date.now();\n\n runner.suite && runner.on?.(\"end\", () => {\n const tests = collectTests(runner.suite);\n const rawTestCases: RawTestCase[] = [];\n\n // If specPath not provided, infer from first stored meta (all stored entries are for this spec in a single-spec run)\n const effectiveSpecPath =\n specPath !== \"unknown\"\n ? specPath\n : (() => {\n const all = getAllMeta();\n return all.length > 0 ? all[0].specRelative : \"unknown\";\n })();\n\n for (const test of tests) {\n const titlePath = getTitlePath(test);\n const meta = getMeta(effectiveSpecPath, titlePath);\n if (!meta) continue; // only document tests that used story.init()\n\n const status = mapCypressStateToRaw(test.state);\n\n // Map stored attachments to RawAttachment[]\n const storedAttachments = getAttachments(effectiveSpecPath, titlePath);\n const attachments: RawAttachment[] | undefined = storedAttachments?.map((a) => ({\n name: a.name,\n mediaType: a.mediaType,\n path: a.path,\n body: typeof a.body === 'string' ? a.body : a.body instanceof Buffer ? a.body.toString('base64') : undefined,\n encoding: a.encoding ?? (a.body instanceof Buffer ? 'BASE64' : undefined),\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 ? [...meta.suitePath, meta.scenario] : [meta.scenario],\n story: meta,\n sourceFile: effectiveSpecPath,\n sourceLine: 1,\n status,\n durationMs: test.duration,\n error: test.err\n ? { message: test.err.message, stack: test.err.stack }\n : undefined,\n attachments: attachments && attachments.length > 0 ? attachments : undefined,\n stepEvents: stepEvents.length > 0 ? stepEvents : undefined,\n retry: 0,\n retries: 0,\n });\n }\n\n if (rawTestCases.length === 0) {\n clearStore();\n return;\n }\n\n const includeMetadata = opts.markdown?.includeMetadata ?? true;\n const rawRun: RawRun = {\n testCases: rawTestCases,\n startedAtMs: startTime,\n finishedAtMs: Date.now(),\n projectRoot,\n packageVersion: includeMetadata ? readPackageVersion(projectRoot) : undefined,\n gitSha: includeMetadata ? readGitSha(projectRoot) : undefined,\n ci: detectCI(),\n };\n\n const rawRunPath = opts.rawRunPath;\n if (rawRunPath) {\n const absolutePath = path.isAbsolute(rawRunPath)\n ? rawRunPath\n : path.join(projectRoot, rawRunPath);\n const dir = path.dirname(absolutePath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(absolutePath, JSON.stringify({ schemaVersion: 1, ...rawRun }, null, 2), \"utf8\");\n }\n\n const canonicalRun = canonicalizeRun(rawRun);\n const generator = new ReportGenerator(opts);\n generator.generate(canonicalRun).catch((err) => {\n console.error(\"Failed to generate reports:\", err);\n });\n\n clearStore();\n });\n}\n\n/**\n * Alternative: build RawRun from Cypress Module API results + stored meta.\n * Use this when not using the Mocha reporter (e.g. in after:run plugin).\n * Call recordMeta via plugin during the run, then call this with cypress.run() result.\n */\nexport interface CypressRunResultTest {\n title: string[];\n state?: string;\n displayError?: string;\n duration?: number;\n attempts?: Array<{\n state?: string;\n duration?: number;\n error?: { message?: string; stack?: string };\n }>;\n}\n\nexport interface CypressRunResultRun {\n spec?: { relative?: string; absolute?: string };\n tests?: CypressRunResultTest[];\n}\n\nexport interface CypressRunResult {\n runs?: CypressRunResultRun[];\n config?: { projectRoot?: string };\n}\n\nexport function buildRawRunFromCypressResult(\n result: CypressRunResult,\n options: StoryReporterOptions = {}\n): RawRun {\n const projectRoot = options.projectRoot ?? result.config?.projectRoot ?? process.cwd();\n const rawTestCases: RawTestCase[] = [];\n\n for (const run of result.runs ?? []) {\n const specRelative = run.spec?.relative ?? \"unknown\";\n for (const test of run.tests ?? []) {\n const titlePath = test.title ?? [];\n const meta = getMeta(specRelative, titlePath);\n if (!meta) continue;\n\n const lastAttempt = test.attempts?.length\n ? test.attempts[test.attempts.length - 1]\n : undefined;\n const state = lastAttempt?.state ?? test.state;\n const err = lastAttempt?.error ?? (test.displayError ? { message: test.displayError } : undefined);\n const duration = lastAttempt?.duration ?? test.duration;\n\n rawTestCases.push({\n title: meta.scenario,\n titlePath: meta.suitePath ? [...meta.suitePath, meta.scenario] : [meta.scenario],\n story: meta,\n sourceFile: specRelative,\n sourceLine: 1,\n status: mapCypressStateToRaw(state),\n durationMs: duration,\n error: err ? { message: err.message, stack: err.stack } : undefined,\n });\n }\n }\n\n const includeMetadata = options.markdown?.includeMetadata ?? true;\n return {\n testCases: rawTestCases,\n startedAtMs: undefined,\n finishedAtMs: Date.now(),\n projectRoot,\n packageVersion: includeMetadata ? readPackageVersion(projectRoot) : undefined,\n gitSha: includeMetadata ? readGitSha(projectRoot) : undefined,\n ci: detectCI(),\n };\n}\n\n/**\n * Generate reports from a RawRun (e.g. after buildRawRunFromCypressResult).\n */\nexport async function generateReportsFromRawRun(\n rawRun: RawRun,\n options: FormatterOptions\n): Promise<void> {\n if (rawRun.testCases.length === 0) return;\n const canonicalRun = canonicalizeRun(rawRun);\n const generator = new ReportGenerator(options);\n await generator.generate(canonicalRun);\n}\n\nexport default createReporter;\n","/**\n * Node-only store for story meta sent from the browser via cy.task.\n * Plugin writes; reporter reads. Keyed by spec + titlePath for merging with run results.\n */\n\nimport type { StoryMeta, ScopedAttachment, RecordMetaPayload } from \"./types\";\n\nexport interface StoredMeta {\n specRelative: string;\n titlePath: string[];\n meta: StoryMeta;\n attachments?: ScopedAttachment[];\n}\n\nfunction key(specRelative: string, titlePath: string[]): string {\n return `${specRelative}\\0${titlePath.join(\"\\0\")}`;\n}\n\nconst store = new Map<string, StoredMeta>();\n\nexport function recordMeta(payload: RecordMetaPayload): null {\n store.set(key(payload.specRelative, payload.titlePath), {\n specRelative: payload.specRelative,\n titlePath: payload.titlePath,\n meta: payload.meta,\n attachments: payload.attachments,\n });\n return null;\n}\n\nexport function getAttachments(specRelative: string, titlePath: string[]): ScopedAttachment[] | undefined {\n return store.get(key(specRelative, titlePath))?.attachments;\n}\n\nexport function getMeta(specRelative: string, titlePath: string[]): StoryMeta | undefined {\n return store.get(key(specRelative, titlePath))?.meta;\n}\n\nexport function getAllMeta(): StoredMeta[] {\n return Array.from(store.values());\n}\n\nexport function clearStore(): void {\n store.clear();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,SAAoB;AACpB,WAAsB;AAGtB,2CAYO;;;ACPP,SAAS,IAAI,cAAsB,WAA6B;AAC9D,SAAO,GAAG,YAAY,KAAK,UAAU,KAAK,IAAI,CAAC;AACjD;AAEA,IAAM,QAAQ,oBAAI,IAAwB;AAYnC,SAAS,eAAe,cAAsB,WAAqD;AACxG,SAAO,MAAM,IAAI,IAAI,cAAc,SAAS,CAAC,GAAG;AAClD;AAEO,SAAS,QAAQ,cAAsB,WAA4C;AACxF,SAAO,MAAM,IAAI,IAAI,cAAc,SAAS,CAAC,GAAG;AAClD;AAEO,SAAS,aAA2B;AACzC,SAAO,MAAM,KAAK,MAAM,OAAO,CAAC;AAClC;AAEO,SAAS,aAAmB;AACjC,QAAM,MAAM;AACd;;;AD4BA,SAAS,aAAa,MAA2B;AAC/C,QAAMA,QAAiB,CAAC;AACxB,MAAI,UAA8C;AAClD,SAAO,SAAS;AACd,UAAM,QAAQ,WAAW,UAAU,QAAQ,QAAQ;AACnD,QAAI,MAAO,CAAAA,MAAK,QAAQ,KAAK;AAC7B,cAAU,YAAY,UAAU,QAAQ,SAAS;AAAA,EACnD;AACA,SAAOA;AACT;AAEA,SAAS,aAAa,OAAgC;AACpD,QAAM,QAAQ,CAAC,GAAG,MAAM,KAAK;AAC7B,aAAW,KAAK,MAAM,QAAQ;AAC5B,UAAM,KAAK,GAAG,aAAa,CAAC,CAAC;AAAA,EAC/B;AACA,SAAO;AACT;AAEA,SAAS,qBAAqB,OAAsC;AAClE,UAAQ,OAAO;AAAA,IACb,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAMA,IAAM,kBAAiD;AAAA,EACrD,aAAa,QAAQ,IAAI;AAC3B;AAOA,SAAS,eACP,QACA,UAA6E,CAAC,GAC9E;AACA,QAAM,eACJ,qBAAqB,WAAW,QAAQ,kBACpC,QAAQ,kBACP;AACP,QAAM,OAA6B,EAAE,GAAG,iBAAiB,GAAG,aAAa;AACzE,QAAM,cAAc,KAAK,eAAe,QAAQ,IAAI;AACpD,QAAM,WAAW,KAAK,YAAY;AAClC,QAAM,YAAY,OAAO,aAAa,KAAK,IAAI;AAE/C,SAAO,SAAS,OAAO,KAAK,OAAO,MAAM;AACvC,UAAM,QAAQ,aAAa,OAAO,KAAK;AACvC,UAAM,eAA8B,CAAC;AAGrC,UAAM,oBACJ,aAAa,YACT,YACC,MAAM;AACL,YAAM,MAAM,WAAW;AACvB,aAAO,IAAI,SAAS,IAAI,IAAI,CAAC,EAAE,eAAe;AAAA,IAChD,GAAG;AAET,eAAW,QAAQ,OAAO;AACxB,YAAM,YAAY,aAAa,IAAI;AACnC,YAAM,OAAO,QAAQ,mBAAmB,SAAS;AACjD,UAAI,CAAC,KAAM;AAEX,YAAM,SAAS,qBAAqB,KAAK,KAAK;AAG9C,YAAM,oBAAoB,eAAe,mBAAmB,SAAS;AACrE,YAAM,cAA2C,mBAAmB,IAAI,CAAC,OAAO;AAAA,QAC9E,MAAM,EAAE;AAAA,QACR,WAAW,EAAE;AAAA,QACb,MAAM,EAAE;AAAA,QACR,MAAM,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO,EAAE,gBAAgB,SAAS,EAAE,KAAK,SAAS,QAAQ,IAAI;AAAA,QACnG,UAAU,EAAE,aAAa,EAAE,gBAAgB,SAAS,WAAW;AAAA,QAC/D,SAAS,EAAE;AAAA,QACX,UAAU,EAAE;AAAA,QACZ,WAAW,EAAE;AAAA,QACb,QAAQ,EAAE;AAAA,MACZ,EAAE;AAGF,YAAM,aAA6B,KAAK,MACrC,OAAO,CAAC,MAA+B,EAAE,eAAe,MAAS,EACjE,IAAI,CAAC,GAA0C,OAAe;AAAA,QAC7D,OAAO;AAAA,QACP,OAAO,EAAE;AAAA,QACT,YAAY,EAAE;AAAA,MAChB,EAAE;AAEJ,mBAAa,KAAK;AAAA,QAChB,OAAO,KAAK;AAAA,QACZ,WAAW,KAAK,YAAY,CAAC,GAAG,KAAK,WAAW,KAAK,QAAQ,IAAI,CAAC,KAAK,QAAQ;AAAA,QAC/E,OAAO;AAAA,QACP,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ;AAAA,QACA,YAAY,KAAK;AAAA,QACjB,OAAO,KAAK,MACR,EAAE,SAAS,KAAK,IAAI,SAAS,OAAO,KAAK,IAAI,MAAM,IACnD;AAAA,QACJ,aAAa,eAAe,YAAY,SAAS,IAAI,cAAc;AAAA,QACnE,YAAY,WAAW,SAAS,IAAI,aAAa;AAAA,QACjD,OAAO;AAAA,QACP,SAAS;AAAA,MACX,CAAC;AAAA,IACH;AAEA,QAAI,aAAa,WAAW,GAAG;AAC7B,iBAAW;AACX;AAAA,IACF;AAEA,UAAM,kBAAkB,KAAK,UAAU,mBAAmB;AAC1D,UAAM,SAAiB;AAAA,MACrB,WAAW;AAAA,MACX,aAAa;AAAA,MACb,cAAc,KAAK,IAAI;AAAA,MACvB;AAAA,MACA,gBAAgB,sBAAkB,yDAAmB,WAAW,IAAI;AAAA,MACpE,QAAQ,sBAAkB,iDAAW,WAAW,IAAI;AAAA,MACpD,QAAI,+CAAS;AAAA,IACf;AAEA,UAAM,aAAa,KAAK;AACxB,QAAI,YAAY;AACd,YAAM,eAAoB,gBAAW,UAAU,IAC3C,aACK,UAAK,aAAa,UAAU;AACrC,YAAM,MAAW,aAAQ,YAAY;AACrC,UAAI,CAAI,cAAW,GAAG,EAAG,CAAG,aAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAC9D,MAAG,iBAAc,cAAc,KAAK,UAAU,EAAE,eAAe,GAAG,GAAG,OAAO,GAAG,MAAM,CAAC,GAAG,MAAM;AAAA,IACjG;AAEA,UAAM,mBAAe,sDAAgB,MAAM;AAC3C,UAAM,YAAY,IAAI,qDAAgB,IAAI;AAC1C,cAAU,SAAS,YAAY,EAAE,MAAM,CAAC,QAAQ;AAC9C,cAAQ,MAAM,+BAA+B,GAAG;AAAA,IAClD,CAAC;AAED,eAAW;AAAA,EACb,CAAC;AACH;AA6BO,SAAS,6BACd,QACA,UAAgC,CAAC,GACzB;AACR,QAAM,cAAc,QAAQ,eAAe,OAAO,QAAQ,eAAe,QAAQ,IAAI;AACrF,QAAM,eAA8B,CAAC;AAErC,aAAW,OAAO,OAAO,QAAQ,CAAC,GAAG;AACnC,UAAM,eAAe,IAAI,MAAM,YAAY;AAC3C,eAAW,QAAQ,IAAI,SAAS,CAAC,GAAG;AAClC,YAAM,YAAY,KAAK,SAAS,CAAC;AACjC,YAAM,OAAO,QAAQ,cAAc,SAAS;AAC5C,UAAI,CAAC,KAAM;AAEX,YAAM,cAAc,KAAK,UAAU,SAC/B,KAAK,SAAS,KAAK,SAAS,SAAS,CAAC,IACtC;AACJ,YAAM,QAAQ,aAAa,SAAS,KAAK;AACzC,YAAM,MAAM,aAAa,UAAU,KAAK,eAAe,EAAE,SAAS,KAAK,aAAa,IAAI;AACxF,YAAM,WAAW,aAAa,YAAY,KAAK;AAE/C,mBAAa,KAAK;AAAA,QAChB,OAAO,KAAK;AAAA,QACZ,WAAW,KAAK,YAAY,CAAC,GAAG,KAAK,WAAW,KAAK,QAAQ,IAAI,CAAC,KAAK,QAAQ;AAAA,QAC/E,OAAO;AAAA,QACP,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,QAAQ,qBAAqB,KAAK;AAAA,QAClC,YAAY;AAAA,QACZ,OAAO,MAAM,EAAE,SAAS,IAAI,SAAS,OAAO,IAAI,MAAM,IAAI;AAAA,MAC5D,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,kBAAkB,QAAQ,UAAU,mBAAmB;AAC7D,SAAO;AAAA,IACL,WAAW;AAAA,IACX,aAAa;AAAA,IACb,cAAc,KAAK,IAAI;AAAA,IACvB;AAAA,IACA,gBAAgB,sBAAkB,yDAAmB,WAAW,IAAI;AAAA,IACpE,QAAQ,sBAAkB,iDAAW,WAAW,IAAI;AAAA,IACpD,QAAI,+CAAS;AAAA,EACf;AACF;AAKA,eAAsB,0BACpB,QACA,SACe;AACf,MAAI,OAAO,UAAU,WAAW,EAAG;AACnC,QAAM,mBAAe,sDAAgB,MAAM;AAC3C,QAAM,YAAY,IAAI,qDAAgB,OAAO;AAC7C,QAAM,UAAU,SAAS,YAAY;AACvC;AAEA,IAAO,mBAAQ;","names":["path"]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { FormatterOptions, RawRun } from 'executable-stories-formatters';
|
|
2
|
+
export { ColocatedStyle, FormatterOptions, OutputFormat, OutputMode, OutputRule } from 'executable-stories-formatters';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cypress/Mocha reporter for executable-stories.
|
|
6
|
+
* Builds RawRun (formatters schema) from run results + stored story meta, then generates reports.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface StoryReporterOptions extends FormatterOptions {
|
|
10
|
+
/** If set, write raw run JSON (schemaVersion 1) to this path for CLI/binary */
|
|
11
|
+
rawRunPath?: string;
|
|
12
|
+
/** Spec file path (relative to projectRoot) for this run. If omitted, inferred from stored meta when possible. */
|
|
13
|
+
specPath?: string;
|
|
14
|
+
/** Project root. Defaults to process.cwd(). */
|
|
15
|
+
projectRoot?: string;
|
|
16
|
+
}
|
|
17
|
+
interface MochaSuite {
|
|
18
|
+
title: string;
|
|
19
|
+
suites: MochaSuite[];
|
|
20
|
+
tests: MochaTest[];
|
|
21
|
+
parent?: MochaSuite;
|
|
22
|
+
}
|
|
23
|
+
interface MochaTest {
|
|
24
|
+
title: string;
|
|
25
|
+
state?: string;
|
|
26
|
+
duration?: number;
|
|
27
|
+
err?: Error & {
|
|
28
|
+
message?: string;
|
|
29
|
+
stack?: string;
|
|
30
|
+
};
|
|
31
|
+
parent?: MochaSuite;
|
|
32
|
+
}
|
|
33
|
+
interface MochaRunner {
|
|
34
|
+
suite: MochaSuite;
|
|
35
|
+
startTime?: number;
|
|
36
|
+
on?(event: string, callback: () => void): void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a Mocha reporter function that Cypress can use (--reporter path/to/reporter).
|
|
40
|
+
* Cypress passes (runner, reporterOptions) with reporterOptions as the second argument directly.
|
|
41
|
+
* Also supports options.reporterOptions for wrapped usage.
|
|
42
|
+
*/
|
|
43
|
+
declare function createReporter(runner: MochaRunner, options?: StoryReporterOptions | {
|
|
44
|
+
reporterOptions?: StoryReporterOptions;
|
|
45
|
+
}): void;
|
|
46
|
+
/**
|
|
47
|
+
* Alternative: build RawRun from Cypress Module API results + stored meta.
|
|
48
|
+
* Use this when not using the Mocha reporter (e.g. in after:run plugin).
|
|
49
|
+
* Call recordMeta via plugin during the run, then call this with cypress.run() result.
|
|
50
|
+
*/
|
|
51
|
+
interface CypressRunResultTest {
|
|
52
|
+
title: string[];
|
|
53
|
+
state?: string;
|
|
54
|
+
displayError?: string;
|
|
55
|
+
duration?: number;
|
|
56
|
+
attempts?: Array<{
|
|
57
|
+
state?: string;
|
|
58
|
+
duration?: number;
|
|
59
|
+
error?: {
|
|
60
|
+
message?: string;
|
|
61
|
+
stack?: string;
|
|
62
|
+
};
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
interface CypressRunResultRun {
|
|
66
|
+
spec?: {
|
|
67
|
+
relative?: string;
|
|
68
|
+
absolute?: string;
|
|
69
|
+
};
|
|
70
|
+
tests?: CypressRunResultTest[];
|
|
71
|
+
}
|
|
72
|
+
interface CypressRunResult {
|
|
73
|
+
runs?: CypressRunResultRun[];
|
|
74
|
+
config?: {
|
|
75
|
+
projectRoot?: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
declare function buildRawRunFromCypressResult(result: CypressRunResult, options?: StoryReporterOptions): RawRun;
|
|
79
|
+
/**
|
|
80
|
+
* Generate reports from a RawRun (e.g. after buildRawRunFromCypressResult).
|
|
81
|
+
*/
|
|
82
|
+
declare function generateReportsFromRawRun(rawRun: RawRun, options: FormatterOptions): Promise<void>;
|
|
83
|
+
|
|
84
|
+
export { type CypressRunResult, type CypressRunResultRun, type CypressRunResultTest, type StoryReporterOptions, buildRawRunFromCypressResult, createReporter as default, generateReportsFromRawRun };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { FormatterOptions, RawRun } from 'executable-stories-formatters';
|
|
2
|
+
export { ColocatedStyle, FormatterOptions, OutputFormat, OutputMode, OutputRule } from 'executable-stories-formatters';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cypress/Mocha reporter for executable-stories.
|
|
6
|
+
* Builds RawRun (formatters schema) from run results + stored story meta, then generates reports.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface StoryReporterOptions extends FormatterOptions {
|
|
10
|
+
/** If set, write raw run JSON (schemaVersion 1) to this path for CLI/binary */
|
|
11
|
+
rawRunPath?: string;
|
|
12
|
+
/** Spec file path (relative to projectRoot) for this run. If omitted, inferred from stored meta when possible. */
|
|
13
|
+
specPath?: string;
|
|
14
|
+
/** Project root. Defaults to process.cwd(). */
|
|
15
|
+
projectRoot?: string;
|
|
16
|
+
}
|
|
17
|
+
interface MochaSuite {
|
|
18
|
+
title: string;
|
|
19
|
+
suites: MochaSuite[];
|
|
20
|
+
tests: MochaTest[];
|
|
21
|
+
parent?: MochaSuite;
|
|
22
|
+
}
|
|
23
|
+
interface MochaTest {
|
|
24
|
+
title: string;
|
|
25
|
+
state?: string;
|
|
26
|
+
duration?: number;
|
|
27
|
+
err?: Error & {
|
|
28
|
+
message?: string;
|
|
29
|
+
stack?: string;
|
|
30
|
+
};
|
|
31
|
+
parent?: MochaSuite;
|
|
32
|
+
}
|
|
33
|
+
interface MochaRunner {
|
|
34
|
+
suite: MochaSuite;
|
|
35
|
+
startTime?: number;
|
|
36
|
+
on?(event: string, callback: () => void): void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a Mocha reporter function that Cypress can use (--reporter path/to/reporter).
|
|
40
|
+
* Cypress passes (runner, reporterOptions) with reporterOptions as the second argument directly.
|
|
41
|
+
* Also supports options.reporterOptions for wrapped usage.
|
|
42
|
+
*/
|
|
43
|
+
declare function createReporter(runner: MochaRunner, options?: StoryReporterOptions | {
|
|
44
|
+
reporterOptions?: StoryReporterOptions;
|
|
45
|
+
}): void;
|
|
46
|
+
/**
|
|
47
|
+
* Alternative: build RawRun from Cypress Module API results + stored meta.
|
|
48
|
+
* Use this when not using the Mocha reporter (e.g. in after:run plugin).
|
|
49
|
+
* Call recordMeta via plugin during the run, then call this with cypress.run() result.
|
|
50
|
+
*/
|
|
51
|
+
interface CypressRunResultTest {
|
|
52
|
+
title: string[];
|
|
53
|
+
state?: string;
|
|
54
|
+
displayError?: string;
|
|
55
|
+
duration?: number;
|
|
56
|
+
attempts?: Array<{
|
|
57
|
+
state?: string;
|
|
58
|
+
duration?: number;
|
|
59
|
+
error?: {
|
|
60
|
+
message?: string;
|
|
61
|
+
stack?: string;
|
|
62
|
+
};
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
interface CypressRunResultRun {
|
|
66
|
+
spec?: {
|
|
67
|
+
relative?: string;
|
|
68
|
+
absolute?: string;
|
|
69
|
+
};
|
|
70
|
+
tests?: CypressRunResultTest[];
|
|
71
|
+
}
|
|
72
|
+
interface CypressRunResult {
|
|
73
|
+
runs?: CypressRunResultRun[];
|
|
74
|
+
config?: {
|
|
75
|
+
projectRoot?: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
declare function buildRawRunFromCypressResult(result: CypressRunResult, options?: StoryReporterOptions): RawRun;
|
|
79
|
+
/**
|
|
80
|
+
* Generate reports from a RawRun (e.g. after buildRawRunFromCypressResult).
|
|
81
|
+
*/
|
|
82
|
+
declare function generateReportsFromRawRun(rawRun: RawRun, options: FormatterOptions): Promise<void>;
|
|
83
|
+
|
|
84
|
+
export { type CypressRunResult, type CypressRunResultRun, type CypressRunResultTest, type StoryReporterOptions, buildRawRunFromCypressResult, createReporter as default, generateReportsFromRawRun };
|
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// src/reporter.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import {
|
|
5
|
+
ReportGenerator,
|
|
6
|
+
canonicalizeRun,
|
|
7
|
+
readGitSha,
|
|
8
|
+
readPackageVersion,
|
|
9
|
+
detectCI
|
|
10
|
+
} from "executable-stories-formatters";
|
|
11
|
+
|
|
12
|
+
// src/store.ts
|
|
13
|
+
function key(specRelative, titlePath) {
|
|
14
|
+
return `${specRelative}\0${titlePath.join("\0")}`;
|
|
15
|
+
}
|
|
16
|
+
var store = /* @__PURE__ */ new Map();
|
|
17
|
+
function getAttachments(specRelative, titlePath) {
|
|
18
|
+
return store.get(key(specRelative, titlePath))?.attachments;
|
|
19
|
+
}
|
|
20
|
+
function getMeta(specRelative, titlePath) {
|
|
21
|
+
return store.get(key(specRelative, titlePath))?.meta;
|
|
22
|
+
}
|
|
23
|
+
function getAllMeta() {
|
|
24
|
+
return Array.from(store.values());
|
|
25
|
+
}
|
|
26
|
+
function clearStore() {
|
|
27
|
+
store.clear();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/reporter.ts
|
|
31
|
+
function getTitlePath(test) {
|
|
32
|
+
const path2 = [];
|
|
33
|
+
let current = test;
|
|
34
|
+
while (current) {
|
|
35
|
+
const title = "title" in current ? current.title : "";
|
|
36
|
+
if (title) path2.unshift(title);
|
|
37
|
+
current = "parent" in current ? current.parent : void 0;
|
|
38
|
+
}
|
|
39
|
+
return path2;
|
|
40
|
+
}
|
|
41
|
+
function collectTests(suite) {
|
|
42
|
+
const tests = [...suite.tests];
|
|
43
|
+
for (const s of suite.suites) {
|
|
44
|
+
tests.push(...collectTests(s));
|
|
45
|
+
}
|
|
46
|
+
return tests;
|
|
47
|
+
}
|
|
48
|
+
function mapCypressStateToRaw(state) {
|
|
49
|
+
switch (state) {
|
|
50
|
+
case "passed":
|
|
51
|
+
return "pass";
|
|
52
|
+
case "failed":
|
|
53
|
+
return "fail";
|
|
54
|
+
case "pending":
|
|
55
|
+
return "skip";
|
|
56
|
+
default:
|
|
57
|
+
return "unknown";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
var DEFAULT_OPTIONS = {
|
|
61
|
+
projectRoot: process.cwd()
|
|
62
|
+
};
|
|
63
|
+
function createReporter(runner, options = {}) {
|
|
64
|
+
const reporterOpts = "reporterOptions" in options && options.reporterOptions ? options.reporterOptions : options;
|
|
65
|
+
const opts = { ...DEFAULT_OPTIONS, ...reporterOpts };
|
|
66
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
67
|
+
const specPath = opts.specPath ?? "unknown";
|
|
68
|
+
const startTime = runner.startTime ?? Date.now();
|
|
69
|
+
runner.suite && runner.on?.("end", () => {
|
|
70
|
+
const tests = collectTests(runner.suite);
|
|
71
|
+
const rawTestCases = [];
|
|
72
|
+
const effectiveSpecPath = specPath !== "unknown" ? specPath : (() => {
|
|
73
|
+
const all = getAllMeta();
|
|
74
|
+
return all.length > 0 ? all[0].specRelative : "unknown";
|
|
75
|
+
})();
|
|
76
|
+
for (const test of tests) {
|
|
77
|
+
const titlePath = getTitlePath(test);
|
|
78
|
+
const meta = getMeta(effectiveSpecPath, titlePath);
|
|
79
|
+
if (!meta) continue;
|
|
80
|
+
const status = mapCypressStateToRaw(test.state);
|
|
81
|
+
const storedAttachments = getAttachments(effectiveSpecPath, titlePath);
|
|
82
|
+
const attachments = storedAttachments?.map((a) => ({
|
|
83
|
+
name: a.name,
|
|
84
|
+
mediaType: a.mediaType,
|
|
85
|
+
path: a.path,
|
|
86
|
+
body: typeof a.body === "string" ? a.body : a.body instanceof Buffer ? a.body.toString("base64") : void 0,
|
|
87
|
+
encoding: a.encoding ?? (a.body instanceof Buffer ? "BASE64" : void 0),
|
|
88
|
+
charset: a.charset,
|
|
89
|
+
fileName: a.fileName,
|
|
90
|
+
stepIndex: a.stepIndex,
|
|
91
|
+
stepId: a.stepId
|
|
92
|
+
}));
|
|
93
|
+
const stepEvents = meta.steps.filter((s) => s.durationMs !== void 0).map((s, i) => ({
|
|
94
|
+
index: i,
|
|
95
|
+
title: s.text,
|
|
96
|
+
durationMs: s.durationMs
|
|
97
|
+
}));
|
|
98
|
+
rawTestCases.push({
|
|
99
|
+
title: meta.scenario,
|
|
100
|
+
titlePath: meta.suitePath ? [...meta.suitePath, meta.scenario] : [meta.scenario],
|
|
101
|
+
story: meta,
|
|
102
|
+
sourceFile: effectiveSpecPath,
|
|
103
|
+
sourceLine: 1,
|
|
104
|
+
status,
|
|
105
|
+
durationMs: test.duration,
|
|
106
|
+
error: test.err ? { message: test.err.message, stack: test.err.stack } : void 0,
|
|
107
|
+
attachments: attachments && attachments.length > 0 ? attachments : void 0,
|
|
108
|
+
stepEvents: stepEvents.length > 0 ? stepEvents : void 0,
|
|
109
|
+
retry: 0,
|
|
110
|
+
retries: 0
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (rawTestCases.length === 0) {
|
|
114
|
+
clearStore();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const includeMetadata = opts.markdown?.includeMetadata ?? true;
|
|
118
|
+
const rawRun = {
|
|
119
|
+
testCases: rawTestCases,
|
|
120
|
+
startedAtMs: startTime,
|
|
121
|
+
finishedAtMs: Date.now(),
|
|
122
|
+
projectRoot,
|
|
123
|
+
packageVersion: includeMetadata ? readPackageVersion(projectRoot) : void 0,
|
|
124
|
+
gitSha: includeMetadata ? readGitSha(projectRoot) : void 0,
|
|
125
|
+
ci: detectCI()
|
|
126
|
+
};
|
|
127
|
+
const rawRunPath = opts.rawRunPath;
|
|
128
|
+
if (rawRunPath) {
|
|
129
|
+
const absolutePath = path.isAbsolute(rawRunPath) ? rawRunPath : path.join(projectRoot, rawRunPath);
|
|
130
|
+
const dir = path.dirname(absolutePath);
|
|
131
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
132
|
+
fs.writeFileSync(absolutePath, JSON.stringify({ schemaVersion: 1, ...rawRun }, null, 2), "utf8");
|
|
133
|
+
}
|
|
134
|
+
const canonicalRun = canonicalizeRun(rawRun);
|
|
135
|
+
const generator = new ReportGenerator(opts);
|
|
136
|
+
generator.generate(canonicalRun).catch((err) => {
|
|
137
|
+
console.error("Failed to generate reports:", err);
|
|
138
|
+
});
|
|
139
|
+
clearStore();
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function buildRawRunFromCypressResult(result, options = {}) {
|
|
143
|
+
const projectRoot = options.projectRoot ?? result.config?.projectRoot ?? process.cwd();
|
|
144
|
+
const rawTestCases = [];
|
|
145
|
+
for (const run of result.runs ?? []) {
|
|
146
|
+
const specRelative = run.spec?.relative ?? "unknown";
|
|
147
|
+
for (const test of run.tests ?? []) {
|
|
148
|
+
const titlePath = test.title ?? [];
|
|
149
|
+
const meta = getMeta(specRelative, titlePath);
|
|
150
|
+
if (!meta) continue;
|
|
151
|
+
const lastAttempt = test.attempts?.length ? test.attempts[test.attempts.length - 1] : void 0;
|
|
152
|
+
const state = lastAttempt?.state ?? test.state;
|
|
153
|
+
const err = lastAttempt?.error ?? (test.displayError ? { message: test.displayError } : void 0);
|
|
154
|
+
const duration = lastAttempt?.duration ?? test.duration;
|
|
155
|
+
rawTestCases.push({
|
|
156
|
+
title: meta.scenario,
|
|
157
|
+
titlePath: meta.suitePath ? [...meta.suitePath, meta.scenario] : [meta.scenario],
|
|
158
|
+
story: meta,
|
|
159
|
+
sourceFile: specRelative,
|
|
160
|
+
sourceLine: 1,
|
|
161
|
+
status: mapCypressStateToRaw(state),
|
|
162
|
+
durationMs: duration,
|
|
163
|
+
error: err ? { message: err.message, stack: err.stack } : void 0
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const includeMetadata = options.markdown?.includeMetadata ?? true;
|
|
168
|
+
return {
|
|
169
|
+
testCases: rawTestCases,
|
|
170
|
+
startedAtMs: void 0,
|
|
171
|
+
finishedAtMs: Date.now(),
|
|
172
|
+
projectRoot,
|
|
173
|
+
packageVersion: includeMetadata ? readPackageVersion(projectRoot) : void 0,
|
|
174
|
+
gitSha: includeMetadata ? readGitSha(projectRoot) : void 0,
|
|
175
|
+
ci: detectCI()
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
async function generateReportsFromRawRun(rawRun, options) {
|
|
179
|
+
if (rawRun.testCases.length === 0) return;
|
|
180
|
+
const canonicalRun = canonicalizeRun(rawRun);
|
|
181
|
+
const generator = new ReportGenerator(options);
|
|
182
|
+
await generator.generate(canonicalRun);
|
|
183
|
+
}
|
|
184
|
+
var reporter_default = createReporter;
|
|
185
|
+
export {
|
|
186
|
+
buildRawRunFromCypressResult,
|
|
187
|
+
reporter_default as default,
|
|
188
|
+
generateReportsFromRawRun
|
|
189
|
+
};
|
|
190
|
+
//# sourceMappingURL=reporter.js.map
|