executable-stories-formatters 0.7.7 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -15813,11 +15813,11 @@ var CucumberHtmlFormatter = class {
15813
15813
  for (const envelope of envelopes) {
15814
15814
  const accepted = htmlStream.write(envelope);
15815
15815
  if (!accepted) {
15816
- await new Promise((resolve7) => htmlStream.once("drain", resolve7));
15816
+ await new Promise((resolve8) => htmlStream.once("drain", resolve8));
15817
15817
  }
15818
15818
  }
15819
- await new Promise((resolve7, reject) => {
15820
- collector.on("finish", resolve7);
15819
+ await new Promise((resolve8, reject) => {
15820
+ collector.on("finish", resolve8);
15821
15821
  collector.on("error", reject);
15822
15822
  htmlStream.end();
15823
15823
  });
@@ -17973,6 +17973,37 @@ function listScenarios(args, _deps) {
17973
17973
  }));
17974
17974
  return JSON.stringify(items, null, 2);
17975
17975
  }
17976
+ if (format === "csv") {
17977
+ const header = "id,scenario,status,sourceFile,sourceLine,tags";
17978
+ const rows = testCases.map((tc) => {
17979
+ const fields = [
17980
+ tc.id,
17981
+ tc.story.scenario,
17982
+ tc.status,
17983
+ tc.sourceFile,
17984
+ String(tc.sourceLine),
17985
+ tc.tags.join(" ")
17986
+ ];
17987
+ return fields.map((f) => {
17988
+ if (f.includes(",") || f.includes('"') || f.includes("\n")) {
17989
+ return `"${f.replace(/"/g, '""')}"`;
17990
+ }
17991
+ return f;
17992
+ }).join(",");
17993
+ });
17994
+ return [header, ...rows].join("\n");
17995
+ }
17996
+ if (format === "markdown-table") {
17997
+ const header = "| Status | Scenario | Location | Tags |";
17998
+ const divider = "|--------|----------|----------|------|";
17999
+ const rows = testCases.map((tc) => {
18000
+ const icon = STATUS_ICONS[tc.status] ?? "?";
18001
+ const location = `${tc.sourceFile}:${tc.sourceLine}`;
18002
+ const tags = tc.tags.map((t) => `@${t}`).join(" ");
18003
+ return `| ${icon} | ${tc.story.scenario} | ${location} | ${tags} |`;
18004
+ });
18005
+ return [header, divider, ...rows].join("\n");
18006
+ }
17976
18007
  if (testCases.length === 0) {
17977
18008
  return "No scenarios found.";
17978
18009
  }
@@ -18444,6 +18475,22 @@ function copyDirRecursive(src, dest) {
18444
18475
  }
18445
18476
  }
18446
18477
 
18478
+ // src/config.ts
18479
+ import { existsSync as existsSync6 } from "fs";
18480
+ import { resolve as resolve6 } from "path";
18481
+ async function loadConfig(configPath) {
18482
+ const resolved = configPath ? resolve6(configPath) : resolve6(process.cwd(), "executable-stories.config.js");
18483
+ if (!existsSync6(resolved)) return {};
18484
+ const mod = await import(resolved);
18485
+ const config = mod.default;
18486
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
18487
+ throw new Error(
18488
+ `Config file at ${resolved} must export a default object. Got: ${typeof config}`
18489
+ );
18490
+ }
18491
+ return config;
18492
+ }
18493
+
18447
18494
  // src/cli.ts
18448
18495
  var EXIT_SUCCESS = 0;
18449
18496
  var EXIT_SCHEMA_VALIDATION = 1;
@@ -18470,7 +18517,7 @@ SUBCOMMANDS
18470
18517
  init-astro Scaffold an Astro Starlight docs site for story output
18471
18518
 
18472
18519
  OPTIONS
18473
- --format <formats> Comma-separated formats (default: html)
18520
+ --format <formats> Comma-separated formats: html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, astro, or custom names from config (default: html)
18474
18521
  astro Starlight-compatible Markdown (for Astro docs sites)
