cucumber-reactive-reporter 1.0.10 → 1.1.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.
@@ -1,179 +1,301 @@
1
+ import { createRequire } from 'module';
1
2
  import fs from 'fs';
2
3
  import 'fs/promises';
3
4
  import ncp from 'ncp';
4
5
  import path from 'path';
5
6
  import ut from 'util';
6
7
 
7
- // import { createRequire } from 'module';
8
-
9
- ncp.limit = 16;
10
-
11
- let modulePath = require.resolve("./package.json"); //trick to resolve path to the installed module
12
-
13
- /**
14
- options.filter - a RegExp instance, against which each file name is tested to determine whether to copy it or not, or a function taking single parameter: copied file name, returning true or false, determining whether to copy file or not.
15
-
16
- options.transform - a function: function (read, write) { read.pipe(write) } used to apply streaming transforms while copying.
17
-
18
- options.clobber - boolean=true. if set to false, ncp will not overwrite destination files that already exist.
19
-
20
- options.dereference - boolean=false. If set to true, ncp will follow symbolic links. For example, a symlink in the source tree pointing to a regular file will become a regular file in the destination tree. Broken symlinks will result in errors.
21
-
22
- options.stopOnErr - boolean=false. If set to true, ncp will behave like cp -r, and stop on the first error it encounters. By default, ncp continues copying, logging all errors and returning an array.
23
-
24
- options.errs - stream. If options.stopOnErr is false, a stream can be provided, and errors will be written to this stream.
25
- */
26
-
27
-
28
- let cp = (source, destination, options) => {
29
- return new Promise((resolve, reject) => {
30
- ncp(source, destination, err => {
31
- if (err) {
32
- reject(new Error(err));
33
- }
34
-
35
- resolve();
36
- });
37
- });
38
- };
39
-
40
- let _makeSafe = input => {
41
- input = input.replace(/&/g, '&');
42
- input = input.replace(/</g, '&lt;');
43
- input = input.replace(/>/g, '&gt;');
44
- return input;
45
- };
46
8
  /**
47
- *
48
- * @param source path to the cucumber results json
49
- * @param dest folder path where html report gets written to
50
- * @param options
9
+ * Purpose: Normalize cucumber JSON into reporter store state.
10
+ * Responsibilities:
11
+ * - Normalize legacy cucumber JSON to a stable feature/scenario/step shape.
12
+ * - Build feature, scenario, and step maps for the UI store.
13
+ * Inputs/Outputs: Accepts parsed cucumber JSON; returns store-shaped state.
14
+ * Invariants: Input must be legacy JSON (features/elements/steps).
15
+ * See: /agents.md
51
16
  */
52
17
 
