executable-stories-formatters 0.11.3 → 0.12.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 +1 @@
1
- export { J as JestAdapterOptions, n as JestAggregatedResult, o as JestFileResult, p as JestTestResult, P as PlaywrightAdapterOptions, r as PlaywrightAnnotation, s as PlaywrightAttachment, t as PlaywrightError, u as PlaywrightLocation, v as PlaywrightStatus, w as PlaywrightTestCase, x as PlaywrightTestResult, G as StoryFileReport, V as VitestAdapterOptions, L as VitestSerializedError, M as VitestState, Q as VitestTestCase, U as VitestTestModule, W as VitestTestResult, i as adaptJestRun, j as adaptPlaywrightRun, k as adaptVitestRun } from './index-DF16Xl5i.cjs';
1
+ export { J as JestAdapterOptions, n as JestAggregatedResult, o as JestFileResult, p as JestTestResult, P as PlaywrightAdapterOptions, r as PlaywrightAnnotation, s as PlaywrightAttachment, t as PlaywrightError, u as PlaywrightLocation, v as PlaywrightStatus, w as PlaywrightTestCase, x as PlaywrightTestResult, G as StoryFileReport, V as VitestAdapterOptions, L as VitestSerializedError, M as VitestState, Q as VitestTestCase, U as VitestTestModule, W as VitestTestResult, i as adaptJestRun, j as adaptPlaywrightRun, k as adaptVitestRun } from './index-mrT6-JSt.cjs';
@@ -1 +1 @@
1
- export { J as JestAdapterOptions, n as JestAggregatedResult, o as JestFileResult, p as JestTestResult, P as PlaywrightAdapterOptions, r as PlaywrightAnnotation, s as PlaywrightAttachment, t as PlaywrightError, u as PlaywrightLocation, v as PlaywrightStatus, w as PlaywrightTestCase, x as PlaywrightTestResult, G as StoryFileReport, V as VitestAdapterOptions, L as VitestSerializedError, M as VitestState, Q as VitestTestCase, U as VitestTestModule, W as VitestTestResult, i as adaptJestRun, j as adaptPlaywrightRun, k as adaptVitestRun } from './index-DF16Xl5i.js';
1
+ export { J as JestAdapterOptions, n as JestAggregatedResult, o as JestFileResult, p as JestTestResult, P as PlaywrightAdapterOptions, r as PlaywrightAnnotation, s as PlaywrightAttachment, t as PlaywrightError, u as PlaywrightLocation, v as PlaywrightStatus, w as PlaywrightTestCase, x as PlaywrightTestResult, G as StoryFileReport, V as VitestAdapterOptions, L as VitestSerializedError, M as VitestState, Q as VitestTestCase, U as VitestTestModule, W as VitestTestResult, i as adaptJestRun, j as adaptPlaywrightRun, k as adaptVitestRun } from './index-mrT6-JSt.js';
package/dist/cli.js CHANGED
@@ -416,6 +416,27 @@ var raw_run_schema_default = {
416
416
  required: ["kind", "path", "phase"],
417
417
  additionalProperties: false
418
418
  },