18475
18522
  html Custom HTML report (accessible, dark mode, mermaid)
18476
18523
  cucumber-html Official Cucumber HTML report
@@ -18478,6 +18525,7 @@ OPTIONS
18478
18525
  junit JUnit XML
18479
18526
  cucumber-json Cucumber JSON
18480
18527
  cucumber-messages Raw NDJSON (Cucumber Messages)
18528
+ --config <path> Path to executable-stories.config.js (default: ./executable-stories.config.js)
18481
18529
  --input-type <type> Input type: raw, canonical, or ndjson (default: raw)
18482
18530
  --output-dir <dir> Output directory (default: reports)
18483
18531
  --output-name <name> Base filename (default: index)
@@ -18501,7 +18549,8 @@ OPTIONS
18501
18549
  --asset-mode <mode> Asset bundling: "none" (default) or "copy"
18502
18550
  --allow-missing-assets Warn on missing assets instead of failing
18503
18551
  --stdin Read JSON from stdin instead of file
18504
- --json-summary Print machine-parsable JSON summary
18552
+ --list-format <format> list output format: text (default), json, csv, markdown-table
18553
+ --json-summary Deprecated alias for --list-format json
18505
18554
  --baseline <path|auto> Compare baseline file, or auto-pick a prior run for compare
18506
18555
  --baseline-dir <dir> Directory to scan when --baseline auto is used
18507
18556
  --pr-summary Print a PR-friendly markdown summary after compare
@@ -18510,7 +18559,8 @@ OPTIONS
18510
18559
  --help Show this help message
18511
18560
 
18512
18561
  LIST
18513
- list prints one scenario per line (text by default, JSON with --json-summary)
18562
+ list prints one scenario per line (--list-format text by default)
18563
+ list --list-format json outputs machine-parsable JSON (--json-summary is a deprecated alias)
18514
18564
  list supports --include-tags, --exclude-tags for filtering
18515
18565
  list supports --input-type and --stdin
18516
18566
 
@@ -18549,7 +18599,7 @@ EXIT CODES
18549
18599
  3 Formatter/generation failure
18550
18600
  4 Bad arguments / usage error
18551
18601
  `.trim();
18552
- function parseCliArgs(argv) {
18602
+ async function parseCliArgs(argv) {
18553
18603
  const args = argv.slice(2);
18554
18604
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
18555
18605
  console.log(HELP_TEXT);
@@ -18609,6 +18659,7 @@ function parseCliArgs(argv) {
18609
18659
  "html-theme-picker": { type: "boolean", default: false },
18610
18660
  stdin: { type: "boolean", default: false },
18611
18661
  "json-summary": { type: "boolean", default: false },
18662
+ "list-format": { type: "string", default: "text" },
18612
18663
  "emit-canonical": { type: "string" },
18613
18664
  "slack-webhook": { type: "string" },
18614
18665
  "teams-webhook": { type: "string" },
@@ -18627,6 +18678,7 @@ function parseCliArgs(argv) {
18627
18678
  "allow-missing-assets": { type: "boolean", default: false },
18628
18679
  "pr-summary": { type: "boolean", default: false },
18629
18680
  "pr-summary-file": { type: "string" },
18681
+ "config": { type: "string" },
18630
18682
  help: { type: "boolean", default: false }
18631
18683
  },
18632
18684
  allowPositionals: true,
@@ -18664,15 +18716,20 @@ function parseCliArgs(argv) {
18664
18716
  console.error(`Error: --input-type must be "raw", "canonical", or "ndjson", got "${inputType}".`);
18665
18717
  process.exit(EXIT_USAGE);
18666
18718
  }
18667
- const validFormats = /* @__PURE__ */ new Set(["astro", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html"]);
18719
+ const pluginConfig = await loadConfig(values["config"]);
18720
+ const customFormatterNames = new Set(Object.keys(pluginConfig.formatters ?? {}));
18721
+ const builtInFormats = /* @__PURE__ */ new Set(["astro", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html"]);
18668
18722
  const formatStr = values.format;
18669
- const formats = formatStr.split(",").map((f) => f.trim());
18670
- for (const f of formats) {
18671
- if (!validFormats.has(f)) {
18672
- console.error(`Error: Unknown format "${f}". Valid: astro, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html.`);
18673
- process.exit(EXIT_USAGE);
18674
- }
18723
+ const allRequestedFormats = formatStr.split(",").map((f) => f.trim());
18724
+ const builtInRequested = allRequestedFormats.filter((f) => builtInFormats.has(f));
18725
+ const customRequested = allRequestedFormats.filter((f) => customFormatterNames.has(f));
18726
+ const unknownFormats = allRequestedFormats.filter((f) => !builtInFormats.has(f) && !customFormatterNames.has(f));
18727
+ if (unknownFormats.length > 0) {
18728
+ const knownCustom = customFormatterNames.size > 0 ? `, ${[...customFormatterNames].join(", ")}` : "";
18729
+ console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html${knownCustom}.`);
18730
+ process.exit(EXIT_USAGE);
18675
18731
  }