53
-
54
- const generate = async (source, dest, options) => {
55
- options ? true : options = {};
56
- const CUCUMBER_JSON_PATH = "_cucumber-results.json";
57
- const SETTINGS_JSON_PATH = "_reporter_settings.json";
58
- const HTML_PATH = path.join(path.dirname(modulePath), "react"); // "linkTags": [{
59
- // "pattern": "[a-zA-Z]*-(\\d)*$",
60
- // "link": "https://bydeluxe.atlassian.net/browse/"
61
- // }]
62
- //defaults
63
-
64
- const {
65
- title = "Cucumber Report",
66
- //report page title
67
- description = "Cucumber report",
68
- //description to be set at the page header
69
- metadata = {},
70
- linkTags = null
71
- } = options;
72
-
73
- let __dirname = path.resolve();
74
-
75
- if (path.isAbsolute(source) === false) {
76
- source = path.join(__dirname, source);
18
+ const LEGACY_FORMAT_HELP = ["Unsupported cucumber output format.", "This reporter expects legacy JSON (features/elements/steps).", "If you are using the message formatter, rerun with --format json:<file> or", 'use inputFormat: "auto" to detect message output.'].join(" ");
19
+ const INPUT_FORMAT_HELP = ['inputFormat must be "legacy-json" or "auto".', 'Use "legacy-json" for --format json:<file> output.', 'Use "auto" to detect and reject message formatter output explicitly.'].join(" ");
20
+ const ATTACHMENTS_ENCODING_HELP = ['attachmentsEncoding must be "auto", "base64", or "raw".', 'Use "raw" if your cucumber JSON stores text attachments unencoded.', 'Use "base64" if text attachments are base64-encoded.', 'Use "auto" to decode base64-looking text attachments.'].join(" ");
21
+ const normalizeMimeType = value => String(value ?? "").split(";")[0].trim().toLowerCase();
22
+ const shouldDecodeEmbedding = mimeType => {
23
+ if (!mimeType) {
24
+ return false;
77
25
  }
78
-
79
- fs.accessSync(source);
80
-
81
- if (!dest) {
82
- dest = path.dirname(source);
83
- } else {
84
- if (path.isAbsolute(dest) === false) {
85
- dest = path.resolve(dest);
26
+ if (mimeType.startsWith("text/")) {
27
+ return true;
28
+ }
29
+ return mimeType === "application/json" || mimeType === "application/xml";
30
+ };
31
+ const looksLikeBase64 = value => {
32
+ if (typeof value !== "string") {
33
+ return false;
34
+ }
35
+ const trimmed = value.trim();
36
+ if (!trimmed || trimmed.length % 4 !== 0) {
37
+ return false;
38
+ }
39
+ if (/[^A-Za-z0-9+/=]/.test(trimmed)) {
40
+ return false;
41
+ }
42
+ return true;
43
+ };
44
+ const isLikelyText = value => {
45
+ if (typeof value !== "string") {
46
+ return false;
47
+ }
48
+ if (value.includes("\uFFFD")) {
49
+ return false;
50
+ }
51
+ const sample = value.slice(0, 2000);
52
+ if (!sample.length) {
53
+ return true;
54
+ }
55
+ let printable = 0;
56
+ for (const char of sample) {
57
+ const code = char.charCodeAt(0);
58
+ if (code === 9 || code === 10 || code === 13) {
59
+ printable += 1;
60
+ continue;
61
+ }
62
+ if (code >= 32 && code !== 127) {
63
+ printable += 1;
86
64
  }
87
65
  }
88
-
89
- console.log(`__dirname: ${__dirname}\n` + `html path: ${HTML_PATH}\n` + `source: ${source}\n` + `destination: ${dest}\n` + `title: ${title}\n` + `description: ${description}\n` + `metadata: ${ut.inspect(metadata, false, null)}\n` + `linkTags: ${ut.inspect(linkTags, false, null)}\n`); //validate input json and make a copy
90
-
91
- let str = fs.readFileSync(source).toString();
92
- let obj = JSON.parse(str);
93
-
94
- let out = _prepDataForStore(obj);
95
-
96
- let modifiedJSON = JSON.stringify(out);
97
- let destExists = true;
98
-
99
- try {
100
- fs.accessSync(dest);
101
- } catch (err) {
102
- destExists = false;
66
+ return printable / sample.length > 0.85;
67
+ };
68
+ const decodeBase64Text = value => {
69
+ if (!looksLikeBase64(value)) {
70
+ return null;
103
71
  }
104
-
105
- if (!destExists) {
106
- fs.mkdirSync(dest, {
107
- recursive: true
108
- });
72
+ const decoded = Buffer.from(value, "base64").toString("utf8");
73
+ if (!isLikelyText(decoded)) {
74
+ return null;
109
75
  }
110
-
111
- fs.writeFileSync(path.join(dest, CUCUMBER_JSON_PATH), modifiedJSON);
112
- fs.writeFileSync(path.join(dest, SETTINGS_JSON_PATH), JSON.stringify(options));
113
- await cp(HTML_PATH, dest); //swap out some tokens in the html
114
-
115
- let indexPagePath = path.join(dest, "index.html");
116
- let htmlStr = fs.readFileSync(indexPagePath, "utf8").toString();
117
- let modified = htmlStr.replace(/-=title=-/g, _makeSafe(title));
118
- fs.writeFileSync(indexPagePath, modified, "utf8");
119
- console.log("done");
76
+ return decoded;
77
+ };
78
+ const normalizeEmbeddings = (embeddings, {
79
+ attachmentsEncoding
80
+ }) => {
81
+ if (!Array.isArray(embeddings)) {
82
+ return embeddings;
83
+ }
84
+ return embeddings.map(embedding => normalizeEmbedding(embedding, {
85
+ attachmentsEncoding
86
+ }));
87
+ };
88
+ const normalizeEmbedding = (embedding, {
89
+ attachmentsEncoding
90
+ }) => {
91
+ if (!embedding || typeof embedding !== "object") {
92
+ return embedding;
93
+ }
94
+ if (attachmentsEncoding === "raw") {
95
+ return embedding;
96
+ }
97
+ const mimeType = normalizeMimeType(embedding.mime_type ?? embedding.media?.type);
98
+ if (!shouldDecodeEmbedding(mimeType)) {
99
+ return embedding;
100
+ }
101
+ if (typeof embedding.data !== "string") {
102
+ return embedding;
103
+ }
104
+ // Legacy cucumber JSON embeds text payloads as base64; decode for readable output.
105
+ const decoded = decodeBase64Text(embedding.data);
106
+ if (!decoded) {
107
+ return embedding;
108
+ }
109
+ if (mimeType === "application/json") {
110
+ try {
111
+ JSON.parse(decoded);
112
+ } catch (err) {
113
+ return embedding;
114
+ }
115
+ } else if (["application/xml", "text/xml", "text/html"].includes(mimeType)) {
116
+ if (!decoded.includes("<")) {
117
+ return embedding;
118
+ }
119
+ }
120
+ return {
121
+ ...embedding,
122
+ data: decoded
123
+ };
124
+ };
125
+ const resolveAttachmentsEncoding = ({
126
+ attachmentsEncoding,
127
+ cucumberVersion
128
+ }) => {
129
+ if (!attachmentsEncoding) {
130
+ const parsed = parseCucumberMajor(cucumberVersion);
131
+ if (Number.isFinite(parsed)) {
132
+ return parsed < 7 ? "raw" : "base64";
133
+ }
134
+ return "auto";
135
+ }
136
+ if (!["auto", "base64", "raw"].includes(attachmentsEncoding)) {
137
+ throw new Error(ATTACHMENTS_ENCODING_HELP);
138
+ }
139
+ return attachmentsEncoding;
140
+ };
141
+ const parseCucumberMajor = cucumberVersion => {
142
+ if (!cucumberVersion) {
143
+ return null;
144
+ }
145
+ const value = String(cucumberVersion).trim();
146
+ if (!value) {
147
+ return null;
148
+ }
149
+ const match = value.match(/(\d+)(?:\.\d+)?/);
150
+ if (!match) {
151
+ return null;
152
+ }
153
+ const major = Number.parseInt(match[1], 10);
154
+ return Number.isFinite(major) ? major : null;
120
155
  };
121
156
 
122
- let _prepDataForStore = data => {
123
- let state = {};
124
- state.features = {};
125
- state.features.list = [];
126
- state.features.featuresMap = {};
127
- state.scenarios = {};
128
- state.scenarios.list = [];
129
- state.scenarios.scenariosMap = {};
130
- state.steps = {};
131
- state.steps.stepsMap = {};
132
- state.steps.totalDurationNanoSec = 0; //parse
133
-
157
+ /**
158
+ * Convert cucumber JSON into the reporter store shape.
159
+ * @param {unknown} input parsed cucumber JSON
160
+ * @returns {Object} normalized state for the UI store
161
+ * @throws {Error} when input is not legacy cucumber JSON
162
+ * @example
163
+ * const state = prepareStoreState(legacyJsonArray);
164
+ */
165
+ const prepareStoreState = (input, {
166
+ inputFormat = "legacy-json",
167
+ attachmentsEncoding,
168
+ cucumberVersion
169
+ } = {}) => {
170
+ if (!["legacy-json", "auto"].includes(inputFormat)) {
171
+ throw new Error(INPUT_FORMAT_HELP);
172
+ }
173
+ if (inputFormat === "auto" && looksLikeMessageStream(input)) {
174
+ throw new Error(LEGACY_FORMAT_HELP);
175
+ }
176
+ const resolvedEncoding = resolveAttachmentsEncoding({
177
+ attachmentsEncoding,
178
+ cucumberVersion
179
+ });
180
+ const features = resolveFeatures(input);
181
+ if (!features) {
182
+ throw new Error(LEGACY_FORMAT_HELP);
183
+ }
184
+ const state = createEmptyState();
134
185
  let featureIndex = 0;
135
- console.time("loadTotal");
136
-
137
- for (let f of data) {
138
- //FEATURE
139
- //cucumber id field is not guaranteed to be unique for feature/scenario/step
140
- f.id = `${featureIndex++}_${f.id}`;
141
-
142
- _processFeature(state, f); //SCENARIO
143
-
144
-
145
- let numScenarios = f.elements.length; //avoid multiple lookups;
146
-
147
- if (f.elements && numScenarios) {
148
- let sc_index = 0;
149
-
150
- for (let sc of f.elements) {
151
- //need to make scenario id unique as well
152
- sc_index++;
153
- let sc_id_arr = sc.id.split(";");
154
- sc_id_arr[0] = f.id;
155
-
156
- if (sc_id_arr.length) {
157
- sc_id_arr[1] = `${sc_index - 1}_${sc_id_arr[1]}`;
158
- }
159
-
160
- sc.id = sc_id_arr.join(";");
161
-
162
- _processScenario(state, f.id, sc); //STEPS
163
-
164
-
165
- for (let st of sc.steps) {
166
- _processStep(state, sc.id, st);
167
- }
168
- }
186
+ for (const rawFeature of features) {
187
+ if (!rawFeature) {
188
+ continue;
169
189
  }
190
+ const feature = normalizeFeature(rawFeature, featureIndex);
191
+ featureIndex += 1;
192
+ processFeature(state, feature);
193
+ processFeatureElements(state, feature, {
194
+ attachmentsEncoding: resolvedEncoding
195
+ });
170
196
  }
171
-
172
- console.timeEnd("loadTotal");
173
197
  return state;
174
198
  };
175
-
176
- let _processFeature = (state, f) => {
199
+ const createEmptyState = () => ({
200
+ features: {
201
+ list: [],
202
+ featuresMap: {}
203
+ },
204
+ scenarios: {
205
+ list: [],
206
+ scenariosMap: {}
207
+ },
208
+ steps: {
209
+ stepsMap: {},
210
+ totalDurationNanoSec: 0
211
+ }
212
+ });
213
+ const looksLikeMessageStream = input => {
214
+ if (!Array.isArray(input)) {
215
+ return false;
216
+ }
217
+ return input.some(item => {
218
+ if (!item || typeof item !== "object") {
219
+ return false;
220
+ }
221
+ return "gherkinDocument" in item || "pickle" in item || "testCaseStarted" in item || "testCaseFinished" in item || "envelope" in item;
222
+ });
223
+ };
224
+ const resolveFeatures = input => {
225
+ if (Array.isArray(input)) {
226
+ return input;
227
+ }
228
+ if (input && Array.isArray(input.features)) {
229
+ return input.features;
230
+ }
231
+ return null;
232
+ };
233
+ const normalizeFeature = (feature, index) => {
234
+ const baseId = feature?.id ?? feature?.name ?? "feature";
235
+ const elements = normalizeElements(feature);
236
+ return {
237
+ ...feature,
238
+ id: `${index}_${baseId}`,
239
+ elements,
240
+ tags: Array.isArray(feature?.tags) ? feature.tags : []
241
+ };
242
+ };
243
+ const normalizeElements = feature => {
244
+ if (!feature) {
245
+ return [];
246
+ }
247
+ if (Array.isArray(feature.elements)) {
248
+ return feature.elements;
249
+ }
250
+ if (Array.isArray(feature.scenarios)) {
251
+ return feature.scenarios;
252
+ }
253
+ if (Array.isArray(feature.children)) {
254
+ return flattenChildren(feature.children);
255
+ }
256
+ return [];
257
+ };
258
+ const flattenChildren = children => {
259
+ const flattened = [];
260
+ for (const child of children) {
261
+ if (!child) {
262
+ continue;
263
+ }
264
+ if (child.scenario) {
265
+ flattened.push(child.scenario);
266
+ continue;
267
+ }
268
+ if (child.background) {
269
+ flattened.push(child.background);
270
+ continue;
271
+ }
272
+ if (child.rule && Array.isArray(child.rule.children)) {
273
+ flattened.push(...flattenChildren(child.rule.children));
274
+ continue;
275
+ }
276
+ if (Array.isArray(child.children)) {
277
+ flattened.push(...flattenChildren(child.children));
278
+ continue;
279
+ }
280
+ flattened.push(child);
281
+ }
282
+ return flattened;
283
+ };
284
+ const normalizeScenario = (featureId, scenario, index) => {
285
+ const baseId = scenario?.id ?? scenario?.name ?? "scenario";
286
+ const scenarioId = buildScenarioId(featureId, baseId, index);
287
+ return {
288
+ ...scenario,
289
+ id: scenarioId,
290
+ tags: Array.isArray(scenario?.tags) ? scenario.tags : []
291
+ };
292
+ };
293
+ const buildScenarioId = (featureId, scenarioId, index) => {
294
+ const parts = String(scenarioId).split(";");
295
+ const suffix = parts.length > 1 ? parts[1] : parts[0];
296
+ return `${featureId};${index}_${suffix}`;
297
+ };
298
+ const processFeature = (state, feature) => {
177
299
  const {
178
300
  description,
179
301
  elements,
@@ -181,42 +303,34 @@ let _processFeature = (state, f) => {
181
303
  keyword,
182
304
  line,
183
305
  name,
184
- tags: [...tags],
306
+ tags,
185
307
  uri
186
- } = f;
187
- const allTags = [...tags]; //figure out if it has failed stuff
188
-
308
+ } = feature;
309
+ const allTags = Array.isArray(tags) ? [...tags] : [];
189
310
  let numFailedScenarios = 0;
190
311
  let numSkippedScenarios = 0;
191
-
192
- if (elements && elements.length) {
193
- for (let el of elements) {
194
- //collect scenario tags
195
- if (el.tags && el.tags.length) {
196
- let temp = allTags.map(t => t.name);
197
- el.tags.forEach(tag => {
198
- if (temp.includes(tag.name) === false) {
199
- allTags.push(tag);
200
- }
201
- });
312
+ const elementList = Array.isArray(elements) ? elements : [];
313
+ for (const element of elementList) {
314
+ const elementTags = Array.isArray(element?.tags) ? element.tags : [];
315
+ const seen = allTags.map(tag => tag.name);
316
+ for (const tag of elementTags) {
317
+ if (tag?.name && !seen.includes(tag.name)) {
318
+ allTags.push(tag);
202
319
  }
203
-
204
- if (el.steps && el.steps.length) {
205
- for (let step of el.steps) {
206
- if (step.result && step.result.status === "failed") {
207
- numFailedScenarios++;
208
- break;
209
- }
210
-
211
- if (step.result && step.result.status === "skipped") {
212
- numSkippedScenarios++;
213
- break;
214
- }
215
- }
320
+ }
321
+ const steps = Array.isArray(element?.steps) ? element.steps : [];
322
+ for (const step of steps) {
323
+ const status = step?.result?.status;
324
+ if (status === "failed") {
325
+ numFailedScenarios += 1;
326
+ break;
327
+ }
328
+ if (status === "skipped") {
329
+ numSkippedScenarios += 1;
330
+ break;
216
331
  }
217
332
  }
218
333
  }
219
-
220
334
  state.features.list.push(id);
221
335
  state.features.featuresMap[id] = {
222
336
  id,
@@ -225,20 +339,19 @@ let _processFeature = (state, f) => {
225
339
  keyword,
226
340
  name,
227
341
  line,
228
- tags,
342
+ tags: Array.isArray(tags) ? tags : [],
229
343
  allTags,
230
344
  numFailedScenarios,
231
345
  numSkippedScenarios
232
346
  };
233
347
  };
234
-
235
- let _processScenario = (state, featureId, scenario) => {
348
+ const processScenario = (state, featureId, scenario) => {
236
349
  const {
237
350
  id,
238
351
  keyword,
239
352
  line,
240
353
  name,
241
- tags: [...tags],
354
+ tags,
242
355
  type,
243
356
  uri
244
357
  } = scenario;
@@ -252,13 +365,44 @@ let _processScenario = (state, featureId, scenario) => {
252
365
  name,
253
366
  passedSteps: 0,
254
367
  skippedSteps: 0,
255
- tags,
368
+ tags: Array.isArray(tags) ? tags : [],
256
369
  type,
257
370
  uri
258
371
  };
259
372
  };
260
-
261
- let _processStep = (state, scenarioId, st) => {
373
+ const processFeatureElements = (state, feature, {
374
+ attachmentsEncoding
375
+ }) => {
376
+ const elements = feature.elements;
377
+ if (!elements.length) {
378
+ return;
379
+ }
380
+ let scenarioIndex = 0;
381
+ for (const rawScenario of elements) {
382
+ if (!rawScenario) {
383
+ continue;
384
+ }
385
+ const scenario = normalizeScenario(feature.id, rawScenario, scenarioIndex);
386
+ scenarioIndex += 1;
387
+ processScenario(state, feature.id, scenario);
388
+ processScenarioSteps(state, scenario, {
389
+ attachmentsEncoding
390
+ });
391
+ }
392
+ };
393
+ const processScenarioSteps = (state, scenario, {
394
+ attachmentsEncoding
395
+ }) => {
396
+ const steps = Array.isArray(scenario.steps) ? scenario.steps : [];
397
+ for (const step of steps) {
398
+ processStep(state, scenario.id, step, {
399
+ attachmentsEncoding
400
+ });
401
+ }
402
+ };
403
+ const processStep = (state, scenarioId, step, {
404
+ attachmentsEncoding
405
+ }) => {
262
406
  const {
263
407
  arguments: args,
264
408
  embeddings,
@@ -266,18 +410,22 @@ let _processStep = (state, scenarioId, st) => {
266
410
  keyword,
267
411
  line,
268
412
  name,
269
- result: {
270
- duration,
271
- error_message,
272
- status
273
- }
274
- } = st;
275
- let location = "";
276
- if (st.match) location = st.match.location;
277
- let step = {
278
- args,
413
+ result
414
+ } = step ?? {};
415
+ const {
279
416
  duration,
280
- embeddings,
417
+ error_message,
418
+ status
419
+ } = result ?? {};
420
+ const durationValue = typeof duration === "string" ? Number(duration) : duration;
421
+ const location = step?.match?.location ?? "";
422
+ const normalizedEmbeddings = normalizeEmbeddings(embeddings, {
423
+ attachmentsEncoding
424
+ });
425
+ const stepData = {
426
+ args,
427
+ duration: durationValue,
428
+ embeddings: normalizedEmbeddings,
281
429
  error_message,
282
430
  keyword,
283
431
  line,
@@ -285,28 +433,177 @@ let _processStep = (state, scenarioId, st) => {
285
433
  name,
286
434
  status
287
435
  };
288
- if (!state.steps.stepsMap[scenarioId]) state.steps.stepsMap[scenarioId] = {
289
- steps: []
290
- };
291
- state.steps.stepsMap[scenarioId].steps.push(step);
292
-
293
- if (isNaN(duration) === false) {
294
- state.steps.totalDurationNanoSec = state.steps.totalDurationNanoSec + duration;
436
+ if (!state.steps.stepsMap[scenarioId]) {
437
+ state.steps.stepsMap[scenarioId] = {
438
+ steps: []
439
+ };
295
440
  }
296
-
297
- if (!hidden || embeddings && embeddings.length) {
441
+ state.steps.stepsMap[scenarioId].steps.push(stepData);
442
+ if (Number.isFinite(durationValue)) {
443
+ state.steps.totalDurationNanoSec += durationValue;
444
+ }
445
+ if (!hidden || normalizedEmbeddings && normalizedEmbeddings.length) {
298
446
  if (status === "passed") {
299
- state.scenarios.scenariosMap[scenarioId].passedSteps++;
447
+ state.scenarios.scenariosMap[scenarioId].passedSteps += 1;
300
448
  } else if (status === "skipped") {
301
- state.scenarios.scenariosMap[scenarioId].skippedSteps++;
449
+ state.scenarios.scenariosMap[scenarioId].skippedSteps += 1;
302
450
  }
303
451
  }
304
-
305
452
  if (status === "failed") {
306
- state.scenarios.scenariosMap[scenarioId].failedSteps++;
453
+ state.scenarios.scenariosMap[scenarioId].failedSteps += 1;
307
454
  }
308
455
  };
309
456
 
457
+ /**
458
+ * Purpose: Generate HTML reports from cucumber JSON output.
459
+ * Responsibilities:
460
+ * - Normalize cucumber JSON into store state.
461
+ * - Copy report assets and write report metadata.
462
+ * Inputs/Outputs: Reads a cucumber JSON file and writes a report folder.
463
+ * Invariants: Expects legacy cucumber JSON (features/elements/steps).
464
+ * See: /agents.md
465
+ */
466
+ const require = createRequire(import.meta.url);
467
+ ncp.limit = 16;
468
+ const modulePath = require.resolve("./package.json"); //trick to resolve path to the installed module
469
+
470
+ /**
471
+ options.filter - a RegExp instance, against which each file name is tested to determine whether to copy it or not, or a function taking single parameter: copied file name, returning true or false, determining whether to copy file or not.
472
+
473
+ options.transform - a function: function (read, write) { read.pipe(write) } used to apply streaming transforms while copying.
474
+
475
+ options.clobber - boolean=true. if set to false, ncp will not overwrite destination files that already exist.
476
+
477
+ options.dereference - boolean=false. If set to true, ncp will follow symbolic links. For example, a symlink in the source tree pointing to a regular file will become a regular file in the destination tree. Broken symlinks will result in errors.
478
+
479
+ options.stopOnErr - boolean=false. If set to true, ncp will behave like cp -r, and stop on the first error it encounters. By default, ncp continues copying, logging all errors and returning an array.
480
+
481
+ options.errs - stream. If options.stopOnErr is false, a stream can be provided, and errors will be written to this stream.
482
+ */
483
+
484
+ let cp = (source, destination, options) => {
485
+ return new Promise((resolve, reject) => {
486
+ ncp(source, destination, err => {
487
+ if (err) {
488
+ reject(new Error(err));
489
+ }
490
+ resolve();
491
+ });
492
+ });
493
+ };
494
+ const _makeSafe = input => {
495
+ input = input.replace(/&/g, '&amp;');
496
+ input = input.replace(/</g, '&lt;');
497
+ input = input.replace(/>/g, '&gt;');
498
+ return input;
499
+ };
500
+
501
+ /**
502
+ * Generate a report from cucumber JSON output.
503
+ * @param {string} source path to the cucumber results JSON file
504
+ * @param {string} dest folder path where the HTML report gets written
505
+ * @param {Object} options report configuration overrides
506
+ * @param {"legacy-json"|"auto"} [options.inputFormat] input JSON format selector
507
+ * @param {"auto"|"base64"|"raw"} [options.attachmentsEncoding] attachment encoding
508
+ * @param {string} [options.cucumberVersion] cucumber version (for encoding hints)
509
+ * @returns {Promise<void>} resolves when report assets are written
510
+ * @throws {Error} when input JSON is invalid or unsupported
511
+ * @example
512
+ * await generate("results/cucumber.json", "reports/out", { title: "Run #1" });
513
+ */
514
+ const generate = async (source, dest, options) => {
515
+ options ? true : options = {};
516
+ const CUCUMBER_JSON_PATH = "_cucumber-results.json";
517
+ const SETTINGS_JSON_PATH = "_reporter_settings.json";
518
+ const HTML_PATH = path.join(path.dirname(modulePath), "react");
519
+
520
+ // "linkTags": [{
521
+ // "pattern": "[a-zA-Z]*-(\\d)*$",
522
+ // "link": "https://bydeluxe.atlassian.net/browse/"
523
+
524
+ // }]
525
+ //defaults
526
+ const {
527
+ title = "Cucumber Report",
528
+ //report page title
529
+ description = "Cucumber report",
530
+ //description to be set at the page header
531
+ metadata = {},
532
+ linkTags = null,
533
+ inputFormat = "legacy-json",
534
+ attachmentsEncoding,
535
+ cucumberVersion
536
+ } = options;
537
+ let __dirname = path.resolve();
538
+ if (path.isAbsolute(source) === false) {
539
+ source = path.join(__dirname, source);
540
+ }
541
+ fs.accessSync(source);
542
+ if (!dest) {
543
+ dest = path.dirname(source);
544
+ } else {
545
+ if (path.isAbsolute(dest) === false) {
546
+ dest = path.resolve(dest);
547
+ }
548
+ }
549
+ console.log(`__dirname: ${__dirname}\n` + `html path: ${HTML_PATH}\n` + `source: ${source}\n` + `destination: ${dest}\n` + `title: ${title}\n` + `description: ${description}\n` + `metadata: ${ut.inspect(metadata, false, null)}\n` + `linkTags: ${ut.inspect(linkTags, false, null)}\n`);
550
+
551
+ //validate input json and make a copy
552
+ let str = fs.readFileSync(source, "utf8");
553
+ let obj = parseInputData(source, str);
554
+ let out = prepareStoreState(obj, {
555
+ inputFormat,
556
+ attachmentsEncoding,
557
+ cucumberVersion
558
+ });
559
+ let modifiedJSON = JSON.stringify(out);
560
+ let destExists = true;
561
+ try {
562
+ fs.accessSync(dest);
563
+ } catch (err) {
564
+ destExists = false;
565
+ }
566
+ if (!destExists) {
567
+ fs.mkdirSync(dest, {
568
+ recursive: true
569
+ });
570
+ }
571
+ fs.writeFileSync(path.join(dest, CUCUMBER_JSON_PATH), modifiedJSON);
572
+ fs.writeFileSync(path.join(dest, SETTINGS_JSON_PATH), JSON.stringify(options));
573
+ await cp(HTML_PATH, dest);
574
+ //swap out some tokens in the html
575
+ let indexPagePath = path.join(dest, "index.html");
576
+ let htmlStr = fs.readFileSync(indexPagePath, "utf8").toString();
577
+ let modified = htmlStr.replace(/-=title=-/g, _makeSafe(title));
578
+ fs.writeFileSync(indexPagePath, modified, "utf8");
579
+ console.log("done");
580
+ };
581
+ const parseInputData = (source, rawText) => {
582
+ try {
583
+ return JSON.parse(rawText);
584
+ } catch (err) {
585
+ const ndjson = parseNdjson(rawText);
586
+ if (ndjson) {
587
+ return ndjson;
588
+ }
589
+ throw new Error(`Invalid JSON in ${source}: ${err.message}`);
590
+ }
591
+ };
592
+ const parseNdjson = rawText => {
593
+ const lines = rawText.split(/\r?\n/).filter(line => line.trim().length);
594
+ if (!lines.length) {
595
+ return null;
596
+ }
597
+ const items = [];
598
+ for (const line of lines) {
599
+ try {
600
+ items.push(JSON.parse(line));
601
+ } catch (err) {
602
+ return null;
603
+ }
604
+ }
605
+ return items;
606
+ };
310
607
  var index = {
311
608
  generate: generate
312
609
  };