plum-e2e 2.4.13 → 2.5.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.
Files changed (55) hide show
  1. package/CLAUDE.md +1 -1
  2. package/backend/_scaffold/utils/browser.ts +47 -2
  3. package/backend/_scaffold/utils/hooks.ts +25 -2
  4. package/backend/config/scripts/run-tests.js +35 -13
  5. package/backend/logs/runner-cmr8o2x3n0000mg01410tpuck.log +20 -0
  6. package/backend/logs/runner-cmr90hvcx0000n2016edtmrtv.log +43 -0
  7. package/backend/logs/runner-cmr90i7h20001n201g2wg6v3n.log +43 -0
  8. package/backend/logs/runner-cmr90iilm0002n201mmisva95.log +43 -0
  9. package/backend/logs/runner-cmr90j70r0003n201xz8525dz.log +20 -0
  10. package/backend/logs/runner-cmr90yup70000qp017ianc2gp.log +43 -0
  11. package/backend/logs/runner-cmr90zbtm0001qp01u9tfdkh1.log +20 -0
  12. package/backend/logs/runner-cmr91a3mw0000tb01414rd1df.log +20 -0
  13. package/backend/logs/runner-cmr91dhla0001tb01jp7thjnl.log +20 -0
  14. package/backend/prisma/migrations/20260706000000_add_report_logs/migration.sql +2 -0
  15. package/backend/prisma/migrations/20260707032554_add_perf_indexes/migration.sql +44 -0
  16. package/backend/prisma/schema.prisma +20 -0
  17. package/backend/routes/node.routes.js +42 -4
  18. package/backend/routes/reports.routes.js +4 -2
  19. package/backend/services/reportService.js +58 -20
  20. package/backend/services/runnerService.js +11 -1
  21. package/backend/websockets/socketHandler.js +162 -16
  22. package/bin/plum.js +96 -68
  23. package/frontend/package.json +1 -1
  24. package/frontend/src/lib/api/reports.js +16 -5
  25. package/frontend/src/lib/components/layout/RunnerPanel.svelte +53 -55
  26. package/frontend/src/lib/components/ui/AutomatedBadge.svelte +56 -0
  27. package/frontend/src/lib/components/ui/BackLink.svelte +77 -0
  28. package/frontend/src/lib/components/ui/Badge.svelte +1 -1
  29. package/frontend/src/lib/components/ui/Button.svelte +1 -1
  30. package/frontend/src/lib/components/ui/CaseIdChip.svelte +59 -0
  31. package/frontend/src/lib/components/ui/ConfirmModal.svelte +1 -1
  32. package/frontend/src/lib/components/ui/Pagination.svelte +1 -1
  33. package/frontend/src/lib/components/ui/PriorityBadge.svelte +83 -0
  34. package/frontend/src/lib/components/ui/ResultChip.svelte +92 -0
  35. package/frontend/src/lib/components/ui/StatusDot.svelte +64 -0
  36. package/frontend/src/lib/components/ui/StepKeyword.svelte +79 -0
  37. package/frontend/src/lib/components/ui/StepStatusIcon.svelte +66 -0
  38. package/frontend/src/lib/components/ui/TagChip.svelte +51 -0
  39. package/frontend/src/lib/constants.js +14 -0
  40. package/frontend/src/lib/stores/runner.js +5 -3
  41. package/frontend/src/lib/styles/tokens.css +26 -0
  42. package/frontend/src/lib/utils/format.js +74 -0
  43. package/frontend/src/routes/+page.svelte +3 -3
  44. package/frontend/src/routes/login/+page.svelte +55 -40
  45. package/frontend/src/routes/reports/+page.svelte +35 -27
  46. package/frontend/src/routes/reports/[id]/+page.svelte +703 -215
  47. package/frontend/src/routes/reports/live/+page.svelte +281 -283
  48. package/frontend/src/routes/scheduled-tests/+page.svelte +3 -3
  49. package/frontend/src/routes/settings/+page.svelte +6 -6
  50. package/frontend/src/routes/setup/+page.svelte +2 -1
  51. package/frontend/src/routes/test-repository/+page.svelte +5 -5
  52. package/frontend/src/routes/test-repository/runs/[id]/+page.svelte +33 -152
  53. package/frontend/src/routes/test-repository/suites/[id]/+page.svelte +4 -4
  54. package/frontend/vite.config.js +7 -1
  55. package/package.json +1 -1