18732
+ const formats = builtInRequested;
18676
18733
  const htmlTheme = values["html-theme"];
18677
18734
  const validThemes = /* @__PURE__ */ new Set(["default", "corporate", "terminal", "minimal", "dashboard", "playful"]);
18678
18735
  if (!validThemes.has(htmlTheme)) {
@@ -18740,7 +18797,7 @@ function parseCliArgs(argv) {
18740
18797
  console.error(`Error: --asset-mode must be "none" or "copy", got "${assetModeRaw}".`);
18741
18798
  process.exit(EXIT_USAGE);
18742
18799
  }
18743
- return {
18800
+ const cliArgs = {
18744
18801
  subcommand,
18745
18802
  inputFile,
18746
18803
  baselineFile,
@@ -18769,6 +18826,7 @@ function parseCliArgs(argv) {
18769
18826
  htmlNoToc: values["html-no-toc"],
18770
18827
  htmlThemePicker: values["html-theme-picker"],
18771
18828
  jsonSummary: values["json-summary"],
18829
+ listFormat: values["list-format"],
18772
18830
  emitCanonical: values["emit-canonical"],
18773
18831
  slackWebhook,
18774
18832
  teamsWebhook,
@@ -18786,8 +18844,10 @@ function parseCliArgs(argv) {
18786
18844
  assetMode: assetModeRaw,
18787
18845
  allowMissingAssets: values["allow-missing-assets"],
18788
18846
  prSummary: values["pr-summary"],
18789
- prSummaryFile: values["pr-summary-file"]
18847
+ prSummaryFile: values["pr-summary-file"],
18848
+ config: values["config"]
18790
18849
  };
18850
+ return { args: cliArgs, pluginConfig, customRequested };
18791
18851
  }
18792
18852
  async function readInput(args) {
18793
18853
  if (args.stdin) {
@@ -18809,11 +18869,11 @@ function readFileInput(filePath) {
18809
18869
  return fs7.readFileSync(resolved, "utf8");
18810
18870
  }
18811
18871
  function readStdin() {
18812
- return new Promise((resolve7, reject) => {
18872
+ return new Promise((resolve8, reject) => {
18813
18873
  const chunks = [];
18814
18874
  process.stdin.setEncoding("utf8");
18815
18875
  process.stdin.on("data", (chunk) => chunks.push(chunk));
18816
- process.stdin.on("end", () => resolve7(chunks.join("")));
18876
+ process.stdin.on("end", () => resolve8(chunks.join("")));
18817
18877
  process.stdin.on("error", reject);
18818
18878
  });
18819
18879
  }
@@ -18973,7 +19033,7 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
18973
19033
  return picked.file;
18974
19034
  }
18975
19035
  async function main() {
18976
- const args = parseCliArgs(process.argv);
19036
+ const { args, pluginConfig, customRequested } = await parseCliArgs(process.argv);
18977
19037
  const startMs = Date.now();
18978
19038
  if (args.subcommand === "compare") {
18979
19039
  const currentText = readFileInput(args.currentFile);
@@ -18994,8 +19054,16 @@ async function main() {
18994
19054
  if (args.subcommand === "list") {
18995
19055
  const text2 = await readInput(args);
18996
19056
  const run = applySelection(normalizeRunFromText(text2, args).run, args);
18997
- const outputFormat = args.jsonSummary ? "json" : "text";
18998
- const output = listScenarios({ testCases: run.testCases, format: outputFormat }, {});
19057
+ const resolvedFormat = args.jsonSummary ? "json" : args.listFormat;
19058
+ const validListFormats = /* @__PURE__ */ new Set(["text", "json", "csv", "markdown-table"]);
19059
+ if (!validListFormats.has(resolvedFormat)) {
19060
+ console.error(`Error: Unknown list format "${resolvedFormat}". Valid: text, json, csv, markdown-table.`);
19061
+ process.exit(EXIT_USAGE);
19062
+ }
19063
+ const output = listScenarios(
19064
+ { testCases: run.testCases, format: resolvedFormat },
19065
+ {}
19066
+ );
18999
19067
  console.log(output);
19000
19068
  process.exit(EXIT_SUCCESS);
19001
19069
  }
@@ -19048,6 +19116,7 @@ async function main() {
19048
19116
  }
19049
19117
  try {
19050
19118
  const result = await generateReports(run, args);
19119
+ runCustomFormatters(run, customRequested, pluginConfig.formatters ?? {}, args);
19051
19120
  await dispatchNotifications(run, args);
19052
19121
  runHistoryPipeline(run, args);
19053
19122
  printResult(result, args, startMs);
@@ -19106,6 +19175,7 @@ ${msg}`);
19106
19175
  }
19107
19176
  try {
19108
19177
  const result = await generateReports(run, args);
19178
+ runCustomFormatters(run, customRequested, pluginConfig.formatters ?? {}, args);
19109
19179
  await dispatchNotifications(run, args);
19110
19180
  runHistoryPipeline(run, args);
19111
19181
  printResult(result, args, startMs);
@@ -19163,6 +19233,7 @@ ${msg}`);
19163
19233
  }
19164
19234
  try {
19165
19235
  const result = await generateReports(canonical, args, droppedMissingStory);
19236
+ runCustomFormatters(canonical, customRequested, pluginConfig.formatters ?? {}, args);
19166
19237
  await dispatchNotifications(canonical, args);
19167
19238
  runHistoryPipeline(canonical, args);
19168
19239
  printResult(result, args, startMs, droppedMissingStory);
@@ -19173,6 +19244,25 @@ ${msg}`);
19173
19244
  process.exit(EXIT_GENERATION);
19174
19245
  }
19175
19246
  }
19247
+ function runCustomFormatters(run, customRequested, formatters, args) {
19248
+ if (customRequested.length === 0) return;
19249
+ const outputDir = args.outputDir ?? ".";
19250
+ for (const formatName of customRequested) {
19251
+ const formatter = formatters[formatName];
19252
+ try {
19253
+ const content = formatter.format(run);
19254
+ const ext = formatter.fileExtension ?? formatName;
19255
+ const baseName = args.outputName ?? "report";
19256
+ const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
19257
+ const filepath = path7.join(outputDir, filename);
19258
+ fs7.mkdirSync(outputDir, { recursive: true });
19259
+ fs7.writeFileSync(filepath, content, "utf8");
19260
+ console.log(`Generated: ${filepath}`);
19261
+ } catch (err) {
19262
+ console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
19263
+ }
19264
+ }
19265
+ }
19176
19266
  async function dispatchNotifications(run, args) {
19177
19267
  const webhooks = args.webhookUrls.map((url) => {
19178
19268
  const opts = { url };