cucumber-reactive-reporter 1.0.11 → 1.1.1

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