plum-e2e 1.2.4 → 1.3.1
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 +245 -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 +13 -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 +160 -31
- 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 +325 -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';
|
|
@@ -63,10 +56,18 @@
|
|
|
63
56
|
return scenario.tags.find((tag) => /^@test[\w-]*/i.test(tag));
|
|
64
57
|
}
|
|
65
58
|
|
|
59
|
+
function featureSuiteTag(feature) {
|
|
60
|
+
for (const scenario of feature.scenarios) {
|
|
61
|
+
const suiteTag = scenario.tags.find((tag) => /suite/i.test(tag));
|
|
62
|
+
if (suiteTag) return suiteTag;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
66
67
|
function visibleTags(scenario) {
|
|
67
68
|
const testTag = scenarioTestTag(scenario);
|
|
68
69
|
if (testTag) return [testTag];
|
|
69
|
-
return scenario.tags.filter((tag) =>
|
|
70
|
+
return scenario.tags.filter((tag) => !tag.includes('suite'));
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
function worstStatus(scenarios) {
|
|
@@ -80,11 +81,9 @@
|
|
|
80
81
|
|
|
81
82
|
function groupedScenarios(scenarios) {
|
|
82
83
|
const groups = new Map();
|
|
83
|
-
|
|
84
84
|
for (const scenario of scenarios) {
|
|
85
85
|
const testTag = scenarioTestTag(scenario);
|
|
86
86
|
const key = testTag || `${scenario.keyword}:${scenario.name}`;
|
|
87
|
-
|
|
88
87
|
if (!groups.has(key)) {
|
|
89
88
|
groups.set(key, {
|
|
90
89
|
key,
|
|
@@ -95,16 +94,15 @@
|
|
|
95
94
|
status: scenario.status
|
|
96
95
|
});
|
|
97
96
|
}
|
|
98
|
-
|
|
99
97
|
const group = groups.get(key);
|
|
100
98
|
group.scenarios.push(scenario);
|
|
101
99
|
group.duration += scenario.duration;
|
|
102
100
|
group.status = worstStatus(group.scenarios);
|
|
103
101
|
}
|
|
104
|
-
|
|
105
102
|
return Array.from(groups.values());
|
|
106
103
|
}
|
|
107
104
|
|
|
105
|
+
$: overallPass = detail?.status === 'PASS';
|
|
108
106
|
$: passed =
|
|
109
107
|
detail?.features.flatMap((f) => f.scenarios).filter((s) => s.status === 'passed').length ?? 0;
|
|
110
108
|
$: failed =
|
|
@@ -115,7 +113,6 @@
|
|
|
115
113
|
.filter((s) => s.status === 'skipped' || s.status === 'pending').length ?? 0;
|
|
116
114
|
$: totalDuration =
|
|
117
115
|
detail?.features.flatMap((f) => f.scenarios).reduce((s, sc) => s + sc.duration, 0) ?? 0;
|
|
118
|
-
$: overallPass = meta?.status === 'PASS';
|
|
119
116
|
$: groupedFeatures =
|
|
120
117
|
detail?.features.map((feature) => ({
|
|
121
118
|
...feature,
|
|
@@ -147,77 +144,240 @@
|
|
|
147
144
|
<div class="error-state">{error}</div>
|
|
148
145
|
{:else if !detail}
|
|
149
146
|
<div class="loading-state">
|
|
150
|
-
<div class="loading-dots">
|
|
151
|
-
<span></span><span></span><span></span>
|
|
152
|
-
</div>
|
|
147
|
+
<div class="loading-dots"><span></span><span></span><span></span></div>
|
|
153
148
|
</div>
|
|
154
149
|
{:else}
|
|
155
|
-
<!-- Header -->
|
|
156
150
|
<div class="report-header" class:pass={overallPass} class:fail={!overallPass}>
|
|
157
151
|
<div class="header-main">
|
|
158
152
|
<div class="header-status">
|
|
159
|
-
<
|
|
153
|
+
<div class="status-icon-wrap" class:pass-bg={overallPass} class:fail-bg={!overallPass}>
|
|
154
|
+
{#if overallPass}
|
|
155
|
+
<svg
|
|
156
|
+
width="26"
|
|
157
|
+
height="26"
|
|
158
|
+
viewBox="0 0 24 24"
|
|
159
|
+
fill="none"
|
|
160
|
+
stroke="currentColor"
|
|
161
|
+
stroke-width="2.5"
|
|
162
|
+
stroke-linecap="round"
|
|
163
|
+
stroke-linejoin="round"
|
|
164
|
+
>
|
|
165
|
+
<polyline points="20 6 9 17 4 12" />
|
|
166
|
+
</svg>
|
|
167
|
+
{:else}
|
|
168
|
+
<svg
|
|
169
|
+
width="26"
|
|
170
|
+
height="26"
|
|
171
|
+
viewBox="0 0 24 24"
|
|
172
|
+
fill="none"
|
|
173
|
+
stroke="currentColor"
|
|
174
|
+
stroke-width="2.5"
|
|
175
|
+
stroke-linecap="round"
|
|
176
|
+
stroke-linejoin="round"
|
|
177
|
+
>
|
|
178
|
+
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
179
|
+
</svg>
|
|
180
|
+
{/if}
|
|
181
|
+
</div>
|
|
182
|
+
|
|
160
183
|
<div>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
184
|
+
{#if isScheduled(detail.triggerType)}
|
|
185
|
+
<p class="report-task-name">
|
|
186
|
+
<svg
|
|
187
|
+
width="11"
|
|
188
|
+
height="11"
|
|
189
|
+
viewBox="0 0 24 24"
|
|
190
|
+
fill="none"
|
|
191
|
+
stroke="currentColor"
|
|
192
|
+
stroke-width="2"
|
|
193
|
+
stroke-linecap="round"
|
|
194
|
+
stroke-linejoin="round"
|
|
195
|
+
>
|
|
196
|
+
<rect x="3" y="4" width="18" height="18" rx="2" />
|
|
197
|
+
<line x1="16" y1="2" x2="16" y2="6" />
|
|
198
|
+
<line x1="8" y1="2" x2="8" y2="6" />
|
|
199
|
+
<line x1="3" y1="10" x2="21" y2="10" />
|
|
200
|
+
</svg>
|
|
201
|
+
{detail.triggerType}
|
|
169
202
|
</p>
|
|
170
203
|
{/if}
|
|
204
|
+
<h1>{overallPass ? 'Passed' : 'Failed'}</h1>
|
|
205
|
+
<div class="header-meta">
|
|
206
|
+
<span class="mono">{detail.tags}</span>
|
|
207
|
+
<span class="meta-sep">·</span>
|
|
208
|
+
<span>{triggerLabel(detail.triggerType)}</span>
|
|
209
|
+
<span class="meta-sep">·</span>
|
|
210
|
+
<span>{new Date(detail.createdAt).toLocaleString()}</span>
|
|
211
|
+
<span class="meta-sep">·</span>
|
|
212
|
+
<span class="browser-pill">
|
|
213
|
+
<BrowserIcon browser={detail.browser ?? 'chromium'} />
|
|
214
|
+
{detail.browser ?? 'chromium'}
|
|
215
|
+
</span>
|
|
216
|
+
{#if detail.runnerName}
|
|
217
|
+
<span class="meta-sep">·</span>
|
|
218
|
+
<span class="runner-pill">
|
|
219
|
+
<svg
|
|
220
|
+
width="10"
|
|
221
|
+
height="10"
|
|
222
|
+
viewBox="0 0 24 24"
|
|
223
|
+
fill="none"
|
|
224
|
+
stroke="currentColor"
|
|
225
|
+
stroke-width="2"
|
|
226
|
+
stroke-linecap="round"
|
|
227
|
+
>
|
|
228
|
+
<rect x="2" y="3" width="20" height="14" rx="2" />
|
|
229
|
+
<path d="M8 21h8M12 17v4" />
|
|
230
|
+
</svg>
|
|
231
|
+
{detail.runnerName}
|
|
232
|
+
</span>
|
|
233
|
+
{/if}
|
|
234
|
+
</div>
|
|
171
235
|
</div>
|
|
172
236
|
</div>
|
|
173
237
|
|
|
174
238
|
<div class="header-stats">
|
|
175
239
|
<div class="stat">
|
|
176
240
|
<span class="stat-num pass-color">{passed}</span>
|
|
177
|
-
<span class="stat-label">
|
|
241
|
+
<span class="stat-label">
|
|
242
|
+
<svg
|
|
243
|
+
width="10"
|
|
244
|
+
height="10"
|
|
245
|
+
viewBox="0 0 24 24"
|
|
246
|
+
fill="none"
|
|
247
|
+
stroke="currentColor"
|
|
248
|
+
stroke-width="2.5"
|
|
249
|
+
stroke-linecap="round"
|
|
250
|
+
stroke-linejoin="round"
|
|
251
|
+
>
|
|
252
|
+
<polyline points="20 6 9 17 4 12" />
|
|
253
|
+
</svg>
|
|
254
|
+
passed
|
|
255
|
+
</span>
|
|
178
256
|
</div>
|
|
179
257
|
{#if failed > 0}
|
|
180
258
|
<div class="stat">
|
|
181
259
|
<span class="stat-num fail-color">{failed}</span>
|
|
182
|
-
<span class="stat-label">
|
|
260
|
+
<span class="stat-label">
|
|
261
|
+
<svg
|
|
262
|
+
width="10"
|
|
263
|
+
height="10"
|
|
264
|
+
viewBox="0 0 24 24"
|
|
265
|
+
fill="none"
|
|
266
|
+
stroke="currentColor"
|
|
267
|
+
stroke-width="2.5"
|
|
268
|
+
stroke-linecap="round"
|
|
269
|
+
stroke-linejoin="round"
|
|
270
|
+
>
|
|
271
|
+
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
272
|
+
</svg>
|
|
273
|
+
failed
|
|
274
|
+
</span>
|
|
183
275
|
</div>
|
|
184
276
|
{/if}
|
|
185
277
|
{#if skipped > 0}
|
|
186
278
|
<div class="stat">
|
|
187
279
|
<span class="stat-num muted-color">{skipped}</span>
|
|
188
|
-
<span class="stat-label">
|
|
280
|
+
<span class="stat-label">
|
|
281
|
+
<svg
|
|
282
|
+
width="10"
|
|
283
|
+
height="10"
|
|
284
|
+
viewBox="0 0 24 24"
|
|
285
|
+
fill="none"
|
|
286
|
+
stroke="currentColor"
|
|
287
|
+
stroke-width="2.5"
|
|
288
|
+
stroke-linecap="round"
|
|
289
|
+
>
|
|
290
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
291
|
+
</svg>
|
|
292
|
+
skipped
|
|
293
|
+
</span>
|
|
189
294
|
</div>
|
|
190
295
|
{/if}
|
|
191
296
|
<div class="stat">
|
|
192
297
|
<span class="stat-num">{fmtDuration(totalDuration)}</span>
|
|
193
|
-
<span class="stat-label">
|
|
298
|
+
<span class="stat-label">
|
|
299
|
+
<svg
|
|
300
|
+
width="10"
|
|
301
|
+
height="10"
|
|
302
|
+
viewBox="0 0 24 24"
|
|
303
|
+
fill="none"
|
|
304
|
+
stroke="currentColor"
|
|
305
|
+
stroke-width="2"
|
|
306
|
+
stroke-linecap="round"
|
|
307
|
+
stroke-linejoin="round"
|
|
308
|
+
>
|
|
309
|
+
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
|
|
310
|
+
</svg>
|
|
311
|
+
duration
|
|
312
|
+
</span>
|
|
313
|
+
</div>
|
|
314
|
+
<div class="stat">
|
|
315
|
+
<span class="stat-num">{detail.runners}</span>
|
|
316
|
+
<span class="stat-label">
|
|
317
|
+
<svg
|
|
318
|
+
width="10"
|
|
319
|
+
height="10"
|
|
320
|
+
viewBox="0 0 24 24"
|
|
321
|
+
fill="none"
|
|
322
|
+
stroke="currentColor"
|
|
323
|
+
stroke-width="2"
|
|
324
|
+
stroke-linecap="round"
|
|
325
|
+
>
|
|
326
|
+
<rect x="2" y="3" width="20" height="14" rx="2" />
|
|
327
|
+
<path d="M8 21h8M12 17v4" />
|
|
328
|
+
</svg>
|
|
329
|
+
runner{detail.runners !== 1 ? 's' : ''}
|
|
330
|
+
</span>
|
|
194
331
|
</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
332
|
</div>
|
|
202
333
|
</div>
|
|
203
334
|
</div>
|
|
204
335
|
|
|
205
|
-
<!-- Features -->
|
|
206
336
|
{#each groupedFeatures as feature, fi}
|
|
207
|
-
<div
|
|
337
|
+
<div
|
|
338
|
+
class="feature"
|
|
339
|
+
class:feature-pass={feature.status === 'passed'}
|
|
340
|
+
class:feature-fail={feature.status !== 'passed'}
|
|
341
|
+
style={stagger(fi, 60)}
|
|
342
|
+
>
|
|
208
343
|
<div class="feature-header">
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
344
|
+
<h2 class="feature-name">
|
|
345
|
+
<svg
|
|
346
|
+
width="13"
|
|
347
|
+
height="13"
|
|
348
|
+
viewBox="0 0 24 24"
|
|
349
|
+
fill="none"
|
|
350
|
+
stroke="currentColor"
|
|
351
|
+
stroke-width="2"
|
|
352
|
+
stroke-linecap="round"
|
|
353
|
+
stroke-linejoin="round"
|
|
354
|
+
class="feature-icon"
|
|
355
|
+
>
|
|
356
|
+
<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" />
|
|
357
|
+
<polyline points="14 2 14 8 20 8" />
|
|
358
|
+
</svg>
|
|
359
|
+
{feature.name}
|
|
360
|
+
{#if featureSuiteTag(feature)}
|
|
361
|
+
<span class="suite-tag">{featureSuiteTag(feature)}</span>
|
|
362
|
+
{/if}
|
|
363
|
+
</h2>
|
|
364
|
+
<div class="feature-right">
|
|
365
|
+
<span class="feature-file">{feature.uri}</span>
|
|
366
|
+
<Badge variant={feature.status === 'passed' ? 'pass' : 'fail'}>
|
|
367
|
+
{feature.status}
|
|
368
|
+
</Badge>
|
|
369
|
+
</div>
|
|
214
370
|
</div>
|
|
215
371
|
|
|
216
372
|
<div class="scenarios">
|
|
217
373
|
{#each feature.scenarioGroups as group, si}
|
|
218
374
|
{@const scenId = `${fi}-${group.key}`}
|
|
219
375
|
{@const open = expandedScenarios.has(scenId)}
|
|
220
|
-
<div
|
|
376
|
+
<div
|
|
377
|
+
class="scenario"
|
|
378
|
+
class:scenario-fail={group.status === 'failed'}
|
|
379
|
+
style={stagger(fi * 5 + si, 40)}
|
|
380
|
+
>
|
|
221
381
|
<button class="scenario-header" on:click={() => toggleScenario(scenId)}>
|
|
222
382
|
<span
|
|
223
383
|
class="scenario-status-dot"
|
|
@@ -296,7 +456,7 @@
|
|
|
296
456
|
<summary class="screenshot-toggle">Screenshot</summary>
|
|
297
457
|
<img
|
|
298
458
|
class="screenshot"
|
|
299
|
-
src=
|
|
459
|
+
src={screenshotUrl(step.screenshot)}
|
|
300
460
|
alt="Failure screenshot"
|
|
301
461
|
/>
|
|
302
462
|
</details>
|
|
@@ -332,11 +492,10 @@
|
|
|
332
492
|
color: var(--text);
|
|
333
493
|
}
|
|
334
494
|
|
|
335
|
-
/* ── Header ── */
|
|
336
495
|
.report-header {
|
|
337
496
|
border-radius: var(--radius-lg);
|
|
338
497
|
border: 1px solid var(--border);
|
|
339
|
-
border-
|
|
498
|
+
border-top-width: 3px;
|
|
340
499
|
padding: 1.5rem;
|
|
341
500
|
margin-bottom: 2rem;
|
|
342
501
|
background: var(--bg-elevated);
|
|
@@ -344,10 +503,10 @@
|
|
|
344
503
|
}
|
|
345
504
|
|
|
346
505
|
.report-header.pass {
|
|
347
|
-
border-
|
|
506
|
+
border-top-color: var(--pass);
|
|
348
507
|
}
|
|
349
508
|
.report-header.fail {
|
|
350
|
-
border-
|
|
509
|
+
border-top-color: var(--fail);
|
|
351
510
|
}
|
|
352
511
|
|
|
353
512
|
.header-main {
|
|
@@ -364,19 +523,37 @@
|
|
|
364
523
|
gap: 1rem;
|
|
365
524
|
}
|
|
366
525
|
|
|
367
|
-
.status-icon {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
526
|
+
.status-icon-wrap {
|
|
527
|
+
width: 52px;
|
|
528
|
+
height: 52px;
|
|
529
|
+
border-radius: 50%;
|
|
530
|
+
display: flex;
|
|
531
|
+
align-items: center;
|
|
532
|
+
justify-content: center;
|
|
533
|
+
flex-shrink: 0;
|
|
371
534
|
}
|
|
372
535
|
|
|
373
|
-
.
|
|
536
|
+
.status-icon-wrap.pass-bg {
|
|
537
|
+
background: var(--pass-soft);
|
|
374
538
|
color: var(--pass);
|
|
375
539
|
}
|
|
376
|
-
.
|
|
540
|
+
.status-icon-wrap.fail-bg {
|
|
541
|
+
background: var(--fail-soft);
|
|
377
542
|
color: var(--fail);
|
|
378
543
|
}
|
|
379
544
|
|
|
545
|
+
.report-task-name {
|
|
546
|
+
display: inline-flex;
|
|
547
|
+
align-items: center;
|
|
548
|
+
gap: 0.35rem;
|
|
549
|
+
font-size: 0.7rem;
|
|
550
|
+
font-weight: 600;
|
|
551
|
+
text-transform: uppercase;
|
|
552
|
+
letter-spacing: 0.1em;
|
|
553
|
+
color: var(--warn);
|
|
554
|
+
margin-bottom: 0.35rem;
|
|
555
|
+
}
|
|
556
|
+
|
|
380
557
|
h1 {
|
|
381
558
|
font-size: 2rem;
|
|
382
559
|
margin-bottom: 0.2rem;
|
|
@@ -387,7 +564,12 @@
|
|
|
387
564
|
color: var(--text-muted);
|
|
388
565
|
display: flex;
|
|
389
566
|
align-items: center;
|
|
390
|
-
|
|
567
|
+
flex-wrap: wrap;
|
|
568
|
+
gap: 0.35rem;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.meta-sep {
|
|
572
|
+
opacity: 0.4;
|
|
391
573
|
}
|
|
392
574
|
|
|
393
575
|
.mono {
|
|
@@ -395,6 +577,29 @@
|
|
|
395
577
|
font-size: 0.78rem;
|
|
396
578
|
}
|
|
397
579
|
|
|
580
|
+
.browser-pill {
|
|
581
|
+
display: inline-flex;
|
|
582
|
+
align-items: center;
|
|
583
|
+
gap: 0.3rem;
|
|
584
|
+
font-family: 'JetBrains Mono', monospace;
|
|
585
|
+
font-size: 0.68rem;
|
|
586
|
+
font-weight: 500;
|
|
587
|
+
background: var(--bg-subtle);
|
|
588
|
+
border: 1px solid var(--border);
|
|
589
|
+
border-radius: 100px;
|
|
590
|
+
padding: 0.1rem 0.45rem;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.runner-pill {
|
|
594
|
+
display: inline-flex;
|
|
595
|
+
align-items: center;
|
|
596
|
+
gap: 0.3rem;
|
|
597
|
+
font-family: 'JetBrains Mono', monospace;
|
|
598
|
+
font-size: 0.68rem;
|
|
599
|
+
font-weight: 500;
|
|
600
|
+
color: var(--text-muted);
|
|
601
|
+
}
|
|
602
|
+
|
|
398
603
|
.header-stats {
|
|
399
604
|
display: flex;
|
|
400
605
|
gap: 1.5rem;
|
|
@@ -406,7 +611,7 @@
|
|
|
406
611
|
display: flex;
|
|
407
612
|
flex-direction: column;
|
|
408
613
|
align-items: center;
|
|
409
|
-
gap: 0.
|
|
614
|
+
gap: 0.15rem;
|
|
410
615
|
}
|
|
411
616
|
|
|
412
617
|
.stat-num {
|
|
@@ -418,6 +623,9 @@
|
|
|
418
623
|
}
|
|
419
624
|
|
|
420
625
|
.stat-label {
|
|
626
|
+
display: inline-flex;
|
|
627
|
+
align-items: center;
|
|
628
|
+
gap: 0.25rem;
|
|
421
629
|
font-size: 0.68rem;
|
|
422
630
|
color: var(--text-muted);
|
|
423
631
|
letter-spacing: 0.06em;
|
|
@@ -434,37 +642,77 @@
|
|
|
434
642
|
color: var(--text-muted);
|
|
435
643
|
}
|
|
436
644
|
|
|
437
|
-
/* ── Features ── */
|
|
438
645
|
.feature {
|
|
439
|
-
margin-bottom: 1.
|
|
646
|
+
margin-bottom: 1.25rem;
|
|
647
|
+
background: var(--bg-elevated);
|
|
648
|
+
border: 1px solid var(--border);
|
|
649
|
+
border-top-width: 3px;
|
|
650
|
+
border-radius: var(--radius-lg);
|
|
651
|
+
overflow: hidden;
|
|
440
652
|
animation: fadeUp 0.35s var(--ease-out) both;
|
|
441
653
|
}
|
|
442
654
|
|
|
655
|
+
.feature-pass {
|
|
656
|
+
border-top-color: var(--pass);
|
|
657
|
+
}
|
|
658
|
+
.feature-fail {
|
|
659
|
+
border-top-color: var(--fail);
|
|
660
|
+
}
|
|
661
|
+
|
|
443
662
|
.feature-header {
|
|
444
663
|
display: flex;
|
|
445
664
|
align-items: center;
|
|
446
665
|
gap: 0.75rem;
|
|
447
|
-
|
|
666
|
+
padding: 0.875rem 1.25rem;
|
|
667
|
+
border-bottom: 1px solid var(--border);
|
|
668
|
+
background: var(--bg-subtle);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.feature-icon {
|
|
672
|
+
flex-shrink: 0;
|
|
673
|
+
color: var(--text-muted);
|
|
674
|
+
opacity: 0.7;
|
|
448
675
|
}
|
|
449
676
|
|
|
450
677
|
.feature-name {
|
|
451
|
-
|
|
678
|
+
display: flex;
|
|
679
|
+
align-items: center;
|
|
680
|
+
gap: 0.5rem;
|
|
681
|
+
font-size: 0.9375rem;
|
|
452
682
|
font-family: var(--font-display);
|
|
453
683
|
font-weight: 400;
|
|
684
|
+
flex: 1;
|
|
685
|
+
min-width: 0;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.feature-right {
|
|
689
|
+
display: flex;
|
|
690
|
+
align-items: center;
|
|
691
|
+
gap: 0.75rem;
|
|
692
|
+
flex-shrink: 0;
|
|
454
693
|
}
|
|
455
694
|
|
|
456
695
|
.feature-file {
|
|
457
696
|
font-family: 'JetBrains Mono', monospace;
|
|
458
697
|
font-size: 0.72rem;
|
|
459
698
|
color: var(--text-muted);
|
|
460
|
-
margin-left: auto;
|
|
461
699
|
}
|
|
462
700
|
|
|
463
|
-
|
|
701
|
+
.suite-tag {
|
|
702
|
+
font-size: 0.65rem;
|
|
703
|
+
font-family: 'JetBrains Mono', monospace;
|
|
704
|
+
background: var(--accent-soft);
|
|
705
|
+
color: var(--accent);
|
|
706
|
+
padding: 0.1rem 0.4rem;
|
|
707
|
+
border-radius: 100px;
|
|
708
|
+
font-weight: 500;
|
|
709
|
+
}
|
|
710
|
+
|
|
464
711
|
.scenarios {
|
|
465
712
|
display: flex;
|
|
466
713
|
flex-direction: column;
|
|
467
|
-
|
|
714
|
+
padding: 0.5rem;
|
|
715
|
+
gap: 0.25rem;
|
|
468
716
|
}
|
|
469
717
|
|
|
470
718
|
.scenario {
|
|
@@ -475,6 +723,11 @@
|
|
|
475
723
|
animation: fadeUp 0.3s var(--ease-out) both;
|
|
476
724
|
}
|
|
477
725
|
|
|
726
|
+
.scenario.scenario-fail {
|
|
727
|
+
border-left-width: 3px;
|
|
728
|
+
border-left-color: var(--fail);
|
|
729
|
+
}
|
|
730
|
+
|
|
478
731
|
.scenario-header {
|
|
479
732
|
display: flex;
|
|
480
733
|
align-items: center;
|
|
@@ -567,7 +820,6 @@
|
|
|
567
820
|
transform: rotate(90deg);
|
|
568
821
|
}
|
|
569
822
|
|
|
570
|
-
/* ── Steps ── */
|
|
571
823
|
.steps {
|
|
572
824
|
border-top: 1px solid var(--border);
|
|
573
825
|
background: var(--bg-subtle);
|
|
@@ -598,11 +850,9 @@
|
|
|
598
850
|
.step {
|
|
599
851
|
padding: 0.375rem 1rem 0.375rem 1.25rem;
|
|
600
852
|
}
|
|
601
|
-
|
|
602
853
|
.step-fail {
|
|
603
854
|
background: color-mix(in srgb, var(--fail-soft) 40%, transparent);
|
|
604
855
|
}
|
|
605
|
-
|
|
606
856
|
.step-skip {
|
|
607
857
|
opacity: 0.5;
|
|
608
858
|
}
|
|
@@ -702,7 +952,6 @@
|
|
|
702
952
|
border: 1px solid var(--border);
|
|
703
953
|
}
|
|
704
954
|
|
|
705
|
-
/* ── Loading / error ── */
|
|
706
955
|
.loading-state {
|
|
707
956
|
display: flex;
|
|
708
957
|
justify-content: center;
|