plum-e2e 1.1.1 → 1.2.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/settings.local.json +27 -25
- package/.husky/pre-commit +2 -2
- package/README.md +142 -70
- package/backend/Dockerfile +4 -2
- package/backend/app.js +4 -2
- package/backend/config/scripts/generate-report.js +38 -30
- package/backend/entrypoint.sh +22 -0
- package/backend/package-lock.json +453 -10
- package/backend/package.json +5 -2
- package/backend/prisma/migrations/20260614000000_init/migration.sql +35 -0
- package/backend/prisma/migrations/20260614000001_add_project/migration.sql +8 -0
- package/backend/prisma/migrations/migration_lock.toml +3 -0
- package/backend/prisma/schema.prisma +53 -0
- package/backend/routes/backup.routes.js +50 -0
- package/backend/routes/cron.routes.js +9 -60
- package/backend/routes/reports.routes.js +39 -6
- package/backend/routes/settings.routes.js +43 -0
- package/backend/server.js +52 -1
- package/backend/services/backupService.js +88 -0
- package/backend/services/cronService.js +68 -78
- package/backend/services/{scheduleService.js → prisma.js} +3 -15
- package/backend/services/reportService.js +48 -20
- package/backend/{routes/schedules.routes.js → services/settingsService.js} +17 -13
- package/bin/plum.js +213 -32
- package/docker-compose.yml +24 -0
- package/frontend/package-lock.json +2 -2
- package/frontend/package.json +1 -1
- package/frontend/src/lib/api/reports.js +38 -27
- package/frontend/src/lib/api/schedules.js +9 -25
- package/frontend/src/lib/api/settings.js +48 -0
- package/frontend/src/lib/components/layout/Nav.svelte +2 -1
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +160 -21
- package/frontend/src/lib/components/ui/Terminal.svelte +2 -2
- package/frontend/src/lib/stores/runner.js +9 -0
- package/frontend/src/routes/+page.svelte +10 -3
- package/frontend/src/routes/reports/+page.svelte +342 -51
- package/frontend/src/routes/reports/[slug]/+page.svelte +2 -0
- package/frontend/src/routes/scheduled-tests/+page.svelte +247 -11
- package/frontend/src/routes/settings/+page.svelte +410 -0
- package/license-config.json +2 -2
- package/package.json +6 -2
- package/backend/config/scripts/create-settings.js +0 -53
package/docker-compose.yml
CHANGED
|
@@ -16,14 +16,35 @@
|
|
|
16
16
|
#
|
|
17
17
|
|
|
18
18
|
services:
|
|
19
|
+
postgres:
|
|
20
|
+
image: postgres:16-alpine
|
|
21
|
+
environment:
|
|
22
|
+
POSTGRES_DB: plum
|
|
23
|
+
POSTGRES_USER: plum
|
|
24
|
+
POSTGRES_PASSWORD: plum
|
|
25
|
+
volumes:
|
|
26
|
+
- postgres_data:/var/lib/postgresql/data
|
|
27
|
+
networks:
|
|
28
|
+
- app-network
|
|
29
|
+
healthcheck:
|
|
30
|
+
test: ['CMD-SHELL', 'pg_isready -U plum']
|
|
31
|
+
interval: 5s
|
|
32
|
+
timeout: 5s
|
|
33
|
+
retries: 10
|
|
34
|
+
|
|
19
35
|
backend:
|
|
20
36
|
build: ./backend
|
|
21
37
|
ports:
|
|
22
38
|
- '3001:3001'
|
|
39
|
+
environment:
|
|
40
|
+
DATABASE_URL: 'postgresql://plum:plum@postgres:5432/plum'
|
|
23
41
|
volumes:
|
|
24
42
|
- ./backend/reports:/app/reports
|
|
25
43
|
- ./backend/config:/app/config
|
|
26
44
|
- ./backend/tests:/app/tests:rw
|
|
45
|
+
depends_on:
|
|
46
|
+
postgres:
|
|
47
|
+
condition: service_healthy
|
|
27
48
|
networks:
|
|
28
49
|
- app-network
|
|
29
50
|
|
|
@@ -39,3 +60,6 @@ services:
|
|
|
39
60
|
networks:
|
|
40
61
|
app-network:
|
|
41
62
|
driver: bridge
|
|
63
|
+
|
|
64
|
+
volumes:
|
|
65
|
+
postgres_data:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plum-frontend",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "plum-frontend",
|
|
9
|
-
"version": "
|
|
9
|
+
"version": "1.2.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"socket.io-client": "^4.8.1"
|
|
12
12
|
},
|
package/frontend/package.json
CHANGED
|
@@ -15,29 +15,12 @@
|
|
|
15
15
|
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
16
|
*/
|
|
17
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
18
|
const BASE = 'http://localhost:3001';
|
|
36
19
|
|
|
37
20
|
export async function fetchReports() {
|
|
38
21
|
const res = await fetch(`${BASE}/reports`);
|
|
39
22
|
const { reports } = await res.json();
|
|
40
|
-
return reports.map(
|
|
23
|
+
return reports.map((r) => ({ ...r, date: new Date(r.createdAt).toLocaleString() }));
|
|
41
24
|
}
|
|
42
25
|
|
|
43
26
|
export async function fetchLatestReport() {
|
|
@@ -50,19 +33,47 @@ export function reportUrl(fileName) {
|
|
|
50
33
|
return `/reports/${encodeURIComponent(fileName)}`;
|
|
51
34
|
}
|
|
52
35
|
|
|
36
|
+
// Parses metadata out of a report filename.
|
|
37
|
+
// Format: {STATUS}_cucumber_report_{trigger}_{tags}_runners_{n}_{timestamp}.json
|
|
38
|
+
export function parseReport(fileName) {
|
|
39
|
+
const m = fileName.match(
|
|
40
|
+
/^(PASS|FAIL)_cucumber_report_(.+?)_(\([^)]+\))_runners_(\d+)_(\d{4}_\d{2}_\d{2}T[\d_]+Z)\.json$/
|
|
41
|
+
);
|
|
42
|
+
if (!m) return null;
|
|
43
|
+
const [, status, triggerType, tags, runners, tsRaw] = m;
|
|
44
|
+
const isoStr = tsRaw.replace(
|
|
45
|
+
/^(\d{4})_(\d{2})_(\d{2})T(\d{2})_(\d{2})_(\d{2})_(\d+)Z$/,
|
|
46
|
+
'$1-$2-$3T$4:$5:$6.$7Z'
|
|
47
|
+
);
|
|
48
|
+
return {
|
|
49
|
+
status,
|
|
50
|
+
triggerType,
|
|
51
|
+
tags,
|
|
52
|
+
runners: parseInt(runners, 10),
|
|
53
|
+
date: new Date(isoStr).toLocaleString()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
53
57
|
export async function fetchReportDetail(fileName) {
|
|
54
58
|
const res = await fetch(`${BASE}/reports/${encodeURIComponent(fileName)}/detail`);
|
|
55
59
|
if (!res.ok) throw new Error('Report not found');
|
|
56
60
|
return res.json();
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
export function
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
);
|
|
63
|
-
if (!
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
export async function deleteReport(fileName) {
|
|
64
|
+
const res = await fetch(`${BASE}/reports/${encodeURIComponent(fileName)}`, {
|
|
65
|
+
method: 'DELETE'
|
|
66
|
+
});
|
|
67
|
+
if (!res.ok) throw new Error('Failed to delete report');
|
|
68
|
+
return res.json();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function deleteReports(fileNames) {
|
|
72
|
+
const res = await fetch(`${BASE}/reports/bulk`, {
|
|
73
|
+
method: 'DELETE',
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
body: JSON.stringify({ fileNames })
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok) throw new Error('Failed to delete reports');
|
|
78
|
+
return res.json();
|
|
68
79
|
}
|
|
@@ -15,45 +15,29 @@
|
|
|
15
15
|
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
16
|
*/
|
|
17
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
18
|
const BASE = 'http://localhost:3001';
|
|
36
19
|
|
|
37
|
-
export async function fetchSchedules() {
|
|
38
|
-
const res = await fetch(`${BASE}/schedules`);
|
|
39
|
-
const { schedules } = await res.json();
|
|
40
|
-
return schedules ?? [];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
20
|
export async function fetchCronJobs() {
|
|
44
21
|
const res = await fetch(`${BASE}/cron-jobs`);
|
|
45
22
|
const { cronJobs } = await res.json();
|
|
46
23
|
return cronJobs ?? [];
|
|
47
24
|
}
|
|
48
25
|
|
|
49
|
-
export async function saveCronJob({
|
|
26
|
+
export async function saveCronJob({
|
|
27
|
+
taskName,
|
|
28
|
+
cronExpression,
|
|
29
|
+
tags,
|
|
30
|
+
workers,
|
|
31
|
+
isEditing,
|
|
32
|
+
editTaskName
|
|
33
|
+
}) {
|
|
50
34
|
const formattedTags = tags.replace(/\sOR\s/gi, (m) => m.toLowerCase());
|
|
51
35
|
const url = isEditing ? `${BASE}/cron-jobs/${editTaskName}` : `${BASE}/cron-jobs`;
|
|
52
36
|
const method = isEditing ? 'PUT' : 'POST';
|
|
53
37
|
const res = await fetch(url, {
|
|
54
38
|
method,
|
|
55
39
|
headers: { 'Content-Type': 'application/json' },
|
|
56
|
-
body: JSON.stringify({ cronExpression, taskName, tags: formattedTags })
|
|
40
|
+
body: JSON.stringify({ cronExpression, taskName, tags: formattedTags, workers })
|
|
57
41
|
});
|
|
58
42
|
return res.json();
|
|
59
43
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
const BASE = 'http://localhost:3001';
|
|
19
|
+
|
|
20
|
+
export async function fetchProject() {
|
|
21
|
+
const res = await fetch(`${BASE}/settings/project`);
|
|
22
|
+
if (!res.ok) return { name: '', logoUrl: '' };
|
|
23
|
+
return res.json();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function saveProject({ name, logoUrl }) {
|
|
27
|
+
const res = await fetch(`${BASE}/settings/project`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
body: JSON.stringify({ name, logoUrl })
|
|
31
|
+
});
|
|
32
|
+
return res.json();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function exportBackup() {
|
|
36
|
+
const res = await fetch(`${BASE}/backup/export`);
|
|
37
|
+
if (!res.ok) throw new Error('Export failed');
|
|
38
|
+
return res.json();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function importBackup(data) {
|
|
42
|
+
const res = await fetch(`${BASE}/backup/import`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify(data)
|
|
46
|
+
});
|
|
47
|
+
return res.json();
|
|
48
|
+
}
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
const links = [
|
|
26
26
|
{ href: '/', label: 'Run Tests' },
|
|
27
27
|
{ href: '/reports', label: 'Reports' },
|
|
28
|
-
{ href: '/scheduled-tests', label: 'Scheduled' }
|
|
28
|
+
{ href: '/scheduled-tests', label: 'Scheduled' },
|
|
29
|
+
{ href: '/settings', label: 'Settings' }
|
|
29
30
|
];
|
|
30
31
|
|
|
31
32
|
function toggleTheme() {
|
|
@@ -19,7 +19,16 @@
|
|
|
19
19
|
import { onMount } from 'svelte';
|
|
20
20
|
import { slide, fly } from 'svelte/transition';
|
|
21
21
|
import { io } from 'socket.io-client';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
socket,
|
|
24
|
+
runnerState,
|
|
25
|
+
runnerConfig,
|
|
26
|
+
panelExpanded,
|
|
27
|
+
triggerRun,
|
|
28
|
+
testsVersion,
|
|
29
|
+
reportsVersion,
|
|
30
|
+
activeCronJobs
|
|
31
|
+
} from '$lib/stores/runner';
|
|
23
32
|
import { fetchLatestReport, reportUrl } from '$lib/api/reports';
|
|
24
33
|
import Terminal from '$lib/components/ui/Terminal.svelte';
|
|
25
34
|
import Button from '$lib/components/ui/Button.svelte';
|
|
@@ -47,30 +56,56 @@
|
|
|
47
56
|
}));
|
|
48
57
|
});
|
|
49
58
|
|
|
59
|
+
s.on('tests-changed', () => {
|
|
60
|
+
testsVersion.update((v) => v + 1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
s.on('report-ready', () => {
|
|
64
|
+
reportsVersion.update((v) => v + 1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
s.on('cron-start', ({ taskName }) => {
|
|
68
|
+
activeCronJobs.update((j) => ({ ...j, [taskName]: true }));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
s.on('cron-done', ({ taskName }) => {
|
|
72
|
+
activeCronJobs.update((j) => {
|
|
73
|
+
const next = { ...j };
|
|
74
|
+
delete next[taskName];
|
|
75
|
+
return next;
|
|
76
|
+
});
|
|
77
|
+
reportsVersion.update((v) => v + 1);
|
|
78
|
+
});
|
|
79
|
+
|
|
50
80
|
return () => s.disconnect();
|
|
51
81
|
});
|
|
52
82
|
|
|
53
83
|
$: state = $runnerState;
|
|
54
84
|
$: cfg = $runnerConfig;
|
|
55
|
-
$:
|
|
56
|
-
$:
|
|
85
|
+
$: dots = Array.from({ length: cfg.workers });
|
|
86
|
+
$: cronJobs = Object.keys($activeCronJobs);
|
|
87
|
+
$: anyCronRunning = cronJobs.length > 0;
|
|
88
|
+
$: anyRunning = state.running || anyCronRunning;
|
|
57
89
|
|
|
58
90
|
$: statusColor =
|
|
59
91
|
state.status === 'pass'
|
|
60
92
|
? 'var(--pass)'
|
|
61
93
|
: state.status === 'fail'
|
|
62
94
|
? 'var(--fail)'
|
|
63
|
-
: state.
|
|
95
|
+
: state.running
|
|
64
96
|
? 'var(--accent)'
|
|
65
|
-
:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
97
|
+
: anyCronRunning
|
|
98
|
+
? 'var(--pass)'
|
|
99
|
+
: 'var(--border)';
|
|
100
|
+
|
|
101
|
+
$: statusLabel = state.running
|
|
102
|
+
? 'running'
|
|
103
|
+
: state.status === 'pass'
|
|
104
|
+
? 'passed'
|
|
105
|
+
: state.status === 'fail'
|
|
106
|
+
? 'failed'
|
|
107
|
+
: anyCronRunning
|
|
108
|
+
? 'scheduled'
|
|
74
109
|
: 'ready';
|
|
75
110
|
|
|
76
111
|
function handleKeydown(e) {
|
|
@@ -142,7 +177,7 @@
|
|
|
142
177
|
class:active={cfg.workers === n}
|
|
143
178
|
on:click={() => runnerConfig.update((c) => ({ ...c, workers: n }))}
|
|
144
179
|
>
|
|
145
|
-
{n
|
|
180
|
+
{n}
|
|
146
181
|
</button>
|
|
147
182
|
{/each}
|
|
148
183
|
</div>
|
|
@@ -152,15 +187,38 @@
|
|
|
152
187
|
</Button>
|
|
153
188
|
</div>
|
|
154
189
|
|
|
155
|
-
<
|
|
190
|
+
<div class="main-row">
|
|
191
|
+
<!-- Terminal column -->
|
|
192
|
+
<div class="terminal-col">
|
|
193
|
+
<Terminal output={state.output} />
|
|
194
|
+
{#if state.testCompleted && state.latestReport}
|
|
195
|
+
<div class="report-row" transition:fly={{ y: 6, duration: 200 }}>
|
|
196
|
+
<a href={reportUrl(state.latestReport)} target="_blank" rel="noopener noreferrer">
|
|
197
|
+
<Button variant="outline" size="sm">View Report →</Button>
|
|
198
|
+
</a>
|
|
199
|
+
</div>
|
|
200
|
+
{/if}
|
|
201
|
+
</div>
|
|
156
202
|
|
|
157
|
-
|
|
158
|
-
<div class="
|
|
159
|
-
<
|
|
160
|
-
<
|
|
161
|
-
|
|
203
|
+
<!-- Sidebar: scheduled jobs -->
|
|
204
|
+
<div class="sidebar">
|
|
205
|
+
<div class="sidebar-section">
|
|
206
|
+
<span class="sidebar-label">Scheduled</span>
|
|
207
|
+
{#if cronJobs.length === 0}
|
|
208
|
+
<span class="sidebar-empty">none running</span>
|
|
209
|
+
{:else}
|
|
210
|
+
<ul class="cron-list">
|
|
211
|
+
{#each cronJobs as name}
|
|
212
|
+
<li class="cron-item" transition:fly={{ x: -6, duration: 180 }}>
|
|
213
|
+
<span class="cron-dot"></span>
|
|
214
|
+
<span class="cron-name">{name}</span>
|
|
215
|
+
</li>
|
|
216
|
+
{/each}
|
|
217
|
+
</ul>
|
|
218
|
+
{/if}
|
|
219
|
+
</div>
|
|
162
220
|
</div>
|
|
163
|
-
|
|
221
|
+
</div>
|
|
164
222
|
</div>
|
|
165
223
|
{/if}
|
|
166
224
|
</div>
|
|
@@ -372,7 +430,88 @@
|
|
|
372
430
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
373
431
|
}
|
|
374
432
|
|
|
433
|
+
/* ── Main row: terminal + sidebar ── */
|
|
434
|
+
.main-row {
|
|
435
|
+
display: flex;
|
|
436
|
+
gap: 1rem;
|
|
437
|
+
align-items: flex-start;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.terminal-col {
|
|
441
|
+
flex: 1;
|
|
442
|
+
min-width: 0;
|
|
443
|
+
display: flex;
|
|
444
|
+
flex-direction: column;
|
|
445
|
+
gap: 0.5rem;
|
|
446
|
+
/* Constrain terminal height via the Terminal component's wrapper */
|
|
447
|
+
--terminal-max-height: 140px;
|
|
448
|
+
}
|
|
449
|
+
|
|
375
450
|
.report-row {
|
|
376
451
|
display: flex;
|
|
377
452
|
}
|
|
453
|
+
|
|
454
|
+
/* ── Sidebar ── */
|
|
455
|
+
.sidebar {
|
|
456
|
+
width: 260px;
|
|
457
|
+
flex-shrink: 0;
|
|
458
|
+
display: flex;
|
|
459
|
+
flex-direction: column;
|
|
460
|
+
gap: 1rem;
|
|
461
|
+
padding-left: 1.25rem;
|
|
462
|
+
border-left: 1px solid var(--border);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.sidebar-section {
|
|
466
|
+
display: flex;
|
|
467
|
+
flex-direction: column;
|
|
468
|
+
gap: 0.5rem;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.sidebar-label {
|
|
472
|
+
font-size: 0.65rem;
|
|
473
|
+
font-weight: 600;
|
|
474
|
+
letter-spacing: 0.09em;
|
|
475
|
+
text-transform: uppercase;
|
|
476
|
+
color: var(--text-muted);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.sidebar-empty {
|
|
480
|
+
font-size: 0.75rem;
|
|
481
|
+
color: var(--text-muted);
|
|
482
|
+
font-style: italic;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.cron-list {
|
|
486
|
+
list-style: none;
|
|
487
|
+
padding: 0;
|
|
488
|
+
margin: 0;
|
|
489
|
+
display: flex;
|
|
490
|
+
flex-direction: column;
|
|
491
|
+
gap: 0.375rem;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.cron-item {
|
|
495
|
+
display: flex;
|
|
496
|
+
align-items: center;
|
|
497
|
+
gap: 0.375rem;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.cron-dot {
|
|
501
|
+
width: 7px;
|
|
502
|
+
height: 7px;
|
|
503
|
+
border-radius: 50%;
|
|
504
|
+
background: var(--pass);
|
|
505
|
+
flex-shrink: 0;
|
|
506
|
+
animation: statusPulse 1.6s ease-in-out infinite;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.cron-name {
|
|
510
|
+
font-size: 0.75rem;
|
|
511
|
+
font-family: 'JetBrains Mono', monospace;
|
|
512
|
+
color: var(--text);
|
|
513
|
+
white-space: nowrap;
|
|
514
|
+
overflow: hidden;
|
|
515
|
+
text-overflow: ellipsis;
|
|
516
|
+
}
|
|
378
517
|
</style>
|
|
@@ -35,6 +35,15 @@ export const runnerConfig = writable({
|
|
|
35
35
|
|
|
36
36
|
export const panelExpanded = writable(true);
|
|
37
37
|
|
|
38
|
+
// Increments whenever the backend detects a change in tests/features/
|
|
39
|
+
export const testsVersion = writable(0);
|
|
40
|
+
|
|
41
|
+
// Increments whenever a new report file is detected
|
|
42
|
+
export const reportsVersion = writable(0);
|
|
43
|
+
|
|
44
|
+
// Map of taskName → true for every cron job currently executing
|
|
45
|
+
export const activeCronJobs = writable({});
|
|
46
|
+
|
|
38
47
|
export function triggerRun(id) {
|
|
39
48
|
const s = get(socket);
|
|
40
49
|
if (!s) return;
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { onDestroy, onMount } from 'svelte';
|
|
20
20
|
import { slide } from 'svelte/transition';
|
|
21
21
|
import { fetchSuites } from '$lib/api/tests';
|
|
22
|
-
import { runnerConfig, triggerRun } from '$lib/stores/runner';
|
|
22
|
+
import { runnerConfig, triggerRun, testsVersion } from '$lib/stores/runner';
|
|
23
23
|
|
|
24
24
|
let suites = [];
|
|
25
25
|
let search = '';
|
|
@@ -27,13 +27,18 @@
|
|
|
27
27
|
let copiedIds = new Set();
|
|
28
28
|
const copyTimers = new Map();
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
async function loadSuites() {
|
|
31
31
|
try {
|
|
32
32
|
suites = await fetchSuites();
|
|
33
33
|
} catch (e) {
|
|
34
34
|
console.error('Failed to fetch suites', e);
|
|
35
35
|
}
|
|
36
|
-
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onMount(loadSuites);
|
|
39
|
+
|
|
40
|
+
// Re-fetch when the backend notifies us that test files changed
|
|
41
|
+
$: if ($testsVersion) loadSuites();
|
|
37
42
|
|
|
38
43
|
function suiteIds(suite) {
|
|
39
44
|
return Array.isArray(suite.suiteId) ? suite.suiteId : [suite.suiteId];
|
|
@@ -105,6 +110,8 @@
|
|
|
105
110
|
$: visibleTests = filtered.reduce((n, s) => n + s.tests.length, 0);
|
|
106
111
|
</script>
|
|
107
112
|
|
|
113
|
+
<svelte:head><title>Run Tests — Plum</title></svelte:head>
|
|
114
|
+
|
|
108
115
|
<div class="page-header">
|
|
109
116
|
<div class="header-top">
|
|
110
117
|
<div>
|