plum-e2e 1.2.4 → 1.3.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/CLAUDE.md +201 -0
- package/README.md +237 -90
- package/backend/_scaffold/utils/browser.ts +5 -2
- package/backend/app.js +9 -1
- package/backend/config/scripts/generate-report.js +34 -73
- package/backend/config/scripts/run-tests.js +7 -3
- package/backend/constants/triggers.js +67 -0
- package/backend/lib/reportFilename.js +37 -0
- package/backend/lib/testChunker.js +73 -0
- package/backend/middleware/auth.js +32 -0
- package/backend/package.json +4 -2
- package/backend/prisma/migrations/20260616000000_add_runners_and_browser/migration.sql +26 -0
- package/backend/prisma/migrations/20260616000001_cron_runner_ids/migration.sql +6 -0
- package/backend/prisma/migrations/20260617000000_cron_enabled/migration.sql +1 -0
- package/backend/prisma/migrations/20260617000001_report_content/migration.sql +8 -0
- package/backend/prisma/schema.prisma +21 -1
- package/backend/routes/cron.routes.js +28 -0
- package/backend/routes/node.routes.js +121 -0
- package/backend/routes/reports.routes.js +23 -20
- package/backend/routes/runners.routes.js +83 -0
- package/backend/scripts/add-local-runner.js +120 -0
- package/backend/scripts/create-test.js +148 -0
- package/backend/server.js +16 -7
- package/backend/services/backupService.js +3 -30
- package/backend/services/cronService.js +220 -36
- package/backend/services/reportService.js +227 -55
- package/backend/services/runnerService.js +179 -0
- package/backend/websockets/socketHandler.js +162 -21
- package/bin/plum.js +132 -19
- package/docker-compose.node.yml +59 -0
- package/docker-compose.yml +2 -0
- package/frontend/package.json +1 -4
- package/frontend/src/app.css +20 -254
- package/frontend/src/app.html +1 -1
- package/frontend/src/lib/api/reports.js +17 -36
- package/frontend/src/lib/api/runners.js +61 -0
- package/frontend/src/lib/api/schedules.js +34 -5
- package/frontend/src/lib/api/settings.js +5 -5
- package/frontend/src/lib/api/tests.js +2 -19
- package/frontend/src/lib/components/icons/BrowserIcon.svelte +75 -0
- package/frontend/src/lib/components/layout/Nav.svelte +42 -47
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +913 -253
- package/frontend/src/lib/components/ui/Badge.svelte +6 -1
- package/frontend/src/lib/components/ui/ConfirmModal.svelte +98 -0
- package/frontend/{tailwind.config.js → src/lib/components/ui/EmptyState.svelte} +27 -8
- package/frontend/{postcss.config.js → src/lib/components/ui/Toast.svelte} +20 -7
- package/frontend/src/lib/constants.js +36 -0
- package/frontend/src/lib/stores/runner.js +23 -12
- package/frontend/src/lib/styles/global.css +176 -0
- package/frontend/src/lib/styles/reset.css +86 -0
- package/frontend/src/lib/styles/tokens.css +90 -0
- package/frontend/src/lib/utils/format.js +46 -0
- package/frontend/src/routes/+page.svelte +16 -35
- package/frontend/src/routes/reports/+page.svelte +84 -167
- package/frontend/src/routes/reports/{[slug] → [id]}/+page.svelte +304 -76
- package/frontend/src/routes/reports/live/+page.svelte +704 -0
- package/frontend/src/routes/scheduled-tests/+page.svelte +328 -88
- package/frontend/src/routes/settings/+page.svelte +774 -127
- package/frontend/static/favicon-32x32.png +0 -0
- package/frontend/static/favicon.ico +0 -0
- package/package.json +1 -1
- package/frontend/static/favicon.png +0 -0
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
import { page } from '$app/stores';
|
|
20
20
|
import { onMount } from 'svelte';
|
|
21
21
|
import { slide } from 'svelte/transition';
|
|
22
|
-
import { fetchReportDetail,
|
|
22
|
+
import { fetchReportDetail, screenshotUrl } from '$lib/api/reports';
|
|
23
|
+
import { isScheduled, triggerLabel, fmtDuration, stagger } from '$lib/utils/format';
|
|
23
24
|
import Badge from '$lib/components/ui/Badge.svelte';
|
|
25
|
+
import BrowserIcon from '$lib/components/icons/BrowserIcon.svelte';
|
|
24
26
|
|
|
25
|
-
const
|
|
26
|
-
const meta = parseReport(fileName);
|
|
27
|
+
const reportId = parseInt($page.params.id, 10);
|
|
27
28
|
|
|
28
29
|
let detail = null;
|
|
29
30
|
let error = null;
|
|
@@ -31,26 +32,18 @@
|
|
|
31
32
|
|
|
32
33
|
onMount(async () => {
|
|
33
34
|
try {
|
|
34
|
-
detail = await fetchReportDetail(
|
|
35
|
-
} catch
|
|
35
|
+
detail = await fetchReportDetail(reportId);
|
|
36
|
+
} catch {
|
|
36
37
|
error = 'Could not load report.';
|
|
37
38
|
}
|
|
38
39
|
});
|
|
39
40
|
|
|
40
41
|
function toggleScenario(id) {
|
|
41
|
-
if (expandedScenarios.has(id))
|
|
42
|
-
|
|
43
|
-
} else {
|
|
44
|
-
expandedScenarios.add(id);
|
|
45
|
-
}
|
|
42
|
+
if (expandedScenarios.has(id)) expandedScenarios.delete(id);
|
|
43
|
+
else expandedScenarios.add(id);
|
|
46
44
|
expandedScenarios = expandedScenarios;
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
function fmtDuration(ms) {
|
|
50
|
-
if (ms >= 1000) return (ms / 1000).toFixed(2) + 's';
|
|
51
|
-
return ms + 'ms';
|
|
52
|
-
}
|
|
53
|
-
|
|
54
47
|
function keywordClass(kw) {
|
|
55
48
|
const k = kw.toLowerCase();
|
|
56
49
|
if (k === 'given') return 'kw-given';
|
|
@@ -66,7 +59,7 @@
|
|
|
66
59
|
function visibleTags(scenario) {
|
|
67
60
|
const testTag = scenarioTestTag(scenario);
|
|
68
61
|
if (testTag) return [testTag];
|
|
69
|
-
return scenario.tags.filter((tag) =>
|
|
62
|
+
return scenario.tags.filter((tag) => !tag.includes('suite'));
|
|
70
63
|
}
|
|
71
64
|
|
|
72
65
|
function worstStatus(scenarios) {
|
|
@@ -80,11 +73,9 @@
|
|
|
80
73
|
|
|
81
74
|
function groupedScenarios(scenarios) {
|
|
82
75
|
const groups = new Map();
|
|
83
|
-
|
|
84
76
|
for (const scenario of scenarios) {
|
|
85
77
|
const testTag = scenarioTestTag(scenario);
|
|
86
78
|
const key = testTag || `${scenario.keyword}:${scenario.name}`;
|
|
87
|
-
|
|
88
79
|
if (!groups.has(key)) {
|
|
89
80
|
groups.set(key, {
|
|
90
81
|
key,
|
|
@@ -95,16 +86,15 @@
|
|
|
95
86
|
status: scenario.status
|
|
96
87
|
});
|
|
97
88
|
}
|
|
98
|
-
|
|
99
89
|
const group = groups.get(key);
|
|
100
90
|
group.scenarios.push(scenario);
|
|
101
91
|
group.duration += scenario.duration;
|
|
102
92
|
group.status = worstStatus(group.scenarios);
|
|
103
93
|
}
|
|
104
|
-
|
|
105
94
|
return Array.from(groups.values());
|
|
106
95
|
}
|
|
107
96
|
|
|
97
|
+
$: overallPass = detail?.status === 'PASS';
|
|
108
98
|
$: passed =
|
|
109
99
|
detail?.features.flatMap((f) => f.scenarios).filter((s) => s.status === 'passed').length ?? 0;
|
|
110
100
|
$: failed =
|
|
@@ -115,7 +105,6 @@
|
|
|
115
105
|
.filter((s) => s.status === 'skipped' || s.status === 'pending').length ?? 0;
|
|
116
106
|
$: totalDuration =
|
|
117
107
|
detail?.features.flatMap((f) => f.scenarios).reduce((s, sc) => s + sc.duration, 0) ?? 0;
|
|
118
|
-
$: overallPass = meta?.status === 'PASS';
|
|
119
108
|
$: groupedFeatures =
|
|
120
109
|
detail?.features.map((feature) => ({
|
|
121
110
|
...feature,
|
|
@@ -147,77 +136,237 @@
|
|
|
147
136
|
<div class="error-state">{error}</div>
|
|
148
137
|
{:else if !detail}
|
|
149
138
|
<div class="loading-state">
|
|
150
|
-
<div class="loading-dots">
|
|
151
|
-
<span></span><span></span><span></span>
|
|
152
|
-
</div>
|
|
139
|
+
<div class="loading-dots"><span></span><span></span><span></span></div>
|
|
153
140
|
</div>
|
|
154
141
|
{:else}
|
|
155
|
-
<!-- Header -->
|
|
156
142
|
<div class="report-header" class:pass={overallPass} class:fail={!overallPass}>
|
|
157
143
|
<div class="header-main">
|
|
158
144
|
<div class="header-status">
|
|
159
|
-
<
|
|
145
|
+
<div class="status-icon-wrap" class:pass-bg={overallPass} class:fail-bg={!overallPass}>
|
|
146
|
+
{#if overallPass}
|
|
147
|
+
<svg
|
|
148
|
+
width="26"
|
|
149
|
+
height="26"
|
|
150
|
+
viewBox="0 0 24 24"
|
|
151
|
+
fill="none"
|
|
152
|
+
stroke="currentColor"
|
|
153
|
+
stroke-width="2.5"
|
|
154
|
+
stroke-linecap="round"
|
|
155
|
+
stroke-linejoin="round"
|
|
156
|
+
>
|
|
157
|
+
<polyline points="20 6 9 17 4 12" />
|
|
158
|
+
</svg>
|
|
159
|
+
{:else}
|
|
160
|
+
<svg
|
|
161
|
+
width="26"
|
|
162
|
+
height="26"
|
|
163
|
+
viewBox="0 0 24 24"
|
|
164
|
+
fill="none"
|
|
165
|
+
stroke="currentColor"
|
|
166
|
+
stroke-width="2.5"
|
|
167
|
+
stroke-linecap="round"
|
|
168
|
+
stroke-linejoin="round"
|
|
169
|
+
>
|
|
170
|
+
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
171
|
+
</svg>
|
|
172
|
+
{/if}
|
|
173
|
+
</div>
|
|
174
|
+
|
|
160
175
|
<div>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
176
|
+
{#if isScheduled(detail.triggerType)}
|
|
177
|
+
<p class="report-task-name">
|
|
178
|
+
<svg
|
|
179
|
+
width="11"
|
|
180
|
+
height="11"
|
|
181
|
+
viewBox="0 0 24 24"
|
|
182
|
+
fill="none"
|
|
183
|
+
stroke="currentColor"
|
|
184
|
+
stroke-width="2"
|
|
185
|
+
stroke-linecap="round"
|
|
186
|
+
stroke-linejoin="round"
|
|
187
|
+
>
|
|
188
|
+
<rect x="3" y="4" width="18" height="18" rx="2" />
|
|
189
|
+
<line x1="16" y1="2" x2="16" y2="6" />
|
|
190
|
+
<line x1="8" y1="2" x2="8" y2="6" />
|
|
191
|
+
<line x1="3" y1="10" x2="21" y2="10" />
|
|
192
|
+
</svg>
|
|
193
|
+
{detail.triggerType}
|
|
169
194
|
</p>
|
|
170
195
|
{/if}
|
|
196
|
+
<h1>{overallPass ? 'Passed' : 'Failed'}</h1>
|
|
197
|
+
<div class="header-meta">
|
|
198
|
+
<span class="mono">{detail.tags}</span>
|
|
199
|
+
<span class="meta-sep">·</span>
|
|
200
|
+
<span>{triggerLabel(detail.triggerType)}</span>
|
|
201
|
+
<span class="meta-sep">·</span>
|
|
202
|
+
<span>{new Date(detail.createdAt).toLocaleString()}</span>
|
|
203
|
+
<span class="meta-sep">·</span>
|
|
204
|
+
<span class="browser-pill">
|
|
205
|
+
<BrowserIcon browser={detail.browser ?? 'chromium'} />
|
|
206
|
+
{detail.browser ?? 'chromium'}
|
|
207
|
+
</span>
|
|
208
|
+
{#if detail.runnerName}
|
|
209
|
+
<span class="meta-sep">·</span>
|
|
210
|
+
<span class="runner-pill">
|
|
211
|
+
<svg
|
|
212
|
+
width="10"
|
|
213
|
+
height="10"
|
|
214
|
+
viewBox="0 0 24 24"
|
|
215
|
+
fill="none"
|
|
216
|
+
stroke="currentColor"
|
|
217
|
+
stroke-width="2"
|
|
218
|
+
stroke-linecap="round"
|
|
219
|
+
>
|
|
220
|
+
<rect x="2" y="3" width="20" height="14" rx="2" />
|
|
221
|
+
<path d="M8 21h8M12 17v4" />
|
|
222
|
+
</svg>
|
|
223
|
+
{detail.runnerName}
|
|
224
|
+
</span>
|
|
225
|
+
{/if}
|
|
226
|
+
</div>
|
|
171
227
|
</div>
|
|
172
228
|
</div>
|
|
173
229
|
|
|
174
230
|
<div class="header-stats">
|
|
175
231
|
<div class="stat">
|
|
176
232
|
<span class="stat-num pass-color">{passed}</span>
|
|
177
|
-
<span class="stat-label">
|
|
233
|
+
<span class="stat-label">
|
|
234
|
+
<svg
|
|
235
|
+
width="10"
|
|
236
|
+
height="10"
|
|
237
|
+
viewBox="0 0 24 24"
|
|
238
|
+
fill="none"
|
|
239
|
+
stroke="currentColor"
|
|
240
|
+
stroke-width="2.5"
|
|
241
|
+
stroke-linecap="round"
|
|
242
|
+
stroke-linejoin="round"
|
|
243
|
+
>
|
|
244
|
+
<polyline points="20 6 9 17 4 12" />
|
|
245
|
+
</svg>
|
|
246
|
+
passed
|
|
247
|
+
</span>
|
|
178
248
|
</div>
|
|
179
249
|
{#if failed > 0}
|
|
180
250
|
<div class="stat">
|
|
181
251
|
<span class="stat-num fail-color">{failed}</span>
|
|
182
|
-
<span class="stat-label">
|
|
252
|
+
<span class="stat-label">
|
|
253
|
+
<svg
|
|
254
|
+
width="10"
|
|
255
|
+
height="10"
|
|
256
|
+
viewBox="0 0 24 24"
|
|
257
|
+
fill="none"
|
|
258
|
+
stroke="currentColor"
|
|
259
|
+
stroke-width="2.5"
|
|
260
|
+
stroke-linecap="round"
|
|
261
|
+
stroke-linejoin="round"
|
|
262
|
+
>
|
|
263
|
+
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
264
|
+
</svg>
|
|
265
|
+
failed
|
|
266
|
+
</span>
|
|
183
267
|
</div>
|
|
184
268
|
{/if}
|
|
185
269
|
{#if skipped > 0}
|
|
186
270
|
<div class="stat">
|
|
187
271
|
<span class="stat-num muted-color">{skipped}</span>
|
|
188
|
-
<span class="stat-label">
|
|
272
|
+
<span class="stat-label">
|
|
273
|
+
<svg
|
|
274
|
+
width="10"
|
|
275
|
+
height="10"
|
|
276
|
+
viewBox="0 0 24 24"
|
|
277
|
+
fill="none"
|
|
278
|
+
stroke="currentColor"
|
|
279
|
+
stroke-width="2.5"
|
|
280
|
+
stroke-linecap="round"
|
|
281
|
+
>
|
|
282
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
283
|
+
</svg>
|
|
284
|
+
skipped
|
|
285
|
+
</span>
|
|
189
286
|
</div>
|
|
190
287
|
{/if}
|
|
191
288
|
<div class="stat">
|
|
192
289
|
<span class="stat-num">{fmtDuration(totalDuration)}</span>
|
|
193
|
-
<span class="stat-label">
|
|
290
|
+
<span class="stat-label">
|
|
291
|
+
<svg
|
|
292
|
+
width="10"
|
|
293
|
+
height="10"
|
|
294
|
+
viewBox="0 0 24 24"
|
|
295
|
+
fill="none"
|
|
296
|
+
stroke="currentColor"
|
|
297
|
+
stroke-width="2"
|
|
298
|
+
stroke-linecap="round"
|
|
299
|
+
stroke-linejoin="round"
|
|
300
|
+
>
|
|
301
|
+
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
|
|
302
|
+
</svg>
|
|
303
|
+
duration
|
|
304
|
+
</span>
|
|
305
|
+
</div>
|
|
306
|
+
<div class="stat">
|
|
307
|
+
<span class="stat-num">{detail.runners}</span>
|
|
308
|
+
<span class="stat-label">
|
|
309
|
+
<svg
|
|
310
|
+
width="10"
|
|
311
|
+
height="10"
|
|
312
|
+
viewBox="0 0 24 24"
|
|
313
|
+
fill="none"
|
|
314
|
+
stroke="currentColor"
|
|
315
|
+
stroke-width="2"
|
|
316
|
+
stroke-linecap="round"
|
|
317
|
+
>
|
|
318
|
+
<rect x="2" y="3" width="20" height="14" rx="2" />
|
|
319
|
+
<path d="M8 21h8M12 17v4" />
|
|
320
|
+
</svg>
|
|
321
|
+
runner{detail.runners !== 1 ? 's' : ''}
|
|
322
|
+
</span>
|
|
194
323
|
</div>
|
|
195
|
-
{#if meta}
|
|
196
|
-
<div class="stat">
|
|
197
|
-
<span class="stat-num">{meta.runners}</span>
|
|
198
|
-
<span class="stat-label">runner{meta.runners !== 1 ? 's' : ''}</span>
|
|
199
|
-
</div>
|
|
200
|
-
{/if}
|
|
201
324
|
</div>
|
|
202
325
|
</div>
|
|
203
326
|
</div>
|
|
204
327
|
|
|
205
|
-
<!-- Features -->
|
|
206
328
|
{#each groupedFeatures as feature, fi}
|
|
207
|
-
<div
|
|
329
|
+
<div
|
|
330
|
+
class="feature"
|
|
331
|
+
class:feature-pass={feature.status === 'passed'}
|
|
332
|
+
class:feature-fail={feature.status !== 'passed'}
|
|
333
|
+
style={stagger(fi, 60)}
|
|
334
|
+
>
|
|
208
335
|
<div class="feature-header">
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
336
|
+
<h2 class="feature-name">
|
|
337
|
+
<svg
|
|
338
|
+
width="13"
|
|
339
|
+
height="13"
|
|
340
|
+
viewBox="0 0 24 24"
|
|
341
|
+
fill="none"
|
|
342
|
+
stroke="currentColor"
|
|
343
|
+
stroke-width="2"
|
|
344
|
+
stroke-linecap="round"
|
|
345
|
+
stroke-linejoin="round"
|
|
346
|
+
class="feature-icon"
|
|
347
|
+
>
|
|
348
|
+
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
|
349
|
+
<polyline points="14 2 14 8 20 8" />
|
|
350
|
+
</svg>
|
|
351
|
+
{feature.name}
|
|
352
|
+
</h2>
|
|
353
|
+
<div class="feature-right">
|
|
354
|
+
<span class="feature-file">{feature.uri}</span>
|
|
355
|
+
<Badge variant={feature.status === 'passed' ? 'pass' : 'fail'}>
|
|
356
|
+
{feature.status}
|
|
357
|
+
</Badge>
|
|
358
|
+
</div>
|
|
214
359
|
</div>
|
|
215
360
|
|
|
216
361
|
<div class="scenarios">
|
|
217
362
|
{#each feature.scenarioGroups as group, si}
|
|
218
363
|
{@const scenId = `${fi}-${group.key}`}
|
|
219
364
|
{@const open = expandedScenarios.has(scenId)}
|
|
220
|
-
<div
|
|
365
|
+
<div
|
|
366
|
+
class="scenario"
|
|
367
|
+
class:scenario-fail={group.status === 'failed'}
|
|
368
|
+
style={stagger(fi * 5 + si, 40)}
|
|
369
|
+
>
|
|
221
370
|
<button class="scenario-header" on:click={() => toggleScenario(scenId)}>
|
|
222
371
|
<span
|
|
223
372
|
class="scenario-status-dot"
|
|
@@ -296,7 +445,7 @@
|
|
|
296
445
|
<summary class="screenshot-toggle">Screenshot</summary>
|
|
297
446
|
<img
|
|
298
447
|
class="screenshot"
|
|
299
|
-
src=
|
|
448
|
+
src={screenshotUrl(step.screenshot)}
|
|
300
449
|
alt="Failure screenshot"
|
|
301
450
|
/>
|
|
302
451
|
</details>
|
|
@@ -332,11 +481,10 @@
|
|
|
332
481
|
color: var(--text);
|
|
333
482
|
}
|
|
334
483
|
|
|
335
|
-
/* ── Header ── */
|
|
336
484
|
.report-header {
|
|
337
485
|
border-radius: var(--radius-lg);
|
|
338
486
|
border: 1px solid var(--border);
|
|
339
|
-
border-
|
|
487
|
+
border-top-width: 3px;
|
|
340
488
|
padding: 1.5rem;
|
|
341
489
|
margin-bottom: 2rem;
|
|
342
490
|
background: var(--bg-elevated);
|
|
@@ -344,10 +492,10 @@
|
|
|
344
492
|
}
|
|
345
493
|
|
|
346
494
|
.report-header.pass {
|
|
347
|
-
border-
|
|
495
|
+
border-top-color: var(--pass);
|
|
348
496
|
}
|
|
349
497
|
.report-header.fail {
|
|
350
|
-
border-
|
|
498
|
+
border-top-color: var(--fail);
|
|
351
499
|
}
|
|
352
500
|
|
|
353
501
|
.header-main {
|
|
@@ -364,19 +512,37 @@
|
|
|
364
512
|
gap: 1rem;
|
|
365
513
|
}
|
|
366
514
|
|
|
367
|
-
.status-icon {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
515
|
+
.status-icon-wrap {
|
|
516
|
+
width: 52px;
|
|
517
|
+
height: 52px;
|
|
518
|
+
border-radius: 50%;
|
|
519
|
+
display: flex;
|
|
520
|
+
align-items: center;
|
|
521
|
+
justify-content: center;
|
|
522
|
+
flex-shrink: 0;
|
|
371
523
|
}
|
|
372
524
|
|
|
373
|
-
.
|
|
525
|
+
.status-icon-wrap.pass-bg {
|
|
526
|
+
background: var(--pass-soft);
|
|
374
527
|
color: var(--pass);
|
|
375
528
|
}
|
|
376
|
-
.
|
|
529
|
+
.status-icon-wrap.fail-bg {
|
|
530
|
+
background: var(--fail-soft);
|
|
377
531
|
color: var(--fail);
|
|
378
532
|
}
|
|
379
533
|
|
|
534
|
+
.report-task-name {
|
|
535
|
+
display: inline-flex;
|
|
536
|
+
align-items: center;
|
|
537
|
+
gap: 0.35rem;
|
|
538
|
+
font-size: 0.7rem;
|
|
539
|
+
font-weight: 600;
|
|
540
|
+
text-transform: uppercase;
|
|
541
|
+
letter-spacing: 0.1em;
|
|
542
|
+
color: var(--warn);
|
|
543
|
+
margin-bottom: 0.35rem;
|
|
544
|
+
}
|
|
545
|
+
|
|
380
546
|
h1 {
|
|
381
547
|
font-size: 2rem;
|
|
382
548
|
margin-bottom: 0.2rem;
|
|
@@ -387,7 +553,12 @@
|
|
|
387
553
|
color: var(--text-muted);
|
|
388
554
|
display: flex;
|
|
389
555
|
align-items: center;
|
|
390
|
-
|
|
556
|
+
flex-wrap: wrap;
|
|
557
|
+
gap: 0.35rem;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.meta-sep {
|
|
561
|
+
opacity: 0.4;
|
|
391
562
|
}
|
|
392
563
|
|
|
393
564
|
.mono {
|
|
@@ -395,6 +566,29 @@
|
|
|
395
566
|
font-size: 0.78rem;
|
|
396
567
|
}
|
|
397
568
|
|
|
569
|
+
.browser-pill {
|
|
570
|
+
display: inline-flex;
|
|
571
|
+
align-items: center;
|
|
572
|
+
gap: 0.3rem;
|
|
573
|
+
font-family: 'JetBrains Mono', monospace;
|
|
574
|
+
font-size: 0.68rem;
|
|
575
|
+
font-weight: 500;
|
|
576
|
+
background: var(--bg-subtle);
|
|
577
|
+
border: 1px solid var(--border);
|
|
578
|
+
border-radius: 100px;
|
|
579
|
+
padding: 0.1rem 0.45rem;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.runner-pill {
|
|
583
|
+
display: inline-flex;
|
|
584
|
+
align-items: center;
|
|
585
|
+
gap: 0.3rem;
|
|
586
|
+
font-family: 'JetBrains Mono', monospace;
|
|
587
|
+
font-size: 0.68rem;
|
|
588
|
+
font-weight: 500;
|
|
589
|
+
color: var(--text-muted);
|
|
590
|
+
}
|
|
591
|
+
|
|
398
592
|
.header-stats {
|
|
399
593
|
display: flex;
|
|
400
594
|
gap: 1.5rem;
|
|
@@ -406,7 +600,7 @@
|
|
|
406
600
|
display: flex;
|
|
407
601
|
flex-direction: column;
|
|
408
602
|
align-items: center;
|
|
409
|
-
gap: 0.
|
|
603
|
+
gap: 0.15rem;
|
|
410
604
|
}
|
|
411
605
|
|
|
412
606
|
.stat-num {
|
|
@@ -418,6 +612,9 @@
|
|
|
418
612
|
}
|
|
419
613
|
|
|
420
614
|
.stat-label {
|
|
615
|
+
display: inline-flex;
|
|
616
|
+
align-items: center;
|
|
617
|
+
gap: 0.25rem;
|
|
421
618
|
font-size: 0.68rem;
|
|
422
619
|
color: var(--text-muted);
|
|
423
620
|
letter-spacing: 0.06em;
|
|
@@ -434,37 +631,67 @@
|
|
|
434
631
|
color: var(--text-muted);
|
|
435
632
|
}
|
|
436
633
|
|
|
437
|
-
/* ── Features ── */
|
|
438
634
|
.feature {
|
|
439
|
-
margin-bottom: 1.
|
|
635
|
+
margin-bottom: 1.25rem;
|
|
636
|
+
background: var(--bg-elevated);
|
|
637
|
+
border: 1px solid var(--border);
|
|
638
|
+
border-top-width: 3px;
|
|
639
|
+
border-radius: var(--radius-lg);
|
|
640
|
+
overflow: hidden;
|
|
440
641
|
animation: fadeUp 0.35s var(--ease-out) both;
|
|
441
642
|
}
|
|
442
643
|
|
|
644
|
+
.feature-pass {
|
|
645
|
+
border-top-color: var(--pass);
|
|
646
|
+
}
|
|
647
|
+
.feature-fail {
|
|
648
|
+
border-top-color: var(--fail);
|
|
649
|
+
}
|
|
650
|
+
|
|
443
651
|
.feature-header {
|
|
444
652
|
display: flex;
|
|
445
653
|
align-items: center;
|
|
446
654
|
gap: 0.75rem;
|
|
447
|
-
|
|
655
|
+
padding: 0.875rem 1.25rem;
|
|
656
|
+
border-bottom: 1px solid var(--border);
|
|
657
|
+
background: var(--bg-subtle);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.feature-icon {
|
|
661
|
+
flex-shrink: 0;
|
|
662
|
+
color: var(--text-muted);
|
|
663
|
+
opacity: 0.7;
|
|
448
664
|
}
|
|
449
665
|
|
|
450
666
|
.feature-name {
|
|
451
|
-
|
|
667
|
+
display: flex;
|
|
668
|
+
align-items: center;
|
|
669
|
+
gap: 0.5rem;
|
|
670
|
+
font-size: 0.9375rem;
|
|
452
671
|
font-family: var(--font-display);
|
|
453
672
|
font-weight: 400;
|
|
673
|
+
flex: 1;
|
|
674
|
+
min-width: 0;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.feature-right {
|
|
678
|
+
display: flex;
|
|
679
|
+
align-items: center;
|
|
680
|
+
gap: 0.75rem;
|
|
681
|
+
flex-shrink: 0;
|
|
454
682
|
}
|
|
455
683
|
|
|
456
684
|
.feature-file {
|
|
457
685
|
font-family: 'JetBrains Mono', monospace;
|
|
458
686
|
font-size: 0.72rem;
|
|
459
687
|
color: var(--text-muted);
|
|
460
|
-
margin-left: auto;
|
|
461
688
|
}
|
|
462
689
|
|
|
463
|
-
/* ── Scenarios ── */
|
|
464
690
|
.scenarios {
|
|
465
691
|
display: flex;
|
|
466
692
|
flex-direction: column;
|
|
467
|
-
|
|
693
|
+
padding: 0.5rem;
|
|
694
|
+
gap: 0.25rem;
|
|
468
695
|
}
|
|
469
696
|
|
|
470
697
|
.scenario {
|
|
@@ -475,6 +702,11 @@
|
|
|
475
702
|
animation: fadeUp 0.3s var(--ease-out) both;
|
|
476
703
|
}
|
|
477
704
|
|
|
705
|
+
.scenario.scenario-fail {
|
|
706
|
+
border-left-width: 3px;
|
|
707
|
+
border-left-color: var(--fail);
|
|
708
|
+
}
|
|
709
|
+
|
|
478
710
|
.scenario-header {
|
|
479
711
|
display: flex;
|
|
480
712
|
align-items: center;
|
|
@@ -567,7 +799,6 @@
|
|
|
567
799
|
transform: rotate(90deg);
|
|
568
800
|
}
|
|
569
801
|
|
|
570
|
-
/* ── Steps ── */
|
|
571
802
|
.steps {
|
|
572
803
|
border-top: 1px solid var(--border);
|
|
573
804
|
background: var(--bg-subtle);
|
|
@@ -598,11 +829,9 @@
|
|
|
598
829
|
.step {
|
|
599
830
|
padding: 0.375rem 1rem 0.375rem 1.25rem;
|
|
600
831
|
}
|
|
601
|
-
|
|
602
832
|
.step-fail {
|
|
603
833
|
background: color-mix(in srgb, var(--fail-soft) 40%, transparent);
|
|
604
834
|
}
|
|
605
|
-
|
|
606
835
|
.step-skip {
|
|
607
836
|
opacity: 0.5;
|
|
608
837
|
}
|
|
@@ -702,7 +931,6 @@
|
|
|
702
931
|
border: 1px solid var(--border);
|
|
703
932
|
}
|
|
704
933
|
|
|
705
|
-
/* ── Loading / error ── */
|
|
706
934
|
.loading-state {
|
|
707
935
|
display: flex;
|
|
708
936
|
justify-content: center;
|