@@ -0,0 +1,59 @@
1
+ <!--
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ -->
17
+
18
+ <!--
19
+ * This file is part of Plum.
20
+ *
21
+ * Plum is free software: you can redistribute it and/or modify
22
+ * it under the terms of the GNU General Public License as published by
23
+ * the Free Software Foundation, either version 3 of the License, or
24
+ * (at your option) any later version.
25
+ *
26
+ * Plum is distributed in the hope that it will be useful,
27
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
28
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
+ * GNU General Public License for more details.
30
+ *
31
+ * You should have received a copy of the GNU General Public License
32
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
33
+ -->
34
+
35
+ <script>
36
+ export let id = '';
37
+ export let small = false;
38
+ </script>
39
+
40
+ <span class="id-chip" class:small>{id}</span>
41
+
42
+ <style>
43
+ .id-chip {
44
+ font-family: 'JetBrains Mono', monospace;
45
+ font-weight: 600;
46
+ color: var(--accent);
47
+ background: var(--accent-soft);
48
+ border-radius: 4px;
49
+ letter-spacing: 0.02em;
50
+ font-size: 0.7rem;
51
+ padding: 0.1rem 0.4rem;
52
+ flex-shrink: 0;
53
+ white-space: nowrap;
54
+ }
55
+
56
+ .id-chip.small {
57
+ font-size: 0.63rem;
58
+ }
59
+ </style>
@@ -67,7 +67,7 @@
67
67
  border: 1px solid var(--fail);
68
68
  border-radius: var(--radius-sm);
69
69
  cursor: pointer;
70
- color: #fff;
70
+ color: var(--white);
71
71
  transition: opacity var(--duration-fast);
72
72
  }
73
73
 
@@ -89,7 +89,7 @@
89
89
 
90
90
  button.active {
91
91
  background: var(--accent);
92
- color: #fff;
92
+ color: var(--white);
93
93
  border-color: var(--accent);
94
94
  }
95
95
 