419
+ {
420
+ type: "object",
421
+ description: "Embedded HTML rendered in a sandboxed iframe. Exactly one of path/url/content.",
422
+ properties: {
423
+ kind: { const: "html" },
424
+ path: { type: "string", description: "Local HTML file path (inlined into the report by default)." },
425
+ url: { type: "string", description: "Remote URL rendered via iframe src." },
426
+ content: { type: "string", description: "Inline HTML content rendered via iframe srcdoc." },
427
+ title: { type: "string" },
428
+ height: { oneOf: [{ type: "number" }, { type: "string" }], description: "Iframe height: number \u2192 px, string passed through (e.g. '60vh'). Default 400px." },
429
+ phase: { $ref: "#/$defs/DocPhase" },
430
+ children: { type: "array", items: { $ref: "#/$defs/DocEntry" }, description: "Nested child doc entries for grouping." }
431
+ },
432
+ required: ["kind", "phase"],
433
+ oneOf: [
434
+ { required: ["path"] },
435
+ { required: ["url"] },
436
+ { required: ["content"] }
437
+ ],
438
+ additionalProperties: false
439
+ },
419
440
  {
420
441
  type: "object",
421
442
  description: "Custom documentation entry with arbitrary data.",
@@ -1237,6 +1258,14 @@ var CucumberJsonFormatter = class {
1237
1258
  }
1238
1259
  const embeddings = [];
1239
1260
  for (const doc of step.docs) {
1261
+ if (doc.kind === "html" && doc.content !== void 0) {
1262
+ embeddings.push({
1263
+ data: Buffer.from(doc.content, "utf8").toString("base64"),
1264
+ mime_type: "text/html",
1265
+ name: doc.title
1266
+ });
1267
+ continue;
1268
+ }
1240
1269
  if (doc.kind !== "screenshot" || !doc.path.startsWith("data:")) {
1241
1270
  continue;
1242
1271
  }
@@ -1416,6 +1445,17 @@ ${doc.markdown}`,
1416
1445
  };
1417
1446
  case "screenshot":
1418
1447
  return null;
1448
+ case "html":
1449
+ if (doc.url !== void 0 || doc.path !== void 0) {
1450
+ return {
1451
+ doc_string: {
1452
+ content: `[${doc.title ?? "Embedded HTML"}](${doc.url ?? doc.path})`,
1453
+ content_type: "text/markdown",
1454
+ line: 0
1455
+ }
1456
+ };
1457
+ }
1458
+ return null;
1419
1459
  default:
1420
1460
  return null;
1421
1461
  }
@@ -1514,6 +1554,17 @@ function copyDocEntry(entry) {
1514
1554
  phase: entry.phase,
1515
1555
  ...children
1516
1556
  };
1557
+ case "html":
1558
+ return {
1559
+ kind: "html",
1560
+ ...entry.path !== void 0 ? { path: entry.path } : {},
1561
+ ...entry.url !== void 0 ? { url: entry.url } : {},
1562
+ ...entry.content !== void 0 ? { content: entry.content } : {},
1563
+ ...entry.title !== void 0 ? { title: entry.title } : {},
1564
+ ...entry.height !== void 0 ? { height: entry.height } : {},
1565
+ phase: entry.phase,
1566
+ ...children
1567
+ };
1517
1568
  case "custom":
1518
1569
  return {
1519
1570
  kind: "custom",
@@ -1902,7 +1953,7 @@ function scenarioHasDocs(scenario) {
1902
1953
  import * as fs2 from "fs";
1903
1954
  import * as path3 from "path";
1904
1955
 
1905
- // src/formatters/html/template.ts
1956
+ // src/formatters/html/template-scripts.ts
1906
1957
  var JS_THEME = `
1907
1958
  // Theme management
1908
1959
  function getSystemTheme() {
@@ -2704,6 +2755,23 @@ function parseMarkdownSections(marked) {
2704
2755
  });
2705
2756
  }
2706
2757
  `;
2758
+ var JS_HTML_EMBED = `
2759
+ // Open srcdoc-embedded HTML (doc-html iframes) in a new tab via a blob URL
2760
+ function initHtmlEmbeds() {
2761
+ document.querySelectorAll('.doc-html-open-srcdoc').forEach((btn) => {
2762
+ btn.addEventListener('click', function() {
2763
+ const container = btn.closest('.doc-html');
2764
+ const iframe = container ? container.querySelector('iframe.doc-html-frame') : null;
2765
+ const html = iframe ? iframe.getAttribute('srcdoc') : null;
2766
+ if (!html) return;
2767
+ const url = URL.createObjectURL(new Blob([html], { type: 'text/html' }));
2768
+ window.open(url, '_blank', 'noopener');
2769
+ });
2770
+ });
2771
+ }
2772
+ `;
2773
+
2774
+ // src/formatters/html/template.ts
2707
2775
  function generateScript(options) {
2708
2776
  const initCalls = [];
2709
2777
  if (options.includeDarkMode) {
@@ -2721,6 +2789,7 @@ function generateScript(options) {
2721
2789
  initCalls.push("initHashScroll();");
2722
2790
  initCalls.push("initToc();");
2723
2791
  initCalls.push("initThemePicker();");
2792
+ initCalls.push("initHtmlEmbeds();");
2724
2793
  const initScript = `
2725
2794
  // Initialize on load
2726
2795
  document.addEventListener('DOMContentLoaded', () => {
@@ -2729,6 +2798,7 @@ document.addEventListener('DOMContentLoaded', () => {
2729
2798
  `;
2730
2799
  let script = options.includeDarkMode ? JS_THEME : "";
2731
2800
  script += JS_CORE;
2801
+ script += JS_HTML_EMBED;
2732
2802
  if (options.additionalJs) {
2733
2803
  script += options.additionalJs;
2734
2804
  }
@@ -4368,6 +4438,82 @@ body {
4368
4438
  opacity: 0.8;
4369
4439
  }
4370
4440
 
4441
+ /* ============================================================================
4442
+ Documentation Entries - Embedded HTML
4443
+ ============================================================================ */
4444
+ .doc-html {
4445
+ margin-bottom: 0.5rem;
4446
+ border: 1px solid var(--border);
4447
+ border-radius: calc(var(--radius) - 2px);
4448
+ overflow: hidden;
4449
+ }
4450
+
4451
+ .doc-html:last-child {
4452
+ margin-bottom: 0;
4453
+ }
4454
+
4455
+ .doc-html-header {
4456
+ display: flex;
4457
+ align-items: center;
4458
+ justify-content: space-between;
4459
+ gap: 0.5rem;
4460
+ padding: 0.375rem 0.75rem;
4461
+ background: var(--muted, transparent);
4462
+ border-bottom: 1px solid var(--border);
4463
+ }
4464
+
4465
+ .doc-html-title {
4466
+ font-size: 0.75rem;
4467
+ font-weight: 600;
4468
+ color: var(--muted-foreground);
4469
+ text-transform: uppercase;
4470
+ letter-spacing: 0.04em;
4471
+ }
4472
+
4473
+ .doc-html-open {
4474
+ font-size: 0.875rem;
4475
+ line-height: 1;
4476
+ padding: 0.125rem 0.375rem;
4477
+ border: 1px solid var(--border);
4478
+ border-radius: calc(var(--radius) - 4px);
4479
+ background: transparent;
4480
+ color: var(--muted-foreground);
4481
+ cursor: pointer;
4482
+ text-decoration: none;
4483
+ }
4484
+
4485
+ .doc-html-open:hover {
4486
+ color: var(--foreground);
4487
+ border-color: var(--foreground);
4488
+ }
4489
+
4490
+ .doc-html-frame {
4491
+ display: block;
4492
+ width: 100%;
4493
+ border: 0;
4494
+ background: #fff;
4495
+ }
4496
+
4497
+ .doc-html-missing {
4498
+ padding: 0.75rem 1rem;
4499
+ border: 1px dashed var(--border);
4500
+ background: var(--muted, transparent);
4501
+ color: var(--muted-foreground);
4502
+ font-size: 0.8125rem;
4503
+ }
4504
+
4505
+ .doc-html-missing-label {
4506
+ font-weight: 600;
4507
+ margin-bottom: 0.25rem;
4508
+ }
4509
+
4510
+ .doc-html-missing-path {
4511
+ font-family: var(--font-mono, ui-monospace, monospace);
4512
+ font-size: 0.75rem;
4513
+ word-break: break-all;
4514
+ opacity: 0.8;
4515
+ }
4516
+
4371
4517
  /* ============================================================================
4372
4518
  Documentation Entries - Visual Check
4373
4519
  ============================================================================ */
@@ -14663,6 +14809,36 @@ function renderDocVideo(entry, deps) {
14663
14809
  ${captionHtml}
14664
14810
  </div>`;
14665
14811
  }
14812
+ function resolveHtmlSource(entry, deps) {
14813
+ if (entry.url !== void 0) return { mode: "src", value: entry.url };
14814
+ if (entry.content !== void 0) return { mode: "srcdoc", value: entry.content };
14815
+ const filePath = entry.path ?? "";
14816
+ if (/^https?:/i.test(filePath)) return { mode: "src", value: filePath };
14817
+ const inlined = deps.readHtmlFile?.(filePath);
14818
+ if (inlined !== void 0) return { mode: "srcdoc", value: inlined };
14819
+ const isAbsoluteFsPath = /^(?:[/\\]|[A-Za-z]:[/\\])/.test(filePath);
14820
+ if (deps.readHtmlFile && isAbsoluteFsPath) return { mode: "missing", value: filePath };
14821
+ return { mode: "src", value: filePath };
14822
+ }
14823
+ function renderDocHtml(entry, deps) {
14824
+ const source = resolveHtmlSource(entry, deps);
14825
+ if (source.mode === "missing") {
14826
+ return `<div class="doc-html doc-html-missing">
14827
+ <div class="doc-html-missing-label">HTML unavailable</div>
14828
+ <div class="doc-html-missing-path">${deps.escapeHtml(source.value)}</div>
14829
+ </div>`;
14830
+ }
14831
+ const heightCss = typeof entry.height === "number" ? `${entry.height}px` : entry.height ?? "400px";
14832
+ const frame = `<iframe class="doc-html-frame" sandbox="allow-scripts" loading="lazy" style="height: ${deps.escapeHtml(heightCss)};" title="${deps.escapeHtml(entry.title ?? "Embedded HTML")}" ${source.mode}="${deps.escapeHtml(source.value)}"></iframe>`;
14833
+ const openBtn = source.mode === "src" ? `<a class="doc-html-open" href="${deps.escapeHtml(source.value)}" target="_blank" rel="noopener noreferrer" title="Open in new tab" aria-label="Open in new tab">&#x2197;</a>` : `<button type="button" class="doc-html-open doc-html-open-srcdoc" title="Open in new tab" aria-label="Open in new tab">&#x2197;</button>`;
14834
+ return `<div class="doc-html">
14835
+ <div class="doc-html-header">
14836
+ <span class="doc-html-title">${deps.escapeHtml(entry.title ?? "HTML")}</span>
14837
+ ${openBtn}
14838
+ </div>
14839
+ ${frame}
14840
+ </div>`;
14841
+ }
14666
14842
  function renderDocCustom(entry, deps) {
14667
14843
  if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
14668
14844
  const data = entry.data;
@@ -14719,6 +14895,9 @@ function renderDocEntry(entry, deps) {
14719
14895
  case "video":
14720
14896
  html = renderDocVideo(entry, deps);
14721
14897
  break;
14898
+ case "html":
14899
+ html = renderDocHtml(entry, deps);
14900
+ break;
14722
14901
  case "custom":
14723
14902
  html = renderDocCustom(entry, deps);
14724
14903
  break;
@@ -15285,6 +15464,21 @@ function readScreenshotAsDataUri(filePath) {
15285
15464
  return void 0;
15286
15465
  }
15287
15466
  }
15467
+ var HTML_INLINE_WARN_BYTES = 1024 * 1024;
15468
+ function readHtmlFileContent(filePath) {
15469
+ try {
15470
+ if (!fs2.existsSync(filePath)) return void 0;
15471
+ const buf = fs2.readFileSync(filePath);
15472
+ if (buf.byteLength > HTML_INLINE_WARN_BYTES) {
15473
+ console.warn(
15474
+ `[executable-stories] Inlining large HTML file (${Math.round(buf.byteLength / 1024)} KiB) into the report: ${filePath}. Consider --asset-mode copy.`
15475
+ );
15476
+ }
15477
+ return buf.toString("utf8");
15478
+ } catch {
15479
+ return void 0;
15480
+ }
15481
+ }
15288
15482
  function normalizeOptions(options = {}) {
15289
15483
  return {
15290
15484
  title: options.title ?? "Test Results",
@@ -15292,6 +15486,7 @@ function normalizeOptions(options = {}) {
15292
15486
  searchable: options.searchable ?? true,
15293
15487
  startCollapsed: options.startCollapsed ?? false,
15294
15488
  embedScreenshots: options.embedScreenshots ?? true,
15489
+ embedHtmlFiles: options.embedHtmlFiles ?? true,
15295
15490
  syntaxHighlighting: options.syntaxHighlighting ?? true,
15296
15491
  mermaidEnabled: options.mermaidEnabled ?? true,
15297
15492
  markdownEnabled: options.markdownEnabled ?? true,
@@ -15310,7 +15505,10 @@ function createHtmlFormatter(options = {}) {
15310
15505
  markdownEnabled: opts.markdownEnabled,
15311
15506
  mermaidEnabled: opts.mermaidEnabled,
15312
15507
  embedScreenshots: opts.embedScreenshots,
15313
- readScreenshot: (filePath) => readScreenshotAsDataUri(filePath)
15508
+ readScreenshot: (filePath) => readScreenshotAsDataUri(filePath),
15509
+ // When html-file inlining is off (e.g. --asset-mode copy), omit the read
15510
+ // hook so doc-html iframes keep their src path for the asset bundler.
15511
+ ...opts.embedHtmlFiles ? { readHtmlFile: (filePath) => readHtmlFileContent(filePath) } : {}
15314
15512
  };
15315
15513
  const renderDocs = (docs, containerClass) => {
15316
15514
  if (!docs || docs.length === 0) return "";
@@ -15602,6 +15800,8 @@ var JUnitFormatter = class {
15602
15800
  }
15603
15801
  case "screenshot":
15604
15802
  return `${indent}Screenshot: ${entry.alt ?? entry.path}`;
15803
+ case "html":
15804
+ return `${indent}HTML: ${entry.title ?? "Embedded HTML"} (${entry.url ?? entry.path ?? "inline"})`;
15605
15805
  case "custom": {
15606
15806
  const dataStr = JSON.stringify(entry.data, null, 2);
15607
15807
  const lines = [];
@@ -16053,6 +16253,25 @@ var MarkdownFormatter = class {
16053
16253
  lines.push(`${indent}`);
16054
16254
  break;
16055
16255
  }
16256
+ case "html": {
16257
+ const htmlLabel = entry.title ?? "Embedded HTML";
16258
+ if (entry.url !== void 0 || entry.path !== void 0) {
16259
+ lines.push(`${indent}[${htmlLabel}](${entry.url ?? entry.path})`);
16260
+ break;
16261
+ }
16262
+ lines.push(`${indent}<details>`);
16263
+ lines.push(`${indent}<summary>${htmlLabel}</summary>`);
16264
+ lines.push(`${indent}`);
16265
+ lines.push(`${indent}\`\`\`html`);
16266
+ for (const line of (entry.content ?? "").split("\n")) {
16267
+ lines.push(`${indent}${line}`);
16268
+ }
16269
+ lines.push(`${indent}\`\`\``);
16270
+ lines.push(`${indent}`);
16271
+ lines.push(`${indent}</details>`);
16272
+ lines.push(`${indent}`);
16273
+ break;
16274
+ }
16056
16275
  case "custom":
16057
16276
  if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
16058
16277
  const data = entry.data;
@@ -17326,6 +17545,8 @@ function formatDocEntry(doc) {
17326
17545
  return `${doc.alt ? `${escapeHtml2(doc.alt)}: ` : ""}${escapeHtml2(doc.path)}`;
17327
17546
  case "video":
17328
17547
  return `${doc.caption ? `${escapeHtml2(doc.caption)}: ` : ""}${escapeHtml2(doc.path)}`;
17548
+ case "html":
17549
+ return `${doc.title ? `${escapeHtml2(doc.title)}: ` : ""}${escapeHtml2(doc.url ?? doc.path ?? "(inline html)")}`;
17329
17550
  case "custom":
17330
17551
  return `${escapeHtml2(doc.type)}: ${escapeHtml2(JSON.stringify(doc.data))}`;
17331
17552
  }
@@ -17784,6 +18005,8 @@ function formatDocEntry2(doc) {
17784
18005
  return `${doc.alt ? `${doc.alt}: ` : ""}${doc.path}`;
17785
18006
  case "video":
17786
18007
  return `${doc.caption ? `${doc.caption}: ` : ""}${doc.path}`;
18008
+ case "html":
18009
+ return `${doc.title ? `${doc.title}: ` : ""}${doc.url ?? doc.path ?? "(inline html)"}`;
17787
18010
  case "custom":
17788
18011
  return `${doc.type}: ${JSON.stringify(doc.data)}`;
17789
18012
  }
@@ -17933,7 +18156,7 @@ import * as path5 from "path";
17933
18156
  function scanHtmlAssets(html) {
17934
18157
  const seen = /* @__PURE__ */ new Set();
17935
18158
  const patterns = [
17936
- /<(?:img|video)\b[^>]*?\bsrc=["']([^"']+)["']/g,
18159
+ /<(?:img|video|iframe)\b[^>]*?\bsrc=["']([^"']+)["']/g,
17937
18160
  /<a\b[^>]*?\bclass=["']attachment["'][^>]*?\bhref=["']([^"']+)["']/g,
17938
18161
  /<a\b[^>]*?\bhref=["']([^"']+)["'][^>]*?\bclass=["']attachment["']/g
17939
18162
  ];
@@ -18012,7 +18235,7 @@ function bundleAssets(htmlPath, options = {}) {
18012
18235
  function replaceAssetRef(html, original, replacement) {
18013
18236
  const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18014
18237
  const srcPattern = new RegExp(
18015
- `(<(?:img|video)\\b[^>]*?\\bsrc=["'])${escaped}(["'])`,
18238
+ `(<(?:img|video|iframe)\\b[^>]*?\\bsrc=["'])${escaped}(["'])`,
18016
18239
  "g"
18017
18240
  );
18018
18241
  html = html.replace(srcPattern, `$1${replacement}$2`);
@@ -18400,6 +18623,21 @@ ${tc.errorStack}` : "");
18400
18623
  ])
18401
18624
  );
18402
18625
  break;
18626
+ case "html":
18627
+ if (entry.url !== void 0 || entry.path !== void 0) {
18628
+ const target = entry.url ?? entry.path ?? "";
18629
+ content.push(
18630
+ paragraph([
18631
+ text(entry.title ?? "Embedded HTML", strong()),
18632
+ text(": "),
18633
+ link(target, target)
18634
+ ])
18635
+ );
18636
+ break;
18637
+ }
18638
+ content.push(paragraph([text(entry.title ?? "Embedded HTML", strong())]));
18639
+ content.push(codeBlock(entry.content ?? "", "html"));
18640
+ break;
18403
18641
  case "custom":
18404
18642
  content.push(paragraph([text(`[${entry.type}]`, strong())]));
18405
18643
  content.push(codeBlock(JSON.stringify(entry.data ?? null, null, 2), "json"));
@@ -20985,6 +21223,9 @@ var ReportGenerator = class {
20985
21223
  searchable: options.html?.searchable ?? true,
20986
21224
  startCollapsed: options.html?.startCollapsed ?? false,
20987
21225
  embedScreenshots: options.html?.embedScreenshots ?? true,
21226
+ // Under "copy" asset mode local html files become hashed assets with
21227
+ // an iframe src instead of being inlined into the report.
21228
+ embedHtmlFiles: options.html?.embedHtmlFiles ?? (options.assetMode ?? "none") !== "copy",
20988
21229
  syntaxHighlighting: options.html?.syntaxHighlighting ?? true,
20989
21230
  mermaidEnabled: options.html?.mermaidEnabled ?? true,
20990
21231
  markdownEnabled: options.html?.markdownEnabled ?? true,
@@ -21160,6 +21401,7 @@ var ReportGenerator = class {
21160
21401
  searchable: this.options.html.searchable,
21161
21402
  startCollapsed: this.options.html.startCollapsed,
21162
21403
  embedScreenshots: this.options.html.embedScreenshots,
21404
+ embedHtmlFiles: this.options.html.embedHtmlFiles,
21163
21405
  syntaxHighlighting: this.options.html.syntaxHighlighting,
21164
21406
  mermaidEnabled: this.options.html.mermaidEnabled,
21165
21407
  markdownEnabled: this.options.html.markdownEnabled,
@@ -21922,7 +22164,7 @@ function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets"
21922
22164
  const visit = (entries) => {
21923
22165
  for (const entry of entries ?? []) {
21924
22166
  const e = entry;
21925
- if (e.kind === "screenshot" || e.kind === "video") {
22167
+ if (e.kind === "screenshot" || e.kind === "video" || e.kind === "html") {
21926
22168
  if (typeof e.path === "string" && !isRemote(e.path) && fs13.existsSync(e.path)) {
21927
22169
  e.path = bundle(e.path);
21928
22170
  }