@test-station/render-html 0.1.7 → 0.2.10

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +142 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@test-station/render-html",
3
- "version": "0.1.7",
3
+ "version": "0.2.10",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/index.js CHANGED
@@ -12,6 +12,7 @@ export function renderHtmlReport(report, options = {}) {
12
12
  const generatedAt = typeof report?.generatedAt === 'string' ? report.generatedAt : 'unknown';
13
13
  const schemaVersion = report?.schemaVersion || '1';
14
14
  const projectName = report?.meta?.projectName || title;
15
+ const policySummary = summary?.policy || {};
15
16
  const moduleFilterOptions = Array.isArray(summary?.filterOptions?.modules) ? summary.filterOptions.modules : dedupe(modules.map((entry) => entry.module));
16
17
  const packageFilterOptions = Array.isArray(summary?.filterOptions?.packages) ? summary.filterOptions.packages : dedupe(packages.map((entry) => entry.name));
17
18
  const frameworkFilterOptions = Array.isArray(summary?.filterOptions?.frameworks)
@@ -479,6 +480,30 @@ export function renderHtmlReport(report, options = {}) {
479
480
  .status-pill.pass { background: color-mix(in srgb, var(--pass) 18%, transparent); color: var(--pass); }
480
481
  .status-pill.fail { background: color-mix(in srgb, var(--fail) 18%, transparent); color: var(--fail); }
481
482
  .status-pill.skip { background: color-mix(in srgb, var(--skip) 18%, transparent); color: var(--skip); }
483
+ .policy-pill {
484
+ display: inline-flex;
485
+ align-items: center;
486
+ justify-content: center;
487
+ padding: 6px 10px;
488
+ border-radius: 999px;
489
+ font-size: 0.72rem;
490
+ font-weight: 700;
491
+ text-transform: uppercase;
492
+ letter-spacing: 0.08em;
493
+ border: 1px solid rgba(124, 160, 224, 0.14);
494
+ }
495
+ .policy-pill--pass { background: color-mix(in srgb, var(--pass) 16%, transparent); color: var(--pass); }
496
+ .policy-pill--warn { background: color-mix(in srgb, var(--skip) 18%, transparent); color: var(--skip); }
497
+ .policy-pill--fail { background: color-mix(in srgb, var(--fail) 18%, transparent); color: var(--fail); }
498
+ .policy-pill--skip { background: rgba(124, 160, 224, 0.14); color: var(--muted); }
499
+ .module-card__badges,
500
+ .module-section__badges,
501
+ .theme-section__badges {
502
+ display: flex;
503
+ flex-wrap: wrap;
504
+ gap: 8px;
505
+ align-items: center;
506
+ }
482
507
  .test-row__name { min-width: 0; }
483
508
  .test-row__title {
484
509
  display: block;
@@ -620,6 +645,38 @@ export function renderHtmlReport(report, options = {}) {
620
645
  letter-spacing: 0.08em;
621
646
  }
622
647
  .coverage-table code { font-size: 0.8rem; color: #d7e5ff; }
648
+ .policy-block {
649
+ margin: 0 0 16px;
650
+ padding: 14px;
651
+ border-radius: 16px;
652
+ border: 1px solid rgba(124, 160, 224, 0.14);
653
+ background: rgba(8, 16, 29, 0.68);
654
+ }
655
+ .policy-block__header {
656
+ display: flex;
657
+ justify-content: space-between;
658
+ gap: 12px;
659
+ align-items: center;
660
+ margin-bottom: 10px;
661
+ }
662
+ .policy-block__title {
663
+ margin: 0;
664
+ font-size: 0.82rem;
665
+ text-transform: uppercase;
666
+ letter-spacing: 0.08em;
667
+ color: var(--muted);
668
+ }
669
+ .policy-block__meta {
670
+ font-size: 0.85rem;
671
+ color: var(--muted);
672
+ word-break: break-word;
673
+ }
674
+ .policy-block__list {
675
+ margin: 10px 0 0;
676
+ padding-left: 18px;
677
+ display: grid;
678
+ gap: 8px;
679
+ }
623
680
  @media (max-width: 920px) {
624
681
  main { width: min(100vw - 24px, 1400px); }
625
682
  .toolbar { align-items: flex-start; }
@@ -642,6 +699,9 @@ export function renderHtmlReport(report, options = {}) {
642
699
  ${renderSummaryCard('Passed', summary.passedTests || 0)}
643
700
  ${renderSummaryCard('Failed', summary.failedTests || 0)}
644
701
  ${renderSummaryCard('Skipped', summary.skippedTests || 0)}
702
+ ${policySummary.failedThresholds > 0 ? renderSummaryCard('Threshold Failures', policySummary.failedThresholds) : ''}
703
+ ${policySummary.warningThresholds > 0 ? renderSummaryCard('Threshold Warnings', policySummary.warningThresholds) : ''}
704
+ ${policySummary.diagnosticsSuites > 0 ? renderSummaryCard('Diagnostic Reruns', policySummary.diagnosticsSuites) : ''}
645
705
  ${renderSummaryCard('Line Coverage', summary.coverage?.lines ? `${summary.coverage.lines.pct.toFixed(2)}%` : 'n/a')}
646
706
  ${renderSummaryCard('Branch Coverage', summary.coverage?.branches ? `${summary.coverage.branches.pct.toFixed(2)}%` : 'n/a')}
647
707
  ${renderSummaryCard('Function Coverage', summary.coverage?.functions ? `${summary.coverage.functions.pct.toFixed(2)}%` : 'n/a')}
@@ -916,6 +976,67 @@ function renderOwnerPill(owner) {
916
976
  return `<span class="owner-pill">Owner: ${escapeHtml(owner)}</span>`;
917
977
  }
918
978
 
979
+ function renderThresholdPill(threshold) {
980
+ if (!threshold?.configured) {
981
+ return '';
982
+ }
983
+ const statusClass = threshold.status === 'failed'
984
+ ? 'fail'
985
+ : (threshold.status === 'warn' ? 'warn' : (threshold.status === 'skipped' ? 'skip' : 'pass'));
986
+ const label = threshold.status === 'failed'
987
+ ? 'Threshold failed'
988
+ : (threshold.status === 'warn' ? 'Threshold warning' : (threshold.status === 'skipped' ? 'Threshold skipped' : 'Threshold met'));
989
+ return `<span class="policy-pill policy-pill--${statusClass}">${escapeHtml(label)}</span>`;
990
+ }
991
+
992
+ function renderThresholdBlock(threshold) {
993
+ if (!threshold?.configured) {
994
+ return '';
995
+ }
996
+ const items = (Array.isArray(threshold.metrics) ? threshold.metrics : [])
997
+ .map((metric) => {
998
+ const actual = Number.isFinite(metric.actualPct) ? `${metric.actualPct.toFixed(2)}%` : 'n/a';
999
+ const annotation = metric.passed
1000
+ ? ''
1001
+ : (Number.isFinite(metric.actualPct) ? ' <strong>(below threshold)</strong>' : ' <strong>(not evaluated)</strong>');
1002
+ return `<li>${escapeHtml(capitalize(metric.metric))} ${escapeHtml(actual)} / minimum ${escapeHtml(metric.minPct.toFixed(2))}%${annotation}</li>`;
1003
+ })
1004
+ .join('');
1005
+ return `
1006
+ <section class="policy-block">
1007
+ <div class="policy-block__header">
1008
+ <h4 class="policy-block__title">Coverage Policy</h4>
1009
+ ${renderThresholdPill(threshold)}
1010
+ </div>
1011
+ <div class="policy-block__meta">${escapeHtml(threshold.reason || `Enforcement: ${threshold.enforcement}`)}</div>
1012
+ <ul class="policy-block__list">
1013
+ ${items}
1014
+ </ul>
1015
+ </section>
1016
+ `;
1017
+ }
1018
+
1019
+ function renderDiagnosticsBlock(diagnostics) {
1020
+ if (!diagnostics || typeof diagnostics !== 'object') {
1021
+ return '';
1022
+ }
1023
+ const meta = [
1024
+ formatDuration(diagnostics.durationMs || 0),
1025
+ diagnostics.command || '',
1026
+ ].filter(Boolean).join(' • ');
1027
+ const timeoutNote = diagnostics.timedOut ? '<div class="policy-block__meta">The diagnostics rerun timed out before completion.</div>' : '';
1028
+ return `
1029
+ <section class="policy-block">
1030
+ <div class="policy-block__header">
1031
+ <h4 class="policy-block__title">${escapeHtml(diagnostics.label || 'Diagnostics')}</h4>
1032
+ ${renderStatusPill(diagnostics.status || 'skipped')}
1033
+ </div>
1034
+ <div class="policy-block__meta">${escapeHtml(meta || 'No diagnostics metadata recorded.')}</div>
1035
+ ${timeoutNote}
1036
+ </section>
1037
+ `;
1038
+ }
1039
+
919
1040
  function renderFilterAttributes({ nodeType, moduleNames = [], packageNames = [], frameworks = [], hasFailures = false, lineCoverage = null }) {
920
1041
  const attributes = [
921
1042
  ['data-filter-node', nodeType || 'node'],
@@ -954,7 +1075,10 @@ function renderModuleCard(moduleEntry) {
954
1075
  </div>
955
1076
  ${renderStatusPill(status)}
956
1077
  </div>
957
- ${renderOwnerPill(moduleEntry.owner)}
1078
+ <div class="module-card__badges">
1079
+ ${renderOwnerPill(moduleEntry.owner)}
1080
+ ${renderThresholdPill(moduleEntry.threshold)}
1081
+ </div>
958
1082
  <div class="module-card__coverage">
959
1083
  ${renderCoverageMiniMetric('Lines', moduleEntry.coverage?.lines)}
960
1084
  ${renderCoverageMiniMetric('Branches', moduleEntry.coverage?.branches)}
@@ -1020,8 +1144,12 @@ function renderModuleSection(moduleEntry, rootDir) {
1020
1144
  <div class="module-section__meta">${escapeHtml(formatSummary(moduleEntry.summary))} • ${escapeHtml(formatDuration(moduleEntry.durationMs || 0))} • ${escapeHtml(`${moduleEntry.packageCount || 0} package${moduleEntry.packageCount === 1 ? '' : 's'}`)}</div>
1021
1145
  </summary>
1022
1146
  <div class="module-section__body">
1023
- ${renderOwnerPill(moduleEntry.owner)}
1147
+ <div class="module-section__badges">
1148
+ ${renderOwnerPill(moduleEntry.owner)}
1149
+ ${renderThresholdPill(moduleEntry.threshold)}
1150
+ </div>
1024
1151
  <div class="module-section__packages">Dominant packages: ${escapeHtml((moduleEntry.dominantPackages || []).join(', ') || 'n/a')}</div>
1152
+ ${renderThresholdBlock(moduleEntry.threshold)}
1025
1153
  ${renderCoverageBlock(moduleEntry.coverage, rootDir)}
1026
1154
  <div class="theme-list">${themeMarkup}</div>
1027
1155
  </div>
@@ -1085,7 +1213,11 @@ function renderThemeSection(moduleEntry, themeEntry, rootDir) {
1085
1213
  <div class="theme-section__meta">${escapeHtml(formatSummary(themeEntry.summary))} • ${escapeHtml(formatDuration(themeEntry.durationMs || 0))} • ${escapeHtml(`${themeEntry.packageCount || 0} package${themeEntry.packageCount === 1 ? '' : 's'}`)}</div>
1086
1214
  </summary>
1087
1215
  <div class="theme-section__body">
1088
- ${renderOwnerPill(themeEntry.owner)}
1216
+ <div class="theme-section__badges">
1217
+ ${renderOwnerPill(themeEntry.owner)}
1218
+ ${renderThresholdPill(themeEntry.threshold)}
1219
+ </div>
1220
+ ${renderThresholdBlock(themeEntry.threshold)}
1089
1221
  ${renderCoverageBlock(themeEntry.coverage, rootDir)}
1090
1222
  <div class="package-list">${packageMarkup}</div>
1091
1223
  </div>
@@ -1130,6 +1262,7 @@ function renderSuite(suite, rootDir, filterContext = {}) {
1130
1262
  ? `<ul class="suite__warnings">${warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join('')}</ul>`
1131
1263
  : '';
1132
1264
  const coverageMarkup = renderCoverageBlock(suite.coverage, rootDir);
1265
+ const diagnosticsMarkup = renderDiagnosticsBlock(suite.diagnostics);
1133
1266
  const artifactMarkup = rawArtifacts.length > 0 ? renderRawArtifactBlock(rawArtifacts) : '';
1134
1267
  const testsMarkup = tests.length === 0
1135
1268
  ? '<div class="test-row"><div class="test-row__summary"><span class="status-pill skip">skip</span><div class="test-row__name"><span class="test-row__title">No test results emitted</span></div></div></div>'
@@ -1154,6 +1287,7 @@ function renderSuite(suite, rootDir, filterContext = {}) {
1154
1287
  <div class="suite__body">
1155
1288
  ${warningMarkup}
1156
1289
  ${coverageMarkup}
1290
+ ${diagnosticsMarkup}
1157
1291
  ${artifactMarkup}
1158
1292
  <div class="test-list">${testsMarkup}</div>
1159
1293
  </div>
@@ -1187,6 +1321,11 @@ function renderRawArtifactItem(artifact) {
1187
1321
  `;
1188
1322
  }
1189
1323
 
1324
+ function capitalize(value) {
1325
+ const normalized = String(value || '');
1326
+ return normalized.length > 0 ? normalized[0].toUpperCase() + normalized.slice(1) : normalized;
1327
+ }
1328
+
1190
1329
  function renderTest(suite, test, rootDir, filterContext = {}) {
1191
1330
  const statusClass = test.status === 'failed' ? 'fail' : test.status === 'skipped' ? 'skip' : 'pass';
1192
1331
  const assertions = Array.isArray(test.assertions) && test.assertions.length > 0