@@ -0,0 +1,83 @@
1
+ <!--
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ -->
17
+
18
+ <!--
19
+ * This file is part of Plum.
20
+ *
21
+ * Plum is free software: you can redistribute it and/or modify
22
+ * it under the terms of the GNU General Public License as published by
23
+ * the Free Software Foundation, either version 3 of the License, or
24
+ * (at your option) any later version.
25
+ *
26
+ * Plum is distributed in the hope that it will be useful,
27
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
28
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
+ * GNU General Public License for more details.
30
+ *
31
+ * You should have received a copy of the GNU General Public License
32
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
33
+ -->
34
+
35
+ <script>
36
+ export let priority = 'Medium';
37
+ export let small = false;
38
+
39
+ $: p = priority?.toLowerCase() ?? 'medium';
40
+ </script>
41
+
42
+ <span
43
+ class="priority-badge"
44
+ class:critical={p === 'critical'}
45
+ class:high={p === 'high'}
46
+ class:medium={p === 'medium'}
47
+ class:low={p === 'low'}
48
+ class:small
49
+ >
50
+ {priority}
51
+ </span>
52
+
53
+ <style>
54
+ .priority-badge {
55
+ font-size: 0.65rem;
56
+ font-weight: 500;
57
+ border-radius: var(--radius-pill);
58
+ padding: 0.12rem 0.45rem;
59
+ flex-shrink: 0;
60
+ white-space: nowrap;
61
+ }
62
+
63
+ .priority-badge.small {
64
+ font-size: 0.6rem;
65
+ }
66
+
67
+ .critical {
68
+ background: var(--fail-soft);
69
+ color: var(--fail);
70
+ }
71
+ .high {
72
+ background: var(--warn-soft);
73
+ color: var(--warn);
74
+ }
75
+ .medium {
76
+ background: var(--node-soft);
77
+ color: var(--node);
78
+ }
79
+ .low {
80
+ background: var(--bg-subtle);
81
+ color: var(--text-muted);
82
+ }
83
+ </style>
@@ -0,0 +1,92 @@
1
+ <!--
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ -->
17
+
18
+ <!--
19
+ * This file is part of Plum.
20
+ *
21
+ * Plum is free software: you can redistribute it and/or modify
22
+ * it under the terms of the GNU General Public License as published by
23
+ * the Free Software Foundation, either version 3 of the License, or
24
+ * (at your option) any later version.
25
+ *
26
+ * Plum is distributed in the hope that it will be useful,
27
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
28
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
+ * GNU General Public License for more details.
30
+ *
31
+ * You should have received a copy of the GNU General Public License
32
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
33
+ -->
34
+
35
+ <script>
36
+ export let status = 'pending';
37
+
38
+ $: v =
39
+ status === 'pass'
40
+ ? 'pass'
41
+ : status === 'fail'
42
+ ? 'fail'
43
+ : status === 'blocked'
44
+ ? 'warn'
45
+ : status === 'skip'
46
+ ? 'muted'
47
+ : 'pending';
48
+ </script>
49
+
50
+ <span
51
+ class="result-chip"
52
+ class:pass={v === 'pass'}
53
+ class:fail={v === 'fail'}
54
+ class:warn={v === 'warn'}
55
+ class:muted={v === 'muted'}
56
+ class:pending={v === 'pending'}
57
+ >
58
+ {status}
59
+ </span>
60
+
61
+ <style>
62
+ .result-chip {
63
+ font-size: 0.7rem;
64
+ font-weight: 600;
65
+ text-transform: uppercase;
66
+ letter-spacing: 0.05em;
67
+ padding: 0.2rem 0.6rem;
68
+ border-radius: var(--radius-pill);
69
+ white-space: nowrap;
70
+ }
71
+
72
+ .pass {
73
+ background: var(--pass-soft);
74
+ color: var(--pass);
75
+ }
76
+ .fail {
77
+ background: var(--fail-soft);
78
+ color: var(--fail);
79
+ }
80
+ .warn {
81
+ background: var(--warn-soft);
82
+ color: var(--warn);
83
+ }
84
+ .muted {
85
+ background: var(--bg-subtle);
86
+ color: var(--text-muted);
87
+ }
88
+ .pending {
89
+ background: var(--bg-subtle);
90
+ color: var(--text-muted);
91
+ }
92
+ </style>
@@ -0,0 +1,64 @@
1
+ <!--
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ -->
17
+
18
+ <!--
19
+ * This file is part of Plum.
20
+ *
21
+ * Plum is free software: you can redistribute it and/or modify
22
+ * it under the terms of the GNU General Public License as published by
23
+ * the Free Software Foundation, either version 3 of the License, or
24
+ * (at your option) any later version.
25
+ *
26
+ * Plum is distributed in the hope that it will be useful,
27
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
28
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
+ * GNU General Public License for more details.
30
+ *
31
+ * You should have received a copy of the GNU General Public License
32
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
33
+ -->
34
+
35
+ <script>
36
+ export let status = 'passed';
37
+ </script>
38
+
39
+ <span
40
+ class="status-dot"
41
+ class:pass={status === 'passed'}
42
+ class:fail={status === 'failed'}
43
+ class:skip={status !== 'passed' && status !== 'failed'}
44
+ ></span>
45
+
46
+ <style>
47
+ .status-dot {
48
+ display: inline-block;
49
+ width: 8px;
50
+ height: 8px;
51
+ border-radius: 50%;
52
+ flex-shrink: 0;
53
+ }
54
+
55
+ .pass {
56
+ background: var(--pass);
57
+ }
58
+ .fail {
59
+ background: var(--fail);
60
+ }
61
+ .skip {
62
+ background: var(--text-muted);
63
+ }
64
+ </style>
@@ -0,0 +1,79 @@
1
+ <!--
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ -->
17
+
18
+ <!--
19
+ * This file is part of Plum.
20
+ *
21
+ * Plum is free software: you can redistribute it and/or modify
22
+ * it under the terms of the GNU General Public License as published by
23
+ * the Free Software Foundation, either version 3 of the License, or
24
+ * (at your option) any later version.
25
+ *
26
+ * Plum is distributed in the hope that it will be useful,
27
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
28
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
+ * GNU General Public License for more details.
30
+ *
31
+ * You should have received a copy of the GNU General Public License
32
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
33
+ -->
34
+
35
+ <script>
36
+ import { keywordClass } from '$lib/utils/format';
37
+
38
+ export let keyword = '';
39
+
40
+ $: klass = keywordClass(keyword);
41
+ </script>
42
+
43
+ <span
44
+ class="kw"
45
+ class:kw-given={klass === 'kw-given'}
46
+ class:kw-when={klass === 'kw-when'}
47
+ class:kw-then={klass === 'kw-then'}
48
+ class:kw-and={klass === 'kw-and'}>{keyword}</span
49
+ >
50
+
51
+ <style>
52
+ .kw {
53
+ font-size: 0.72rem;
54
+ font-weight: 500;
55
+ letter-spacing: 0.04em;
56
+ flex-shrink: 0;
57
+ padding: 0.05rem 0.4rem;
58
+ border-radius: 3px;
59
+ font-family: 'JetBrains Mono', monospace;
60
+ }
61
+
62
+ .kw-given {
63
+ background: var(--accent-soft);
64
+ color: var(--accent);
65
+ }
66
+ .kw-when {
67
+ background: var(--warn-soft);
68
+ color: var(--warn);
69
+ }
70
+ .kw-then {
71
+ background: var(--pass-soft);
72
+ color: var(--pass);
73
+ }
74
+ .kw-and {
75
+ background: var(--bg-elevated);
76
+ color: var(--text-muted);
77
+ border: 1px solid var(--border);
78
+ }
79
+ </style>
@@ -0,0 +1,66 @@
1
+ <!--
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ -->
17
+
18
+ <!--
19
+ * This file is part of Plum.
20
+ *
21
+ * Plum is free software: you can redistribute it and/or modify
22
+ * it under the terms of the GNU General Public License as published by
23
+ * the Free Software Foundation, either version 3 of the License, or
24
+ * (at your option) any later version.
25
+ *
26
+ * Plum is distributed in the hope that it will be useful,
27
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
28
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
+ * GNU General Public License for more details.
30
+ *
31
+ * You should have received a copy of the GNU General Public License
32
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
33
+ -->
34
+
35
+ <script>
36
+ export let status = 'passed';
37
+ </script>
38
+
39
+ <span
40
+ class="step-status-icon"
41
+ class:icon-pass={status === 'passed'}
42
+ class:icon-fail={status === 'failed'}
43
+ class:icon-skip={status !== 'passed' && status !== 'failed'}
44
+ >
45
+ {#if status === 'passed'}✓{:else if status === 'failed'}✗{:else}−{/if}
46
+ </span>
47
+
48
+ <style>
49
+ .step-status-icon {
50
+ font-size: 0.75rem;
51
+ font-weight: 600;
52
+ width: 14px;
53
+ flex-shrink: 0;
54
+ text-align: center;
55
+ }
56
+
57
+ .icon-pass {
58
+ color: var(--pass);
59
+ }
60
+ .icon-fail {
61
+ color: var(--fail);
62
+ }
63
+ .icon-skip {
64
+ color: var(--text-muted);
65
+ }
66
+ </style>
@@ -0,0 +1,51 @@
1
+ <!--
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ -->
17
+
18
+ <!--
19
+ * This file is part of Plum.
20
+ *
21
+ * Plum is free software: you can redistribute it and/or modify
22
+ * it under the terms of the GNU General Public License as published by
23
+ * the Free Software Foundation, either version 3 of the License, or
24
+ * (at your option) any later version.
25
+ *
26
+ * Plum is distributed in the hope that it will be useful,
27
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
28
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
+ * GNU General Public License for more details.
30
+ *
31
+ * You should have received a copy of the GNU General Public License
32
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
33
+ -->
34
+
35
+ <script>
36
+ export let tag = '';
37
+ </script>
38
+
39
+ <span class="tag-chip">{tag}</span>
40
+
41
+ <style>
42
+ .tag-chip {
43
+ font-size: 0.65rem;
44
+ font-family: 'JetBrains Mono', monospace;
45
+ background: var(--accent-soft);
46
+ color: var(--accent);
47
+ padding: 0.1rem 0.4rem;
48
+ border-radius: var(--radius-pill);
49
+ white-space: nowrap;
50
+ }
51
+ </style>
@@ -34,3 +34,17 @@ export const SUITE_CASES_PER_PAGE = 20;
34
34
 
35
35
  export const COPY_TIMEOUT_MS = 1400;
36
36
  export const TOAST_TIMEOUT_MS = 4000;
37
+
38
+ export const REPLAY_STEP_MS = 900;
39
+ export const REDIRECT_DELAY_MS = 3000;
40
+ export const RUN_REFRESH_MS = 15000;
41
+
42
+ export const WORKERS_MIN = 1;
43
+ export const WORKERS_MAX = 10;
44
+ export const RUN_PICKER_LIMIT = 200;
45
+ export const RUN_TAG_DISPLAY_LIMIT = 5;
46
+
47
+ export const BUILTIN_RUNNER_ID = 'built-in';
48
+ export const BUILTIN_RUNNER_LABEL = 'Built-in';
49
+ export const ALL_TESTS_LABEL = 'all tests';
50
+ export const AUTOMATED_LABEL = 'Automated';
@@ -26,8 +26,9 @@ export const runnerState = writable({
26
26
  latestReportId: null, // number | null — set after test finishes
27
27
  status: 'idle', // 'idle' | 'running' | 'pass' | 'fail'
28
28
  lastRunId: '',
29
- lanes: [], // [{ id, name, testCount, status, logs }] multi-runner only
30
- currentRun: null // { tag, workers, browser, runners } — set while running
29
+ lanes: [], // [{ id, name, testCount, status, logs, latestScreenshot }] multi-runner only
30
+ currentRun: null, // { tag, workers, browser, runners } — set while running
31
+ latestScreenshot: null // { stepName, data: base64 } for single built-in runner
31
32
  });
32
33
 
33
34
  export const runnerConfig = writable({
@@ -63,7 +64,8 @@ export function triggerRun(id, testRunId, notify = {}, runTitle = null) {
63
64
  status: 'running',
64
65
  lastRunId: runId,
65
66
  lanes: [],
66
- currentRun: { tag: runId, workers, browser, runners: selectedRunners, runTitle }
67
+ currentRun: { tag: runId, workers, browser, runners: selectedRunners, runTitle },
68
+ latestScreenshot: null
67
69
  });
68
70
  panelExpanded.set(true);
69
71
 
@@ -48,6 +48,32 @@
48
48
  --radius-sm: 6px;
49
49
  --radius-md: 10px;
50
50
  --radius-lg: 16px;
51
+ --radius-pill: 100px;
52
+
53
+ --white: #ffffff;
54
+ }
55
+
56
+ @keyframes fadeUp {
57
+ from {
58
+ opacity: 0;
59
+ transform: translateY(6px);
60
+ }
61
+ to {
62
+ opacity: 1;
63
+ transform: translateY(0);
64
+ }
65
+ }
66
+
67
+ @keyframes dotPulse {
68
+ 0%,
69
+ 100% {
70
+ opacity: 1;
71
+ transform: scale(1);
72
+ }
73
+ 50% {
74
+ opacity: 0.4;
75
+ transform: scale(0.65);
76
+ }
51
77
  }
52
78
 
53
79
  /* ── Design tokens (dark) ──────────────────────────────────────────────── */
@@ -64,3 +64,77 @@ export function featureFile(uri) {
64
64
  if (idx !== -1) return normalized.slice(idx + 1);
65
65
  return normalized.split('/').pop();
66
66
  }
67
+
68
+ export function keywordClass(kw) {
69
+ const k = (kw ?? '').toLowerCase().replace(/:$/, '');
70
+ if (k === 'given') return 'kw-given';
71
+ if (k === 'when') return 'kw-when';
72
+ if (k === 'then') return 'kw-then';
73
+ return 'kw-and';
74
+ }
75
+
76
+ export function scenarioTestTag(scenario) {
77
+ return scenario.tags?.find((tag) => /^@test[\w-]*/i.test(tag)) ?? null;
78
+ }
79
+
80
+ export function featureSuiteTag(feature) {
81
+ for (const scenario of feature.scenarios ?? []) {
82
+ const suiteTag = scenario.tags?.find((tag) => /suite/i.test(tag));
83
+ if (suiteTag) return suiteTag;
84
+ }
85
+ return null;
86
+ }
87
+
88
+ export function visibleTags(scenario) {
89
+ const testTag = scenarioTestTag(scenario);
90
+ if (testTag) return [testTag];
91
+ return (scenario.tags ?? []).filter((tag) => !/suite/i.test(tag));
92
+ }
93
+
94
+ const STATUS_RANK = { failed: 3, pending: 2, skipped: 1, passed: 0 };
95
+
96
+ export function worstStatus(scenarios) {
97
+ return scenarios.reduce(
98
+ (s, scenario) =>
99
+ (STATUS_RANK[scenario.status] ?? 0) > (STATUS_RANK[s] ?? 0) ? scenario.status : s,
100
+ 'passed'
101
+ );
102
+ }
103
+
104
+ export function groupedScenarios(scenarios) {
105
+ const groups = new Map();
106
+ for (const scenario of scenarios) {
107
+ const testTag = scenarioTestTag(scenario);
108
+ const key = testTag ?? `${scenario.keyword}:${scenario.name}`;
109
+ if (!groups.has(key)) {
110
+ groups.set(key, {
111
+ key,
112
+ name: scenario.name,
113
+ tags: visibleTags(scenario),
114
+ scenarios: [],
115
+ duration: 0,
116
+ status: scenario.status
117
+ });
118
+ }
119
+ const group = groups.get(key);
120
+ group.scenarios.push(scenario);
121
+ group.duration += scenario.duration ?? 0;
122
+ group.status = worstStatus(group.scenarios);
123
+ }
124
+ return Array.from(groups.values());
125
+ }
126
+
127
+ export function parseRunnerLogs(logs) {
128
+ if (!logs) return [];
129
+ if (!/^=== .+ ===/m.test(logs)) return [{ name: 'Logs', content: logs }];
130
+ const sections = [];
131
+ for (const chunk of logs.split(/\n\n(?==== )/)) {
132
+ const m = chunk.match(/^=== (.+?) ===\n?([\s\S]*)/);
133
+ if (m) sections.push({ name: m[1].trim(), content: m[2].trim() });
134
+ }
135
+ return sections;
136
+ }
137
+
138
+ export function scenarioHasScreenshots(scenario) {
139
+ return scenario.steps?.some((step) => step.screenshot) ?? false;
140
+ }
@@ -516,7 +516,7 @@
516
516
 
517
517
  .run-icon-btn:hover {
518
518
  background: var(--accent);
519
- color: #fff;
519
+ color: var(--white);
520
520
  border-color: var(--accent);
521
521
  }
522
522
 
@@ -534,7 +534,7 @@
534
534
  color: var(--accent);
535
535
  background: var(--accent-soft);
536
536
  border: none;
537
- border-radius: 100px;
537
+ border-radius: var(--radius-pill);
538
538
  padding: 0.15rem 0.55rem;
539
539
  cursor: pointer;
540
540
  font-family: 'JetBrains Mono', monospace;
@@ -589,7 +589,7 @@
589
589
  background: var(--warn-soft);
590
590
  color: var(--warn);
591
591
  padding: 0.1rem 0.4rem;
592
- border-radius: 100px;
592
+ border-radius: var(--radius-pill);
593
593
  margin-left: 0.4rem;
594
594
  vertical-align: middle;
595
595
  }