@ng-prism/plugin-coverage 22.6.0 → 22.7.0-beta.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ng-prism/plugin-coverage
2
2
 
3
- Test coverage plugin for [@ng-prism/core](https://github.com/ng-prism/ng-prism). Reads Istanbul/v8 coverage reports at build time and displays per-component test coverage metrics with a score circle in the tab bar.
3
+ Test coverage plugin for [@ng-prism/core](https://github.com/ng-prism/ng-prism). Reads Istanbul/v8 coverage reports at build time and displays per-component test coverage metrics in a panel and a library-wide figure in the header.
4
4
 
5
5
  > **Full documentation:** [ng-prism Docs: Coverage Plugin](https://ng-prism.github.io/ng-prism/#/plugins/coverage)
6
6
 
@@ -36,9 +36,9 @@ export default defineConfig({
36
36
  ## What It Does
37
37
 
38
38
  - Adds a **Coverage** panel to the addon tab bar
39
- - Shows a **score circle** with the average coverage percentage (like the A11y panel)
40
- - Displays 4 metric bars: Statements, Branches, Functions, Lines
41
- - Color-coded thresholds: green (>= 80%), yellow (>= 50%), red (< 50%)
39
+ - Shows the four metrics, Statements, Branches, Functions and Lines, as cards with a bar and a tick at the threshold
40
+ - Grades each against its threshold: ok at the target, warn from 75% of it, danger below, the same rule the header pill, the component head and the sidebar use
41
+ - Shows a **Coverage** pill in the header with the library-wide figure
42
42
  - Works with any test runner that outputs Istanbul-compatible coverage (Jest, Karma, Vitest, nyc, c8)
43
43
 
44
44
  ## Options
@@ -67,13 +67,13 @@ npx nx test my-lib --coverage
67
67
  ng run my-lib:prism
68
68
  ```
69
69
 
70
- Each component's Coverage tab shows its statement, branch, function, and line coverage with a score circle in the tab bar.
70
+ Each component's Coverage tab shows its statement, branch, function and line coverage. The tab carries no badge (since 22.7); the figures are in the panel, the component head and the header.
71
71
 
72
72
  ## How It Works
73
73
 
74
74
  **Build time:** The `onComponentScanned` hook reads `coverage-summary.json` (cached per build run, with mtime-based invalidation) and matches entries to components by file path. Coverage data is injected into `showcaseConfig.meta.coverage`.
75
75
 
76
- **Runtime:** The `CoveragePanelComponent` reads the metadata and renders the metric bars. The score circle is rendered in `PrismPanelHostComponent` using the same `A11yScoreComponent`. The panel component is lazy-loaded via `loadComponent`.
76
+ **Runtime:** The `CoveragePanelComponent` reads the metadata and renders the metric cards with `PrismMetricCardComponent` from `@ng-prism/core`. The panel component is lazy-loaded via `loadComponent`.
77
77
 
78
78
  ## License
79
79
 
@@ -1,12 +1,3 @@
1
- import type { NavigationDecorationDefinition, PanelBadge, RuntimeComponent } from '@ng-prism/core/plugin';
2
- /**
3
- * The score on the panel's own tab.
4
- *
5
- * Declared by the plugin because the core's built-in badge uses a fixed 90/70
6
- * scale and could show red where the navigation marker, which uses the
7
- * configured thresholds, shows amber. The build-time `summary` wins when
8
- * present; otherwise the badge is derived from `thresholds`.
9
- */
10
- export declare function coverageBadge(component: RuntimeComponent): PanelBadge | null;
1
+ import type { NavigationDecorationDefinition } from '@ng-prism/core/plugin';
11
2
  export declare const COVERAGE_NAVIGATION_DECORATION: NavigationDecorationDefinition;
12
3
  //# sourceMappingURL=coverage-contributions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"coverage-contributions.d.ts","sourceRoot":"","sources":["../src/coverage-contributions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAa1G;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,gBAAgB,GAAG,UAAU,GAAG,IAAI,CAQ5E;AAED,eAAO,MAAM,8BAA8B,EAAE,8BAW5C,CAAC"}
1
+ {"version":3,"file":"coverage-contributions.d.ts","sourceRoot":"","sources":["../src/coverage-contributions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAoB,MAAM,uBAAuB,CAAC;AAY9F,eAAO,MAAM,8BAA8B,EAAE,8BAW5C,CAAC"}
@@ -1,4 +1,3 @@
1
- import { deriveCoverageSummary } from './coverage-summary.js';
2
1
  /**
3
2
  * The runtime contributions both entry points declare. `coverage-plugin.ts`
4
3
  * carries the build-time hook and must never be reachable from a browser
@@ -7,21 +6,6 @@ import { deriveCoverageSummary } from './coverage-summary.js';
7
6
  function componentMeta(component) {
8
7
  return component.meta?.showcaseConfig?.meta?.['coverage'] ?? null;
9
8
  }
10
- /**
11
- * The score on the panel's own tab.
12
- *
13
- * Declared by the plugin because the core's built-in badge uses a fixed 90/70
14
- * scale and could show red where the navigation marker, which uses the
15
- * configured thresholds, shows amber. The build-time `summary` wins when
16
- * present; otherwise the badge is derived from `thresholds`.
17
- */
18
- export function coverageBadge(component) {
19
- const meta = componentMeta(component);
20
- if (!meta?.found)
21
- return null;
22
- const variant = meta.summary?.variant ?? (meta.thresholds ? deriveCoverageSummary(meta.score, meta.thresholds).variant : 'default');
23
- return { text: String(meta.score), variant };
24
- }
25
9
  export const COVERAGE_NAVIGATION_DECORATION = {
26
10
  id: 'coverage',
27
11
  icon: 'shield-check',
@@ -31,7 +31,7 @@ export class CoverageHeaderBadgeComponent {
31
31
  }, /* @ts-ignore */
32
32
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
33
33
  static ɵfac = function CoverageHeaderBadgeComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || CoverageHeaderBadgeComponent)(); };
34
- static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: CoverageHeaderBadgeComponent, selectors: [["prism-coverage-header-badge"]], decls: 1, vars: 1, consts: [["icon", "shield-check", "label", "Library coverage", 3, "value", "variant", "title"]], template: function CoverageHeaderBadgeComponent_Template(rf, ctx) { if (rf & 1) {
34
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: CoverageHeaderBadgeComponent, selectors: [["prism-coverage-header-badge"]], decls: 1, vars: 1, consts: [["icon", "shield-check", "label", "Library coverage", "shortLabel", "Coverage", 3, "value", "variant", "title"]], template: function CoverageHeaderBadgeComponent_Template(rf, ctx) { if (rf & 1) {
35
35
  i0.ɵɵconditionalCreate(0, CoverageHeaderBadgeComponent_Conditional_0_Template, 1, 3, "prism-metric-badge", 0);
36
36
  } if (rf & 2) {
37
37
  let tmp_0_0;
@@ -40,6 +40,6 @@ export class CoverageHeaderBadgeComponent {
40
40
  }
41
41
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(CoverageHeaderBadgeComponent, [{
42
42
  type: Component,
43
- args: [{ selector: 'prism-coverage-header-badge', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [PrismMetricBadgeComponent], template: "@if (data(); as d) {\n <prism-metric-badge\n icon=\"shield-check\"\n label=\"Library coverage\"\n [value]=\"d.score + '%'\"\n [variant]=\"d.variant\"\n [title]=\"d.title\" />\n}\n" }]
43
+ args: [{ selector: 'prism-coverage-header-badge', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [PrismMetricBadgeComponent], template: "@if (data(); as d) {\n <prism-metric-badge\n icon=\"shield-check\"\n label=\"Library coverage\"\n shortLabel=\"Coverage\"\n [value]=\"d.score + '%'\"\n [variant]=\"d.variant\"\n [title]=\"d.title\" />\n}\n" }]
44
44
  }], null, null); })();
45
45
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(CoverageHeaderBadgeComponent, { className: "CoverageHeaderBadgeComponent", filePath: "coverage-header-badge.component.ts", lineNumber: 15 }); })();
@@ -0,0 +1,4 @@
1
+ import { type StatusTone } from '@ng-prism/core/plugin';
2
+ /** One coverage metric graded against its own threshold, by the rule every key figure uses. */
3
+ export declare function coverageMetricTone(pct: number, threshold: number): StatusTone;
4
+ //# sourceMappingURL=coverage-metric-tone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage-metric-tone.d.ts","sourceRoot":"","sources":["../src/coverage-metric-tone.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,UAAU,EAAuB,MAAM,uBAAuB,CAAC;AAExF,+FAA+F;AAC/F,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,CAE7E"}
@@ -0,0 +1,5 @@
1
+ import { statusFor, thresholdFromTarget } from '@ng-prism/core/plugin';
2
+ /** One coverage metric graded against its own threshold, by the rule every key figure uses. */
3
+ export function coverageMetricTone(pct, threshold) {
4
+ return statusFor(pct, thresholdFromTarget(threshold));
5
+ }
@@ -3,12 +3,17 @@ import * as i0 from "@angular/core";
3
3
  export declare class CoveragePanelComponent {
4
4
  readonly activeComponent: import("@angular/core").InputSignal<unknown>;
5
5
  protected readonly coverage: import("@angular/core").Signal<CoverageData | null>;
6
+ protected readonly thresholds: import("@angular/core").Signal<CoverageThresholds>;
7
+ /** One card per metric, graded against that metric's own threshold, which also marks the bar. */
6
8
  protected readonly summaryStats: import("@angular/core").Signal<{
7
- label: string;
9
+ label: "Lines" | "Branches" | "Functions" | "Statements";
8
10
  pct: number;
11
+ threshold: number;
12
+ tone: import("@ng-prism/core").BadgeTone;
13
+ statusLabel: "OK" | "Warning" | "Critical";
14
+ title: string;
9
15
  }[]>;
10
16
  protected readonly files: import("@angular/core").Signal<FileCoverageDetail[]>;
11
- protected readonly thresholds: import("@angular/core").Signal<CoverageThresholds>;
12
17
  protected fileName(path: string): string;
13
18
  static ɵfac: i0.ɵɵFactoryDeclaration<CoveragePanelComponent, never>;
14
19
  static ɵcmp: i0.ɵɵComponentDeclaration<CoveragePanelComponent, "prism-coverage-panel", never, { "activeComponent": { "alias": "activeComponent"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
@@ -1 +1 @@
1
- {"version":3,"file":"coverage-panel.component.d.ts","sourceRoot":"","sources":["../src/coverage-panel.component.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;;AAShG,qBAOa,sBAAsB;IAC/B,QAAQ,CAAC,eAAe,+CAAwB;IAEhD,SAAS,CAAC,QAAQ,CAAC,QAAQ,sDAIxB;IAEH,SAAS,CAAC,QAAQ,CAAC,YAAY;;;SAU5B;IAEH,SAAS,CAAC,QAAQ,CAAC,KAAK,uDAErB;IAEH,SAAS,CAAC,QAAQ,CAAC,UAAU,qDAE1B;IAEH,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;yCA7B/B,sBAAsB;2CAAtB,sBAAsB;CAkClC"}
1
+ {"version":3,"file":"coverage-panel.component.d.ts","sourceRoot":"","sources":["../src/coverage-panel.component.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;;AAShG,qBAQa,sBAAsB;IAC/B,QAAQ,CAAC,eAAe,+CAAwB;IAEhD,SAAS,CAAC,QAAQ,CAAC,QAAQ,sDAIxB;IAEH,SAAS,CAAC,QAAQ,CAAC,UAAU,qDAA0F;IAEvH,iGAAiG;IACjG,SAAS,CAAC,QAAQ,CAAC,YAAY;;;;;;;SA2B5B;IAEH,SAAS,CAAC,QAAQ,CAAC,KAAK,uDAAsE;IAE9F,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;yCA3C/B,sBAAsB;2CAAtB,sBAAsB;CAgDlC"}
@@ -1,44 +1,31 @@
1
1
  import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
+ import { PrismMetricCardComponent, toneForStatus } from '@ng-prism/core';
3
+ import { coverageMetricTone } from './coverage-metric-tone.js';
2
4
  import * as i0 from "@angular/core";
3
5
  const _forTrack0 = ($index, $item) => $item.label;
4
6
  const _forTrack1 = ($index, $item) => $item.path;
5
- function CoveragePanelComponent_Conditional_0_For_3_Template(rf, ctx) { if (rf & 1) {
6
- i0.ɵɵdomElementStart(0, "div", 3)(1, "div", 5);
7
- i0.ɵɵtext(2);
8
- i0.ɵɵdomElementEnd();
9
- i0.ɵɵdomElementStart(3, "div", 6);
10
- i0.ɵɵtext(4);
11
- i0.ɵɵdomElementStart(5, "small");
12
- i0.ɵɵtext(6, "%");
13
- i0.ɵɵdomElementEnd()();
14
- i0.ɵɵdomElementStart(7, "div", 7);
15
- i0.ɵɵdomElement(8, "div", 8);
16
- i0.ɵɵdomElementEnd()();
7
+ function CoveragePanelComponent_Conditional_0_For_2_Template(rf, ctx) { if (rf & 1) {
8
+ i0.ɵɵelement(0, "prism-metric-card", 2);
17
9
  } if (rf & 2) {
18
10
  const stat_r1 = ctx.$implicit;
19
- i0.ɵɵadvance(2);
20
- i0.ɵɵtextInterpolate(stat_r1.label);
21
- i0.ɵɵadvance(2);
22
- i0.ɵɵtextInterpolate(stat_r1.pct);
23
- i0.ɵɵadvance(4);
24
- i0.ɵɵstyleProp("width", stat_r1.pct, "%");
11
+ i0.ɵɵproperty("label", stat_r1.label)("value", "" + stat_r1.pct)("tone", stat_r1.tone)("statusLabel", stat_r1.statusLabel)("fraction", stat_r1.pct / 100)("threshold", stat_r1.threshold / 100)("title", stat_r1.title);
25
12
  } }
26
- function CoveragePanelComponent_Conditional_0_Conditional_4_For_13_Template(rf, ctx) { if (rf & 1) {
27
- i0.ɵɵdomElementStart(0, "div", 10)(1, "span", 11);
13
+ function CoveragePanelComponent_Conditional_0_Conditional_3_For_13_Template(rf, ctx) { if (rf & 1) {
14
+ i0.ɵɵelementStart(0, "div", 5)(1, "span", 6);
28
15
  i0.ɵɵtext(2);
29
- i0.ɵɵdomElementEnd();
30
- i0.ɵɵdomElementStart(3, "span", 12);
16
+ i0.ɵɵelementEnd();
17
+ i0.ɵɵelementStart(3, "span", 7);
31
18
  i0.ɵɵtext(4);
32
- i0.ɵɵdomElementEnd();
33
- i0.ɵɵdomElementStart(5, "span", 12);
19
+ i0.ɵɵelementEnd();
20
+ i0.ɵɵelementStart(5, "span", 7);
34
21
  i0.ɵɵtext(6);
35
- i0.ɵɵdomElementEnd();
36
- i0.ɵɵdomElementStart(7, "span", 12);
22
+ i0.ɵɵelementEnd();
23
+ i0.ɵɵelementStart(7, "span", 7);
37
24
  i0.ɵɵtext(8);
38
- i0.ɵɵdomElementEnd();
39
- i0.ɵɵdomElementStart(9, "span", 12);
25
+ i0.ɵɵelementEnd();
26
+ i0.ɵɵelementStart(9, "span", 7);
40
27
  i0.ɵɵtext(10);
41
- i0.ɵɵdomElementEnd()();
28
+ i0.ɵɵelementEnd()();
42
29
  } if (rf & 2) {
43
30
  const file_r2 = ctx.$implicit;
44
31
  const ctx_r2 = i0.ɵɵnextContext(3);
@@ -61,46 +48,45 @@ function CoveragePanelComponent_Conditional_0_Conditional_4_For_13_Template(rf,
61
48
  i0.ɵɵadvance();
62
49
  i0.ɵɵtextInterpolate1("", file_r2.statements.pct, "%");
63
50
  } }
64
- function CoveragePanelComponent_Conditional_0_Conditional_4_Template(rf, ctx) { if (rf & 1) {
65
- i0.ɵɵdomElementStart(0, "div", 4)(1, "div", 9)(2, "span");
51
+ function CoveragePanelComponent_Conditional_0_Conditional_3_Template(rf, ctx) { if (rf & 1) {
52
+ i0.ɵɵelementStart(0, "div", 3)(1, "div", 4)(2, "span");
66
53
  i0.ɵɵtext(3, "File");
67
- i0.ɵɵdomElementEnd();
68
- i0.ɵɵdomElementStart(4, "span");
54
+ i0.ɵɵelementEnd();
55
+ i0.ɵɵelementStart(4, "span");
69
56
  i0.ɵɵtext(5, "Lines");
70
- i0.ɵɵdomElementEnd();
71
- i0.ɵɵdomElementStart(6, "span");
57
+ i0.ɵɵelementEnd();
58
+ i0.ɵɵelementStart(6, "span");
72
59
  i0.ɵɵtext(7, "Branch");
73
- i0.ɵɵdomElementEnd();
74
- i0.ɵɵdomElementStart(8, "span");
60
+ i0.ɵɵelementEnd();
61
+ i0.ɵɵelementStart(8, "span");
75
62
  i0.ɵɵtext(9, "Func");
76
- i0.ɵɵdomElementEnd();
77
- i0.ɵɵdomElementStart(10, "span");
63
+ i0.ɵɵelementEnd();
64
+ i0.ɵɵelementStart(10, "span");
78
65
  i0.ɵɵtext(11, "Stmt");
79
- i0.ɵɵdomElementEnd()();
80
- i0.ɵɵrepeaterCreate(12, CoveragePanelComponent_Conditional_0_Conditional_4_For_13_Template, 11, 13, "div", 10, _forTrack1);
81
- i0.ɵɵdomElementEnd();
66
+ i0.ɵɵelementEnd()();
67
+ i0.ɵɵrepeaterCreate(12, CoveragePanelComponent_Conditional_0_Conditional_3_For_13_Template, 11, 13, "div", 5, _forTrack1);
68
+ i0.ɵɵelementEnd();
82
69
  } if (rf & 2) {
83
70
  const ctx_r2 = i0.ɵɵnextContext(2);
84
71
  i0.ɵɵadvance(12);
85
72
  i0.ɵɵrepeater(ctx_r2.files());
86
73
  } }
87
74
  function CoveragePanelComponent_Conditional_0_Template(rf, ctx) { if (rf & 1) {
88
- i0.ɵɵdomElementStart(0, "div", 0)(1, "div", 2);
89
- i0.ɵɵrepeaterCreate(2, CoveragePanelComponent_Conditional_0_For_3_Template, 9, 4, "div", 3, _forTrack0);
90
- i0.ɵɵdomElementEnd();
91
- i0.ɵɵconditionalCreate(4, CoveragePanelComponent_Conditional_0_Conditional_4_Template, 14, 0, "div", 4);
92
- i0.ɵɵdomElementEnd();
75
+ i0.ɵɵelementStart(0, "div", 1);
76
+ i0.ɵɵrepeaterCreate(1, CoveragePanelComponent_Conditional_0_For_2_Template, 1, 7, "prism-metric-card", 2, _forTrack0);
77
+ i0.ɵɵelementEnd();
78
+ i0.ɵɵconditionalCreate(3, CoveragePanelComponent_Conditional_0_Conditional_3_Template, 14, 0, "div", 3);
93
79
  } if (rf & 2) {
94
80
  const ctx_r2 = i0.ɵɵnextContext();
95
- i0.ɵɵadvance(2);
81
+ i0.ɵɵadvance();
96
82
  i0.ɵɵrepeater(ctx_r2.summaryStats());
97
83
  i0.ɵɵadvance(2);
98
- i0.ɵɵconditional(ctx_r2.files().length ? 4 : -1);
84
+ i0.ɵɵconditional(ctx_r2.files().length ? 3 : -1);
99
85
  } }
100
86
  function CoveragePanelComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
101
- i0.ɵɵdomElementStart(0, "div", 1)(1, "p");
87
+ i0.ɵɵelementStart(0, "div", 0)(1, "p");
102
88
  i0.ɵɵtext(2, "No coverage data. Run tests with coverage enabled.");
103
- i0.ɵɵdomElementEnd()();
89
+ i0.ɵɵelementEnd()();
104
90
  } }
105
91
  const FALLBACK_THRESHOLDS = {
106
92
  lines: 80,
@@ -116,39 +102,49 @@ export class CoveragePanelComponent {
116
102
  return comp?.meta?.showcaseConfig?.meta?.['coverage'] ?? null;
117
103
  }, /* @ts-ignore */
118
104
  ...(ngDevMode ? [{ debugName: "coverage" }] : /* istanbul ignore next */ []));
105
+ thresholds = computed(() => this.coverage()?.thresholds ?? FALLBACK_THRESHOLDS, /* @ts-ignore */
106
+ ...(ngDevMode ? [{ debugName: "thresholds" }] : /* istanbul ignore next */ []));
107
+ /** One card per metric, graded against that metric's own threshold, which also marks the bar. */
119
108
  summaryStats = computed(() => {
120
109
  const c = this.coverage();
110
+ const t = this.thresholds();
121
111
  if (!c?.found)
122
112
  return [];
113
+ const LABEL = { ok: 'OK', warn: 'Warning', danger: 'Critical' };
123
114
  return [
124
- { label: 'Lines', pct: c.lines.pct },
125
- { label: 'Branches', pct: c.branches.pct },
126
- { label: 'Functions', pct: c.functions.pct },
127
- { label: 'Statements', pct: c.statements.pct }
128
- ];
115
+ ['Lines', 'lines'],
116
+ ['Branches', 'branches'],
117
+ ['Functions', 'functions'],
118
+ ['Statements', 'statements']
119
+ ].map(([label, key]) => {
120
+ const detail = c[key];
121
+ const status = coverageMetricTone(detail.pct, t[key]);
122
+ return {
123
+ label,
124
+ pct: detail.pct,
125
+ threshold: t[key],
126
+ tone: toneForStatus(status),
127
+ statusLabel: LABEL[status],
128
+ title: `${label}: ${detail.covered} of ${detail.total} covered · target ${t[key]}%`
129
+ };
130
+ });
129
131
  }, /* @ts-ignore */
130
132
  ...(ngDevMode ? [{ debugName: "summaryStats" }] : /* istanbul ignore next */ []));
131
- files = computed(() => {
132
- return this.coverage()?.files ?? [];
133
- }, /* @ts-ignore */
133
+ files = computed(() => this.coverage()?.files ?? [], /* @ts-ignore */
134
134
  ...(ngDevMode ? [{ debugName: "files" }] : /* istanbul ignore next */ []));
135
- thresholds = computed(() => {
136
- return this.coverage()?.thresholds ?? FALLBACK_THRESHOLDS;
137
- }, /* @ts-ignore */
138
- ...(ngDevMode ? [{ debugName: "thresholds" }] : /* istanbul ignore next */ []));
139
135
  fileName(path) {
140
136
  const parts = path.split('/');
141
137
  return parts[parts.length - 1];
142
138
  }
143
139
  static ɵfac = function CoveragePanelComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || CoveragePanelComponent)(); };
144
- static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: CoveragePanelComponent, selectors: [["prism-coverage-panel"]], inputs: { activeComponent: [1, "activeComponent"] }, decls: 2, vars: 1, consts: [[1, "cov-body"], [1, "cov-empty"], [1, "cov-summary"], [1, "cov-stat"], [1, "cov-files"], [1, "cov-stat-k"], [1, "cov-stat-v"], [1, "cov-bar"], [1, "cov-bar-fill"], [1, "cov-file", "cov-file--header"], [1, "cov-file"], [1, "cov-file-name"], [1, "cov-file-val"]], template: function CoveragePanelComponent_Template(rf, ctx) { if (rf & 1) {
145
- i0.ɵɵconditionalCreate(0, CoveragePanelComponent_Conditional_0_Template, 5, 1, "div", 0)(1, CoveragePanelComponent_Conditional_1_Template, 3, 0, "div", 1);
140
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: CoveragePanelComponent, selectors: [["prism-coverage-panel"]], inputs: { activeComponent: [1, "activeComponent"] }, decls: 2, vars: 1, consts: [[1, "cov-empty"], [1, "cov-body"], ["unit", "%", 3, "label", "value", "tone", "statusLabel", "fraction", "threshold", "title"], [1, "cov-files"], [1, "cov-file", "cov-file--header"], [1, "cov-file"], [1, "cov-file-name"], [1, "cov-file-val"]], template: function CoveragePanelComponent_Template(rf, ctx) { if (rf & 1) {
141
+ i0.ɵɵconditionalCreate(0, CoveragePanelComponent_Conditional_0_Template, 4, 1)(1, CoveragePanelComponent_Conditional_1_Template, 3, 0, "div", 0);
146
142
  } if (rf & 2) {
147
143
  i0.ɵɵconditional(ctx.coverage()?.found ? 0 : 1);
148
- } }, styles: ["[_nghost-%COMP%] {\n display: block;\n height: 100%;\n overflow: auto;\n}\n\n.cov-body[_ngcontent-%COMP%] {\n padding: 20px 24px;\n}\n\n.cov-summary[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 12px;\n margin-bottom: 18px;\n}\n\n.cov-stat[_ngcontent-%COMP%] {\n padding: 12px 14px;\n background: var(--%NS%prism-bg-surface);\n border: 1px solid var(--%NS%prism-border);\n border-radius: 8px;\n}\n\n.cov-stat-k[_ngcontent-%COMP%] {\n margin-bottom: 6px;\n font-size: 10.5px;\n font-weight: 700;\n color: var(--%NS%prism-text-ghost);\n text-transform: uppercase;\n letter-spacing: 0.08em;\n}\n\n.cov-stat-v[_ngcontent-%COMP%] {\n font-family: var(--%NS%font-mono);\n font-size: 22px;\n font-weight: 700;\n color: var(--%NS%prism-text);\n letter-spacing: -0.02em;\n}\n\n.cov-stat-v[_ngcontent-%COMP%] small[_ngcontent-%COMP%] {\n font-size: 13px;\n font-weight: 500;\n color: var(--%NS%prism-text-muted);\n}\n\n.cov-bar[_ngcontent-%COMP%] {\n height: 4px;\n margin-top: 8px;\n overflow: hidden;\n background: var(--%NS%prism-input-bg);\n border-radius: 2px;\n}\n\n.cov-bar-fill[_ngcontent-%COMP%] {\n height: 100%;\n background: linear-gradient(90deg, var(--%NS%prism-primary-from), var(--%NS%prism-primary-to));\n border-radius: 2px;\n transition: width 0.6s;\n}\n\n.cov-files[_ngcontent-%COMP%] {\n padding: 12px;\n background: var(--%NS%prism-bg-surface);\n border: 1px solid var(--%NS%prism-border);\n border-radius: 8px;\n}\n\n.cov-file[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: 1fr auto auto auto auto;\n gap: 16px;\n align-items: center;\n padding: 7px 4px;\n font-family: var(--%NS%font-mono);\n font-size: 12px;\n border-bottom: 1px solid var(--%NS%prism-border);\n}\n\n.cov-file[_ngcontent-%COMP%]:last-child {\n border-bottom: 0;\n}\n\n.cov-file--header[_ngcontent-%COMP%] {\n padding-bottom: 8px;\n font-size: 10.5px;\n font-weight: 700;\n color: var(--%NS%prism-text-ghost);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n border-bottom: 1px solid var(--%NS%prism-border-strong);\n}\n\n.cov-file-name[_ngcontent-%COMP%] {\n color: var(--%NS%prism-text-2);\n}\n\n.cov-file-val[_ngcontent-%COMP%] {\n min-width: 54px;\n color: var(--%NS%prism-text);\n text-align: right;\n}\n\n.cov-file-val.bad[_ngcontent-%COMP%] {\n color: var(--%NS%prism-warn);\n}\n\n.cov-empty[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 120px;\n font-size: 13px;\n color: var(--%NS%prism-text-muted);\n text-align: center;\n}"] });
144
+ } }, dependencies: [PrismMetricCardComponent], styles: [".cov-body[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\n gap: 12px;\n padding: 16px 18px;\n}\n\n.cov-files[_ngcontent-%COMP%] {\n padding: 12px;\n margin: 0 18px 16px;\n background: var(--%NS%prism-bg-surface);\n border: 1px solid var(--%NS%prism-border);\n border-radius: 6px;\n}\n\n.cov-file[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: 1fr auto auto auto auto;\n gap: 16px;\n align-items: center;\n padding: 7px 4px;\n font-family: var(--%NS%font-mono);\n font-size: 12px;\n border-bottom: 1px solid var(--%NS%prism-border);\n}\n\n.cov-file[_ngcontent-%COMP%]:last-child {\n border-bottom: 0;\n}\n\n.cov-file--header[_ngcontent-%COMP%] {\n padding-bottom: 8px;\n font-size: 10.5px;\n font-weight: 700;\n color: var(--%NS%prism-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n border-bottom: 1px solid var(--%NS%prism-border);\n}\n\n.cov-file-name[_ngcontent-%COMP%] {\n color: var(--%NS%prism-text-2);\n}\n\n.cov-file-val[_ngcontent-%COMP%] {\n min-width: 54px;\n color: var(--%NS%prism-text);\n text-align: right;\n}\n\n.cov-file-val.bad[_ngcontent-%COMP%] {\n color: var(--%NS%prism-danger);\n}\n\n.cov-empty[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 120px;\n font-size: 13px;\n color: var(--%NS%prism-text-muted);\n text-align: center;\n}"] });
149
145
  }
150
146
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(CoveragePanelComponent, [{
151
147
  type: Component,
152
- args: [{ selector: 'prism-coverage-panel', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (coverage()?.found) {\n <div class=\"cov-body\">\n <div class=\"cov-summary\">\n @for (stat of summaryStats(); track stat.label) {\n <div class=\"cov-stat\">\n <div class=\"cov-stat-k\">{{ stat.label }}</div>\n <div class=\"cov-stat-v\">{{ stat.pct }}<small>%</small></div>\n <div class=\"cov-bar\">\n <div\n class=\"cov-bar-fill\"\n [style.width.%]=\"stat.pct\"></div>\n </div>\n </div>\n }\n </div>\n\n @if (files().length) {\n <div class=\"cov-files\">\n <div class=\"cov-file cov-file--header\">\n <span>File</span>\n <span>Lines</span>\n <span>Branch</span>\n <span>Func</span>\n <span>Stmt</span>\n </div>\n @for (file of files(); track file.path) {\n <div class=\"cov-file\">\n <span class=\"cov-file-name\">{{ fileName(file.path) }}</span>\n <span\n class=\"cov-file-val\"\n [class.bad]=\"file.lines.pct < thresholds().lines\"\n >{{ file.lines.pct }}%</span\n >\n <span\n class=\"cov-file-val\"\n [class.bad]=\"file.branches.pct < thresholds().branches\"\n >{{ file.branches.pct }}%</span\n >\n <span\n class=\"cov-file-val\"\n [class.bad]=\"file.functions.pct < thresholds().functions\"\n >{{ file.functions.pct }}%</span\n >\n <span\n class=\"cov-file-val\"\n [class.bad]=\"file.statements.pct < thresholds().statements\"\n >{{ file.statements.pct }}%</span\n >\n </div>\n }\n </div>\n }\n </div>\n} @else {\n <div class=\"cov-empty\">\n <p>No coverage data. Run tests with coverage enabled.</p>\n </div>\n}\n", styles: [":host {\n display: block;\n height: 100%;\n overflow: auto;\n}\n\n.cov-body {\n padding: 20px 24px;\n}\n\n.cov-summary {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 12px;\n margin-bottom: 18px;\n}\n\n.cov-stat {\n padding: 12px 14px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-radius: 8px;\n}\n\n.cov-stat-k {\n margin-bottom: 6px;\n font-size: 10.5px;\n font-weight: 700;\n color: var(--prism-text-ghost);\n text-transform: uppercase;\n letter-spacing: 0.08em;\n}\n\n.cov-stat-v {\n font-family: var(--font-mono);\n font-size: 22px;\n font-weight: 700;\n color: var(--prism-text);\n letter-spacing: -0.02em;\n}\n\n.cov-stat-v small {\n font-size: 13px;\n font-weight: 500;\n color: var(--prism-text-muted);\n}\n\n.cov-bar {\n height: 4px;\n margin-top: 8px;\n overflow: hidden;\n background: var(--prism-input-bg);\n border-radius: 2px;\n}\n\n.cov-bar-fill {\n height: 100%;\n background: linear-gradient(90deg, var(--prism-primary-from), var(--prism-primary-to));\n border-radius: 2px;\n transition: width 0.6s;\n}\n\n.cov-files {\n padding: 12px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-radius: 8px;\n}\n\n.cov-file {\n display: grid;\n grid-template-columns: 1fr auto auto auto auto;\n gap: 16px;\n align-items: center;\n padding: 7px 4px;\n font-family: var(--font-mono);\n font-size: 12px;\n border-bottom: 1px solid var(--prism-border);\n}\n\n.cov-file:last-child {\n border-bottom: 0;\n}\n\n.cov-file--header {\n padding-bottom: 8px;\n font-size: 10.5px;\n font-weight: 700;\n color: var(--prism-text-ghost);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n border-bottom: 1px solid var(--prism-border-strong);\n}\n\n.cov-file-name {\n color: var(--prism-text-2);\n}\n\n.cov-file-val {\n min-width: 54px;\n color: var(--prism-text);\n text-align: right;\n}\n\n.cov-file-val.bad {\n color: var(--prism-warn);\n}\n\n.cov-empty {\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 120px;\n font-size: 13px;\n color: var(--prism-text-muted);\n text-align: center;\n}\n"] }]
148
+ args: [{ selector: 'prism-coverage-panel', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [PrismMetricCardComponent], template: "@if (coverage()?.found) {\n <div class=\"cov-body\">\n @for (stat of summaryStats(); track stat.label) {\n <prism-metric-card\n unit=\"%\"\n [label]=\"stat.label\"\n [value]=\"'' + stat.pct\"\n [tone]=\"stat.tone\"\n [statusLabel]=\"stat.statusLabel\"\n [fraction]=\"stat.pct / 100\"\n [threshold]=\"stat.threshold / 100\"\n [title]=\"stat.title\" />\n }\n </div>\n\n @if (files().length) {\n <div class=\"cov-files\">\n <div class=\"cov-file cov-file--header\">\n <span>File</span>\n <span>Lines</span>\n <span>Branch</span>\n <span>Func</span>\n <span>Stmt</span>\n </div>\n @for (file of files(); track file.path) {\n <div class=\"cov-file\">\n <span class=\"cov-file-name\">{{ fileName(file.path) }}</span>\n <span\n class=\"cov-file-val\"\n [class.bad]=\"file.lines.pct < thresholds().lines\"\n >{{ file.lines.pct }}%</span\n >\n <span\n class=\"cov-file-val\"\n [class.bad]=\"file.branches.pct < thresholds().branches\"\n >{{ file.branches.pct }}%</span\n >\n <span\n class=\"cov-file-val\"\n [class.bad]=\"file.functions.pct < thresholds().functions\"\n >{{ file.functions.pct }}%</span\n >\n <span\n class=\"cov-file-val\"\n [class.bad]=\"file.statements.pct < thresholds().statements\"\n >{{ file.statements.pct }}%</span\n >\n </div>\n }\n </div>\n }\n} @else {\n <div class=\"cov-empty\">\n <p>No coverage data. Run tests with coverage enabled.</p>\n </div>\n}\n", styles: [".cov-body {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\n gap: 12px;\n padding: 16px 18px;\n}\n\n.cov-files {\n padding: 12px;\n margin: 0 18px 16px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-radius: 6px;\n}\n\n.cov-file {\n display: grid;\n grid-template-columns: 1fr auto auto auto auto;\n gap: 16px;\n align-items: center;\n padding: 7px 4px;\n font-family: var(--font-mono);\n font-size: 12px;\n border-bottom: 1px solid var(--prism-border);\n}\n\n.cov-file:last-child {\n border-bottom: 0;\n}\n\n.cov-file--header {\n padding-bottom: 8px;\n font-size: 10.5px;\n font-weight: 700;\n color: var(--prism-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n border-bottom: 1px solid var(--prism-border);\n}\n\n.cov-file-name {\n color: var(--prism-text-2);\n}\n\n.cov-file-val {\n min-width: 54px;\n color: var(--prism-text);\n text-align: right;\n}\n\n.cov-file-val.bad {\n color: var(--prism-danger);\n}\n\n.cov-empty {\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 120px;\n font-size: 13px;\n color: var(--prism-text-muted);\n text-align: center;\n}\n"] }]
153
149
  }], null, { activeComponent: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeComponent", required: false }] }] }); })();
154
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(CoveragePanelComponent, { className: "CoveragePanelComponent", filePath: "coverage-panel.component.ts", lineNumber: 18 }); })();
150
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(CoveragePanelComponent, { className: "CoveragePanelComponent", filePath: "coverage-panel.component.ts", lineNumber: 21 }); })();
@@ -1 +1 @@
1
- {"version":3,"file":"coverage-plugin.browser.d.ts","sourceRoot":"","sources":["../src/coverage-plugin.browser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAEjE,wBAAgB,cAAc,CAAC,QAAQ,CAAC,EAAE,qBAAqB,GAAG,aAAa,CAuB9E"}
1
+ {"version":3,"file":"coverage-plugin.browser.d.ts","sourceRoot":"","sources":["../src/coverage-plugin.browser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAEjE,wBAAgB,cAAc,CAAC,QAAQ,CAAC,EAAE,qBAAqB,GAAG,aAAa,CAsB9E"}
@@ -1,4 +1,4 @@
1
- import { COVERAGE_NAVIGATION_DECORATION, coverageBadge } from './coverage-contributions.js';
1
+ import { COVERAGE_NAVIGATION_DECORATION } from './coverage-contributions.js';
2
2
  export function coveragePlugin(_options) {
3
3
  return {
4
4
  name: '@ng-prism/plugin-coverage',
@@ -6,7 +6,6 @@ export function coveragePlugin(_options) {
6
6
  {
7
7
  id: 'coverage',
8
8
  label: 'Coverage',
9
- badge: coverageBadge,
10
9
  icon: 'shield-check',
11
10
  loadComponent: () => import('./coverage-panel.component.js').then((m) => m.CoveragePanelComponent)
12
11
  }
@@ -1 +1 @@
1
- {"version":3,"file":"coverage-plugin.d.ts","sourceRoot":"","sources":["../src/coverage-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAIrF,eAAO,MAAM,2BAA2B,EAAE,kBAKzC,CAAC;AAEF,wBAAgB,yBAAyB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,GAAG,kBAAkB,CAW1G;AAED,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,aAAa,CAyD7E"}
1
+ {"version":3,"file":"coverage-plugin.d.ts","sourceRoot":"","sources":["../src/coverage-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAIrF,eAAO,MAAM,2BAA2B,EAAE,kBAKzC,CAAC;AAEF,wBAAgB,yBAAyB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,GAAG,kBAAkB,CAW1G;AAED,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,aAAa,CAwD7E"}
@@ -1,4 +1,4 @@
1
- import { COVERAGE_NAVIGATION_DECORATION, coverageBadge } from './coverage-contributions.js';
1
+ import { COVERAGE_NAVIGATION_DECORATION } from './coverage-contributions.js';
2
2
  const DEFAULT_COVERAGE_PATH = 'coverage/coverage-summary.json';
3
3
  export const DEFAULT_COVERAGE_THRESHOLDS = {
4
4
  lines: 80,
@@ -58,7 +58,6 @@ export function coveragePlugin(options) {
58
58
  {
59
59
  id: 'coverage',
60
60
  label: 'Coverage',
61
- badge: coverageBadge,
62
61
  loadComponent: () => import('./coverage-panel.component.js').then((m) => m.CoveragePanelComponent)
63
62
  }
64
63
  ],
@@ -1 +1 @@
1
- {"version":3,"file":"coverage-summary.d.ts","sourceRoot":"","sources":["../src/coverage-summary.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE/E;;;;;;;GAOG;AACH,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,UAAU,EAAE,kBAAkB,GAAG,MAAM,CAEnE;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,kBAAkB,GAAG,eAAe,CAKpG"}
1
+ {"version":3,"file":"coverage-summary.d.ts","sourceRoot":"","sources":["../src/coverage-summary.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE/E;;;;;;;GAOG;AACH,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,UAAU,EAAE,kBAAkB,GAAG,MAAM,CAEnE;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,kBAAkB,GAAG,eAAe,CAKpG"}
@@ -1,3 +1,4 @@
1
+ import { statusFor, thresholdFromTarget } from '@ng-prism/core/plugin';
1
2
  /**
2
3
  * Threshold derivation lives in its own module so both sides can reach it:
3
4
  * `coverage-plugin.ts` carries the build-time hook that reads the istanbul
@@ -12,6 +13,6 @@ export function avgThreshold(thresholds) {
12
13
  }
13
14
  export function deriveCoverageSummary(score, thresholds) {
14
15
  const avg = avgThreshold(thresholds);
15
- const variant = score >= avg ? 'ok' : score >= avg * 0.75 ? 'warn' : 'danger';
16
- return { variant, label: `Coverage: ${score}% (target ${avg}%)` };
16
+ const variant = statusFor(score, thresholdFromTarget(avg));
17
+ return { variant, label: `Coverage ${score}% · target ${avg}%` };
17
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-prism/plugin-coverage",
3
- "version": "22.6.0",
3
+ "version": "22.7.0-beta.0",
4
4
  "description": "Test coverage panel for ng-prism.",
5
5
  "ng-add": {
6
6
  "save": "devDependencies"
@@ -35,7 +35,7 @@
35
35
  ],
36
36
  "peerDependencies": {
37
37
  "@angular/core": ">=20.0.0",
38
- "@ng-prism/core": "^22.6.0-0",
38
+ "@ng-prism/core": "^22.7.0-0",
39
39
  "@angular-devkit/schematics": ">=20.0.0"
40
40
  },
41
41
  "devDependencies": {