executable-stories-formatters 0.15.0 → 0.16.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/dist/cli.js +238 -64
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +197 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +197 -55
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/schemas/README.md +1 -1
- package/templates/astro-starlight/astro.config.mjs +57 -0
- package/templates/astro-starlight/gitignore +14 -0
- package/templates/astro-starlight/package.json +20 -0
- package/templates/astro-starlight/public/stories/assets/.gitkeep +0 -0
- package/templates/astro-starlight/public/stories/notes-index.json +4 -0
- package/templates/astro-starlight/public/stories/story-report.json +17 -0
- package/templates/astro-starlight/src/components/ApiOperations.astro +366 -0
- package/templates/astro-starlight/src/components/Checklist.astro +15 -0
- package/templates/astro-starlight/src/components/HealthDashboard.astro +171 -0
- package/templates/astro-starlight/src/components/PageTitle.astro +53 -0
- package/templates/astro-starlight/src/components/VerifiedBy.astro +281 -0
- package/templates/astro-starlight/src/components/VerifiedStep.astro +91 -0
- package/templates/astro-starlight/src/content/docs/examples/example-adr.mdx +45 -0
- package/templates/astro-starlight/src/content/docs/guides/behavior-portal.mdx +41 -0
- package/templates/astro-starlight/src/content/docs/guides/writing-docs.mdx +49 -0
- package/templates/astro-starlight/src/content/docs/index.mdx +49 -0
- package/templates/astro-starlight/src/content/docs/stories/.gitkeep +0 -0
- package/templates/astro-starlight/src/content.config.ts +18 -0
- package/templates/astro-starlight/src/lib/config.ts +50 -0
- package/templates/astro-starlight/src/lib/render-doc-entry.ts +154 -0
- package/templates/astro-starlight/src/lib/report-health.ts +61 -0
- package/templates/astro-starlight/src/lib/verification.ts +247 -0
- package/templates/astro-starlight/src/pages/explorer/explorer.css +729 -0
- package/templates/astro-starlight/src/pages/explorer/index.astro +404 -0
- package/templates/astro-starlight/src/styles/global.css +293 -0
- package/templates/astro-starlight/src/styles/themes/corporate.css +83 -0
- package/templates/astro-starlight/src/styles/themes/dashboard.css +76 -0
- package/templates/astro-starlight/src/styles/themes/default.css +86 -0
- package/templates/astro-starlight/src/styles/themes/minimal.css +87 -0
- package/templates/astro-starlight/src/styles/themes/playful.css +77 -0
- package/templates/astro-starlight/src/styles/themes/terminal.css +77 -0
- package/templates/astro-starlight/tsconfig.json +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "executable-stories-formatters",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Cucumber-compatible report formats (HTML, Markdown, JUnit XML, Cucumber JSON) for executable-stories test results.",
|
|
5
5
|
"author": "Jag Reehal <jag@jagreehal.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"files": [
|
|
37
37
|
"dist",
|
|
38
38
|
"skills",
|
|
39
|
-
"schemas"
|
|
39
|
+
"schemas",
|
|
40
|
+
"templates"
|
|
40
41
|
],
|
|
41
42
|
"engines": {
|
|
42
43
|
"node": ">=22"
|
package/schemas/README.md
CHANGED
|
@@ -172,7 +172,7 @@ SUBCOMMANDS
|
|
|
172
172
|
| `--format <formats>` | `html` | Comma-separated: `html`, `markdown`, `junit`, `cucumber-json`, `cucumber-html`, `cucumber-messages` |
|
|
173
173
|
| `--input-type <type>` | `raw` | Input type: `raw`, `canonical`, or `ndjson` |
|
|
174
174
|
| `--output-dir <dir>` | `reports` | Output directory |
|
|
175
|
-
| `--output-name <name>` | `
|
|
175
|
+
| `--output-name <name>` | `index` | Base filename |
|
|
176
176
|
| `--synthesize-stories` | on | Synthesize story metadata for plain test results |
|
|
177
177
|
| `--no-synthesize-stories` | | Disable story synthesis (strict mode) |
|
|
178
178
|
| `--html-title <title>` | `Test Results` | HTML report title |
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import starlight from '@astrojs/starlight';
|
|
3
|
+
import astroMermaid from 'astro-mermaid';
|
|
4
|
+
import remarkGfm from 'remark-gfm';
|
|
5
|
+
import { defineConfig } from 'astro/config';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Available themes (match HTML formatter themes):
|
|
9
|
+
* default — cucumber green, IBM Plex typography
|
|
10
|
+
* corporate — navy, Playfair Display serif headings
|
|
11
|
+
* terminal — green-on-dark, JetBrains Mono, flat
|
|
12
|
+
* minimal — teal, Noto Serif Display headings, warm neutrals
|
|
13
|
+
* dashboard — blue, DM Sans + JetBrains Mono
|
|
14
|
+
* playful — coral, rounded corners, Source Sans 3
|
|
15
|
+
*
|
|
16
|
+
* To switch themes, replace `themes/default.css` below with the filename
|
|
17
|
+
* of the theme you want (e.g. `./src/styles/themes/corporate.css`).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
// GFM tables don't render in .mdx by default — enable them so hand-written
|
|
22
|
+
// and generated .mdx pages (ADRs, API coverage, decision logs) show tables.
|
|
23
|
+
markdown: {
|
|
24
|
+
remarkPlugins: [remarkGfm],
|
|
25
|
+
},
|
|
26
|
+
integrations: [
|
|
27
|
+
astroMermaid(),
|
|
28
|
+
starlight({
|
|
29
|
+
title: 'Story Docs',
|
|
30
|
+
description: 'Living documentation generated from executable stories.',
|
|
31
|
+
// Renders a live verification badge under the title of any page that
|
|
32
|
+
// declares `verifiedBy` in its frontmatter.
|
|
33
|
+
components: {
|
|
34
|
+
PageTitle: './src/components/PageTitle.astro',
|
|
35
|
+
},
|
|
36
|
+
sidebar: [
|
|
37
|
+
{ label: 'Home', slug: 'index' },
|
|
38
|
+
{ label: 'Explorer', link: '/explorer/' },
|
|
39
|
+
// Hand-written content — edited by the team, never overwritten by a regenerate.
|
|
40
|
+
{ label: 'Guides', autogenerate: { directory: 'guides' } },
|
|
41
|
+
{ label: 'Notes', autogenerate: { directory: 'notes' } },
|
|
42
|
+
// Autogenerated, not a hard-coded slug — clearing the sample ADR (or the
|
|
43
|
+
// whole directory) leaves the build green instead of 404ing on a missing slug.
|
|
44
|
+
{ label: 'Examples', autogenerate: { directory: 'examples' } },
|
|
45
|
+
// Generated from tests by `format --format astro`.
|
|
46
|
+
{
|
|
47
|
+
label: 'Stories',
|
|
48
|
+
autogenerate: { directory: 'stories' },
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
customCss: [
|
|
52
|
+
'./src/styles/global.css',
|
|
53
|
+
'./src/styles/themes/default.css',
|
|
54
|
+
],
|
|
55
|
+
}),
|
|
56
|
+
],
|
|
57
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
.astro/
|
|
3
|
+
dist/
|
|
4
|
+
|
|
5
|
+
# Machine-owned portal output. Regenerated by build-docs / CI.
|
|
6
|
+
src/content/docs/stories/*
|
|
7
|
+
!src/content/docs/stories/.gitkeep
|
|
8
|
+
public/stories/assets/*
|
|
9
|
+
!public/stories/assets/.gitkeep
|
|
10
|
+
public/stories/story-report.json
|
|
11
|
+
public/stories/scenario-links.json
|
|
12
|
+
public/stories/notes-index.json
|
|
13
|
+
!public/stories/notes-index.json
|
|
14
|
+
public/stories/changes.json
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "story-docs",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "astro dev",
|
|
8
|
+
"build": "astro build",
|
|
9
|
+
"preview": "astro preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@astrojs/starlight": "^0.38.3",
|
|
13
|
+
"astro": "^6.1.8",
|
|
14
|
+
"astro-mermaid": "^2.0.1",
|
|
15
|
+
"highlight.js": "^11.10.0",
|
|
16
|
+
"marked": "^12.0.0",
|
|
17
|
+
"mermaid": "^11.14.0",
|
|
18
|
+
"remark-gfm": "^4.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0",
|
|
3
|
+
"runId": "empty",
|
|
4
|
+
"startedAtMs": 0,
|
|
5
|
+
"finishedAtMs": 0,
|
|
6
|
+
"durationMs": 0,
|
|
7
|
+
"projectRoot": "",
|
|
8
|
+
"summary": {
|
|
9
|
+
"total": 0,
|
|
10
|
+
"passed": 0,
|
|
11
|
+
"failed": 0,
|
|
12
|
+
"skipped": 0,
|
|
13
|
+
"pending": 0,
|
|
14
|
+
"durationMs": 0
|
|
15
|
+
},
|
|
16
|
+
"features": []
|
|
17
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <ApiOperations tag="Transfers" hasRun endpoints={[...]} summary={{...}} />
|
|
4
|
+
*
|
|
5
|
+
* Renders the API coverage table for one OpenAPI tag. Generated by
|
|
6
|
+
* `executable-stories import-openapi`, which writes an MDX page that imports
|
|
7
|
+
* this component and passes the endpoints + their test coverage.
|
|
8
|
+
*
|
|
9
|
+
* Each endpoint shows an HTTP-method pill, its path, a coverage badge, and the
|
|
10
|
+
* stories that exercise it — linked straight into the Scenario Explorer, so the
|
|
11
|
+
* API docs can never drift from the tests that prove them.
|
|
12
|
+
*/
|
|
13
|
+
import { explorerUrl } from "../lib/config";
|
|
14
|
+
|
|
15
|
+
type CoverageStatus = "covered" | "failing" | "uncovered";
|
|
16
|
+
|
|
17
|
+
export interface ApiStoryRef {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
status: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ApiEndpoint {
|
|
24
|
+
method: string;
|
|
25
|
+
path: string;
|
|
26
|
+
summary: string;
|
|
27
|
+
status: CoverageStatus;
|
|
28
|
+
stories: ApiStoryRef[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Props {
|
|
32
|
+
tag: string;
|
|
33
|
+
hasRun: boolean;
|
|
34
|
+
endpoints: ApiEndpoint[];
|
|
35
|
+
summary: { total: number; covered: number; failing: number; uncovered: number };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { hasRun, endpoints, summary } = Astro.props;
|
|
39
|
+
|
|
40
|
+
const coverageLabel: Record<CoverageStatus, string> = {
|
|
41
|
+
covered: "Covered",
|
|
42
|
+
failing: "Failing",
|
|
43
|
+
uncovered: "Uncovered",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const methodClass = (method: string) => `m-${method.toLowerCase()}`;
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
<section class="api-ops">
|
|
50
|
+
<header class="api-ops__bar">
|
|
51
|
+
<p class="api-ops__lede">
|
|
52
|
+
Generated from the OpenAPI spec.{" "}
|
|
53
|
+
{hasRun
|
|
54
|
+
? "Coverage reflects the last test run."
|
|
55
|
+
: "Re-run with --run <story-report.json> to show per-endpoint coverage."}
|
|
56
|
+
</p>
|
|
57
|
+
{
|
|
58
|
+
hasRun && (
|
|
59
|
+
<div class="api-ops__stats" role="group" aria-label="Coverage summary">
|
|
60
|
+
<span class="stat stat--covered">
|
|
61
|
+
<strong>{summary.covered}</strong> covered
|
|
62
|
+
</span>
|
|
63
|
+
{summary.failing > 0 && (
|
|
64
|
+
<span class="stat stat--failing">
|
|
65
|
+
<strong>{summary.failing}</strong> failing
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
<span class="stat stat--uncovered">
|
|
69
|
+
<strong>{summary.uncovered}</strong> no test
|
|
70
|
+
</span>
|
|
71
|
+
<span class="stat stat--total">of {summary.total}</span>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
</header>
|
|
76
|
+
|
|
77
|
+
<div class="api-ops__rows" role="table" aria-label="API endpoints">
|
|
78
|
+
<div class="api-ops__row api-ops__row--head" role="row">
|
|
79
|
+
<span role="columnheader">Endpoint</span>
|
|
80
|
+
<span role="columnheader">Summary</span>
|
|
81
|
+
{hasRun && <span role="columnheader">Coverage</span>}
|
|
82
|
+
{hasRun && <span role="columnheader">Verified by</span>}
|
|
83
|
+
</div>
|
|
84
|
+
{
|
|
85
|
+
endpoints.map((ep) => (
|
|
86
|
+
<div class="api-ops__row" role="row">
|
|
87
|
+
<span class="cell-endpoint" role="cell">
|
|
88
|
+
<span class:list={["method", methodClass(ep.method)]}>{ep.method}</span>
|
|
89
|
+
<code class="path">{ep.path}</code>
|
|
90
|
+
</span>
|
|
91
|
+
<span class="cell-summary" role="cell">{ep.summary || "—"}</span>
|
|
92
|
+
{hasRun && (
|
|
93
|
+
<span role="cell">
|
|
94
|
+
<span class:list={["coverage", `coverage--${ep.status}`]}>
|
|
95
|
+
<span class="coverage__dot" aria-hidden="true" />
|
|
96
|
+
{coverageLabel[ep.status]}
|
|
97
|
+
</span>
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
{hasRun && (
|
|
101
|
+
<span class="cell-stories" role="cell">
|
|
102
|
+
{ep.stories.length === 0 ? (
|
|
103
|
+
<span class="no-stories">—</span>
|
|
104
|
+
) : (
|
|
105
|
+
ep.stories.map((s) => (
|
|
106
|
+
<a class:list={["story-chip", `story-chip--${s.status}`]} href={explorerUrl(s.id)}>
|
|
107
|
+
<span class="story-chip__dot" aria-hidden="true" />
|
|
108
|
+
{s.title}
|
|
109
|
+
</a>
|
|
110
|
+
))
|
|
111
|
+
)}
|
|
112
|
+
</span>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
))
|
|
116
|
+
}
|
|
117
|
+
</div>
|
|
118
|
+
</section>
|
|
119
|
+
|
|
120
|
+
<style>
|
|
121
|
+
.api-ops {
|
|
122
|
+
margin: 1.25rem 0 2rem;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.api-ops__bar {
|
|
126
|
+
display: flex;
|
|
127
|
+
flex-wrap: wrap;
|
|
128
|
+
align-items: center;
|
|
129
|
+
justify-content: space-between;
|
|
130
|
+
gap: 0.75rem 1.25rem;
|
|
131
|
+
margin-bottom: 1rem;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.api-ops__lede {
|
|
135
|
+
margin: 0;
|
|
136
|
+
color: var(--es-text-muted);
|
|
137
|
+
font-size: var(--sl-text-sm);
|
|
138
|
+
max-width: 52ch;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.api-ops__stats {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 0.4rem;
|
|
145
|
+
flex-wrap: wrap;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.stat {
|
|
149
|
+
display: inline-flex;
|
|
150
|
+
align-items: baseline;
|
|
151
|
+
gap: 0.3rem;
|
|
152
|
+
font-size: var(--sl-text-xs);
|
|
153
|
+
color: var(--es-text-muted);
|
|
154
|
+
padding: 0.25rem 0.6rem;
|
|
155
|
+
border: 1px solid var(--es-line);
|
|
156
|
+
border-radius: var(--es-radius-pill);
|
|
157
|
+
background: var(--es-surface);
|
|
158
|
+
}
|
|
159
|
+
.stat strong {
|
|
160
|
+
font-size: var(--sl-text-sm);
|
|
161
|
+
color: var(--es-text);
|
|
162
|
+
}
|
|
163
|
+
.stat--covered {
|
|
164
|
+
color: var(--es-passed);
|
|
165
|
+
border-color: color-mix(in oklab, var(--es-passed) 40%, var(--es-line));
|
|
166
|
+
background: var(--es-passed-soft);
|
|
167
|
+
}
|
|
168
|
+
.stat--covered strong {
|
|
169
|
+
color: var(--es-passed);
|
|
170
|
+
}
|
|
171
|
+
.stat--failing {
|
|
172
|
+
color: var(--es-failed);
|
|
173
|
+
border-color: color-mix(in oklab, var(--es-failed) 40%, var(--es-line));
|
|
174
|
+
background: var(--es-failed-soft);
|
|
175
|
+
}
|
|
176
|
+
.stat--failing strong {
|
|
177
|
+
color: var(--es-failed);
|
|
178
|
+
}
|
|
179
|
+
.stat--uncovered {
|
|
180
|
+
color: var(--es-skipped);
|
|
181
|
+
border-color: color-mix(in oklab, var(--es-skipped) 40%, var(--es-line));
|
|
182
|
+
background: var(--es-skipped-soft);
|
|
183
|
+
}
|
|
184
|
+
.stat--uncovered strong {
|
|
185
|
+
color: var(--es-skipped);
|
|
186
|
+
}
|
|
187
|
+
.stat--total {
|
|
188
|
+
border: none;
|
|
189
|
+
background: none;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.api-ops__rows {
|
|
193
|
+
border: 1px solid var(--es-line);
|
|
194
|
+
border-radius: var(--es-radius);
|
|
195
|
+
overflow: hidden;
|
|
196
|
+
background: var(--es-bg);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.api-ops__row {
|
|
200
|
+
display: grid;
|
|
201
|
+
grid-template-columns: minmax(220px, 1.2fr) minmax(140px, 1.4fr) auto minmax(160px, 1.3fr);
|
|
202
|
+
gap: 1rem;
|
|
203
|
+
align-items: center;
|
|
204
|
+
padding: 0.7rem 1rem;
|
|
205
|
+
border-top: 1px solid var(--es-line);
|
|
206
|
+
}
|
|
207
|
+
.api-ops__row:first-child {
|
|
208
|
+
border-top: none;
|
|
209
|
+
}
|
|
210
|
+
.api-ops__row:not(.api-ops__row--head):hover {
|
|
211
|
+
background: var(--es-surface);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.api-ops__row--head {
|
|
215
|
+
background: var(--es-surface-2);
|
|
216
|
+
font-size: var(--sl-text-xs);
|
|
217
|
+
font-weight: 600;
|
|
218
|
+
text-transform: uppercase;
|
|
219
|
+
letter-spacing: 0.06em;
|
|
220
|
+
color: var(--es-text-muted);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.cell-endpoint {
|
|
224
|
+
display: inline-flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
gap: 0.6rem;
|
|
227
|
+
min-width: 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.method {
|
|
231
|
+
flex: none;
|
|
232
|
+
font-family: var(--es-mono);
|
|
233
|
+
font-size: 0.7rem;
|
|
234
|
+
font-weight: 700;
|
|
235
|
+
letter-spacing: 0.04em;
|
|
236
|
+
padding: 0.2rem 0.45rem;
|
|
237
|
+
border-radius: var(--es-radius-sm);
|
|
238
|
+
color: #fff;
|
|
239
|
+
background: var(--es-method-default);
|
|
240
|
+
}
|
|
241
|
+
.m-get {
|
|
242
|
+
background: var(--es-method-get);
|
|
243
|
+
}
|
|
244
|
+
.m-post {
|
|
245
|
+
background: var(--es-method-post);
|
|
246
|
+
}
|
|
247
|
+
.m-put {
|
|
248
|
+
background: var(--es-method-put);
|
|
249
|
+
}
|
|
250
|
+
.m-patch {
|
|
251
|
+
background: var(--es-method-patch);
|
|
252
|
+
}
|
|
253
|
+
.m-delete {
|
|
254
|
+
background: var(--es-method-delete);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.path {
|
|
258
|
+
font-family: var(--es-mono);
|
|
259
|
+
font-size: var(--sl-text-sm);
|
|
260
|
+
color: var(--es-text);
|
|
261
|
+
background: none;
|
|
262
|
+
border: none;
|
|
263
|
+
padding: 0;
|
|
264
|
+
overflow: hidden;
|
|
265
|
+
text-overflow: ellipsis;
|
|
266
|
+
white-space: nowrap;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.cell-summary {
|
|
270
|
+
color: var(--es-text-soft);
|
|
271
|
+
font-size: var(--sl-text-sm);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.coverage {
|
|
275
|
+
display: inline-flex;
|
|
276
|
+
align-items: center;
|
|
277
|
+
gap: 0.4rem;
|
|
278
|
+
font-size: var(--sl-text-xs);
|
|
279
|
+
font-weight: 600;
|
|
280
|
+
padding: 0.2rem 0.6rem;
|
|
281
|
+
border-radius: var(--es-radius-pill);
|
|
282
|
+
border: 1px solid currentcolor;
|
|
283
|
+
white-space: nowrap;
|
|
284
|
+
}
|
|
285
|
+
.coverage__dot {
|
|
286
|
+
width: 0.5rem;
|
|
287
|
+
height: 0.5rem;
|
|
288
|
+
border-radius: 50%;
|
|
289
|
+
background: currentcolor;
|
|
290
|
+
}
|
|
291
|
+
.coverage--covered {
|
|
292
|
+
color: var(--es-passed);
|
|
293
|
+
background: var(--es-passed-soft);
|
|
294
|
+
}
|
|
295
|
+
.coverage--failing {
|
|
296
|
+
color: var(--es-failed);
|
|
297
|
+
background: var(--es-failed-soft);
|
|
298
|
+
}
|
|
299
|
+
.coverage--uncovered {
|
|
300
|
+
color: var(--es-skipped);
|
|
301
|
+
background: var(--es-skipped-soft);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.cell-stories {
|
|
305
|
+
display: flex;
|
|
306
|
+
flex-wrap: wrap;
|
|
307
|
+
gap: 0.35rem;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.story-chip {
|
|
311
|
+
display: inline-flex;
|
|
312
|
+
align-items: center;
|
|
313
|
+
gap: 0.35rem;
|
|
314
|
+
font-size: var(--sl-text-xs);
|
|
315
|
+
text-decoration: none;
|
|
316
|
+
color: var(--es-text-soft);
|
|
317
|
+
padding: 0.2rem 0.5rem;
|
|
318
|
+
border: 1px solid var(--es-line);
|
|
319
|
+
border-radius: var(--es-radius-pill);
|
|
320
|
+
background: var(--es-surface);
|
|
321
|
+
transition: border-color 120ms ease, color 120ms ease;
|
|
322
|
+
}
|
|
323
|
+
.story-chip:hover {
|
|
324
|
+
border-color: var(--es-accent);
|
|
325
|
+
color: var(--es-text);
|
|
326
|
+
}
|
|
327
|
+
.story-chip:focus-visible {
|
|
328
|
+
outline: 2px solid var(--es-accent);
|
|
329
|
+
outline-offset: 2px;
|
|
330
|
+
}
|
|
331
|
+
.story-chip__dot {
|
|
332
|
+
width: 0.45rem;
|
|
333
|
+
height: 0.45rem;
|
|
334
|
+
border-radius: 50%;
|
|
335
|
+
background: var(--chip-dot, var(--es-text-muted));
|
|
336
|
+
}
|
|
337
|
+
.story-chip--passed {
|
|
338
|
+
--chip-dot: var(--es-passed);
|
|
339
|
+
}
|
|
340
|
+
.story-chip--failed {
|
|
341
|
+
--chip-dot: var(--es-failed);
|
|
342
|
+
}
|
|
343
|
+
.story-chip--skipped {
|
|
344
|
+
--chip-dot: var(--es-skipped);
|
|
345
|
+
}
|
|
346
|
+
.story-chip--pending {
|
|
347
|
+
--chip-dot: var(--es-pending);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.no-stories {
|
|
351
|
+
color: var(--es-text-muted);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
@media (max-width: 720px) {
|
|
355
|
+
.api-ops__row {
|
|
356
|
+
grid-template-columns: 1fr;
|
|
357
|
+
gap: 0.5rem;
|
|
358
|
+
}
|
|
359
|
+
.api-ops__row--head {
|
|
360
|
+
display: none;
|
|
361
|
+
}
|
|
362
|
+
.cell-summary {
|
|
363
|
+
color: var(--es-text-muted);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <Checklist> wraps a set of <VerifiedStep> items into a clean runbook list.
|
|
4
|
+
*/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<ul class="checklist"><slot /></ul>
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
.checklist {
|
|
11
|
+
margin: 1rem 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
border-top: 1px solid var(--sl-color-gray-6);
|
|
14
|
+
}
|
|
15
|
+
</style>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <HealthDashboard /> — a living landing widget.
|
|
4
|
+
*
|
|
5
|
+
* Reads the latest run at build time and shows pass rate, counts, freshness,
|
|
6
|
+
* and any failing stories. Drop it at the top of the home page so the first
|
|
7
|
+
* thing a stakeholder sees is whether the documented system is green.
|
|
8
|
+
*/
|
|
9
|
+
import { report } from "../lib/config";
|
|
10
|
+
import { summarizeHealth, type FailingScenario } from "../lib/report-health";
|
|
11
|
+
|
|
12
|
+
const health = summarizeHealth(report);
|
|
13
|
+
const passPct = Math.round(health.passRate * 100);
|
|
14
|
+
const lastRun = health.lastRunMs
|
|
15
|
+
? new Date(health.lastRunMs).toLocaleString(undefined, {
|
|
16
|
+
dateStyle: "medium",
|
|
17
|
+
timeStyle: "short",
|
|
18
|
+
})
|
|
19
|
+
: null;
|
|
20
|
+
|
|
21
|
+
const overall = health.empty
|
|
22
|
+
? "empty"
|
|
23
|
+
: health.failed > 0
|
|
24
|
+
? "failing"
|
|
25
|
+
: "healthy";
|
|
26
|
+
|
|
27
|
+
const metrics: Array<{ label: string; value: string | number }> = [
|
|
28
|
+
{ label: "Pass rate", value: health.empty ? "—" : `${passPct}%` },
|
|
29
|
+
{ label: "Scenarios", value: health.total },
|
|
30
|
+
{ label: "Passed", value: health.passed },
|
|
31
|
+
{ label: "Failed", value: health.failed },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const failing: FailingScenario[] = health.failing;
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
<section class:list={["health", `health--${overall}`]} aria-label="Test health dashboard">
|
|
38
|
+
<header class="health__head">
|
|
39
|
+
<span class="health__dot" aria-hidden="true"></span>
|
|
40
|
+
<strong class="health__title">
|
|
41
|
+
{health.empty ? "No test run yet" : health.failed > 0 ? "Attention needed" : "All systems documented & passing"}
|
|
42
|
+
</strong>
|
|
43
|
+
{lastRun && <span class="health__meta">last run {lastRun}</span>}
|
|
44
|
+
</header>
|
|
45
|
+
|
|
46
|
+
<div class="health__metrics">
|
|
47
|
+
{
|
|
48
|
+
metrics.map((m) => (
|
|
49
|
+
<div class="health__metric">
|
|
50
|
+
<span class="health__metric-value">{m.value}</span>
|
|
51
|
+
<span class="health__metric-label">{m.label}</span>
|
|
52
|
+
</div>
|
|
53
|
+
))
|
|
54
|
+
}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
health.empty && (
|
|
59
|
+
<p class="health__hint">
|
|
60
|
+
Generate a run with <code>executable-stories build-docs reports/raw-run.json --site-dir .</code> to light up this dashboard.
|
|
61
|
+
</p>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
failing.length > 0 && (
|
|
67
|
+
<details class="health__failing" open>
|
|
68
|
+
<summary>{failing.length} failing {failing.length === 1 ? "story" : "stories"}</summary>
|
|
69
|
+
<ul>
|
|
70
|
+
{failing.map((f) => (
|
|
71
|
+
<li>
|
|
72
|
+
<span class="health__failing-title">{f.title}</span>
|
|
73
|
+
{f.feature && <span class="health__failing-feature"> · {f.feature}</span>}
|
|
74
|
+
</li>
|
|
75
|
+
))}
|
|
76
|
+
</ul>
|
|
77
|
+
</details>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
</section>
|
|
81
|
+
|
|
82
|
+
<style>
|
|
83
|
+
.health {
|
|
84
|
+
--health-accent: var(--sl-color-gray-3);
|
|
85
|
+
border: 1px solid var(--sl-color-gray-5);
|
|
86
|
+
border-radius: 0.75rem;
|
|
87
|
+
padding: 1.25rem 1.5rem;
|
|
88
|
+
margin: 0 0 2rem;
|
|
89
|
+
background: var(--sl-color-gray-7, var(--sl-color-gray-6));
|
|
90
|
+
}
|
|
91
|
+
.health--healthy {
|
|
92
|
+
--health-accent: #16a34a;
|
|
93
|
+
}
|
|
94
|
+
.health--failing {
|
|
95
|
+
--health-accent: #dc2626;
|
|
96
|
+
}
|
|
97
|
+
.health--empty {
|
|
98
|
+
--health-accent: #d97706;
|
|
99
|
+
}
|
|
100
|
+
.health__head {
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
gap: 0.6rem;
|
|
104
|
+
margin-bottom: 1rem;
|
|
105
|
+
}
|
|
106
|
+
.health__dot {
|
|
107
|
+
width: 0.7rem;
|
|
108
|
+
height: 0.7rem;
|
|
109
|
+
border-radius: 999px;
|
|
110
|
+
background: var(--health-accent);
|
|
111
|
+
flex: none;
|
|
112
|
+
}
|
|
113
|
+
.health__title {
|
|
114
|
+
font-size: var(--sl-text-lg);
|
|
115
|
+
}
|
|
116
|
+
.health__meta {
|
|
117
|
+
margin-left: auto;
|
|
118
|
+
color: var(--sl-color-gray-3);
|
|
119
|
+
font-size: var(--sl-text-xs);
|
|
120
|
+
}
|
|
121
|
+
.health__metrics {
|
|
122
|
+
display: grid;
|
|
123
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
124
|
+
gap: 0.75rem;
|
|
125
|
+
}
|
|
126
|
+
.health__metric {
|
|
127
|
+
border: 1px solid var(--sl-color-gray-5);
|
|
128
|
+
border-radius: 0.5rem;
|
|
129
|
+
padding: 0.75rem;
|
|
130
|
+
text-align: center;
|
|
131
|
+
background: var(--sl-color-black, transparent);
|
|
132
|
+
}
|
|
133
|
+
.health__metric-value {
|
|
134
|
+
display: block;
|
|
135
|
+
font-size: var(--sl-text-2xl);
|
|
136
|
+
font-weight: 700;
|
|
137
|
+
line-height: 1;
|
|
138
|
+
}
|
|
139
|
+
.health__metric-label {
|
|
140
|
+
display: block;
|
|
141
|
+
margin-top: 0.35rem;
|
|
142
|
+
font-size: var(--sl-text-xs);
|
|
143
|
+
color: var(--sl-color-gray-3);
|
|
144
|
+
}
|
|
145
|
+
.health__hint {
|
|
146
|
+
margin: 1rem 0 0;
|
|
147
|
+
font-size: var(--sl-text-sm);
|
|
148
|
+
color: var(--sl-color-gray-2);
|
|
149
|
+
}
|
|
150
|
+
.health__failing {
|
|
151
|
+
margin-top: 1rem;
|
|
152
|
+
}
|
|
153
|
+
.health__failing > summary {
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
color: #dc2626;
|
|
156
|
+
font-weight: 600;
|
|
157
|
+
}
|
|
158
|
+
.health__failing ul {
|
|
159
|
+
margin: 0.5rem 0 0;
|
|
160
|
+
padding-left: 1.1rem;
|
|
161
|
+
}
|
|
162
|
+
.health__failing-feature {
|
|
163
|
+
color: var(--sl-color-gray-3);
|
|
164
|
+
font-size: var(--sl-text-sm);
|
|
165
|
+
}
|
|
166
|
+
@media (max-width: 640px) {
|
|
167
|
+
.health__metrics {
|
|
168
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
</style>
|