plum-e2e 1.3.2 → 1.3.4
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 +1 -1
- package/README.md +100 -26
- package/backend/app.js +10 -13
- package/backend/config/scripts/run-tests.js +60 -10
- package/backend/lib/nodeRegister.js +101 -0
- package/backend/lib/runnerProcess.js +180 -0
- package/backend/lib/serverConfig.js +112 -0
- package/backend/package-lock.json +2 -2
- package/backend/package.json +2 -2
- package/backend/scripts/manage-runners.mjs +297 -0
- package/backend/server.js +8 -5
- package/backend/services/reportService.js +17 -1
- package/backend/services/runnerService.js +18 -3
- package/backend/websockets/socketHandler.js +11 -3
- package/bin/plum.js +394 -102
- package/bin/scaffold-tests.js +34 -0
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/Button.cBruH0aD.css +17 -0
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.BVnUajEa.css +17 -0
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/0.CnXRuPt4.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/2.CGnCsn5q.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/3.BVnUajEa.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/4.DBhBrHFz.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/5.D93VAB-w.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/Button.cBruH0aD.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.BVnUajEa.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/Button.cBruH0aD.css +17 -0
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.BVnUajEa.css +17 -0
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
- package/frontend/build/client/_app/immutable/assets/0.CnXRuPt4.css +17 -0
- package/frontend/build/client/_app/immutable/assets/2.CGnCsn5q.css +17 -0
- package/frontend/build/client/_app/immutable/assets/3.BVnUajEa.css +17 -0
- package/frontend/build/client/_app/immutable/assets/4.DBhBrHFz.css +17 -0
- package/frontend/build/client/_app/immutable/assets/5.D93VAB-w.css +17 -0
- package/frontend/build/client/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
- package/frontend/build/client/_app/immutable/assets/Button.cBruH0aD.css +17 -0
- package/frontend/build/client/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
- package/frontend/build/client/_app/immutable/assets/_page.BVnUajEa.css +17 -0
- package/frontend/build/client/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
- package/frontend/build/client/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
- package/frontend/build/client/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
- package/frontend/package-lock.json +6 -1357
- package/frontend/package.json +1 -1
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +22 -16
- package/frontend/src/lib/utils/format.js +13 -0
- package/frontend/src/routes/reports/[id]/+page.svelte +2 -30
- package/frontend/src/routes/reports/live/+page.svelte +2 -2
- package/package.json +2 -2
- package/backend/scripts/add-local-runner.js +0 -120
package/frontend/package.json
CHANGED
|
@@ -113,24 +113,30 @@
|
|
|
113
113
|
runnerState.update((r) => ({ ...r, output: r.output + data + '\n' }));
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
s.on('done', (
|
|
116
|
+
s.on('done', (payload) => {
|
|
117
|
+
// Distributed runs send { code, reportId }; the built-in path sends a bare code.
|
|
118
|
+
const code = typeof payload === 'object' && payload !== null ? payload.code : payload;
|
|
119
|
+
const providedId =
|
|
120
|
+
typeof payload === 'object' && payload !== null ? payload.reportId : undefined;
|
|
117
121
|
const passed = code === 0 || code === null;
|
|
118
122
|
const cancelled = code === 130;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
123
|
+
const resolveId =
|
|
124
|
+
providedId !== undefined && providedId !== null
|
|
125
|
+
? Promise.resolve(providedId)
|
|
126
|
+
: fetchLatestReportId().catch(() => null);
|
|
127
|
+
resolveId.then((id) => {
|
|
128
|
+
runnerState.update((r) => ({
|
|
129
|
+
...r,
|
|
130
|
+
output:
|
|
131
|
+
r.output +
|
|
132
|
+
(cancelled ? '' : passed ? '\n✓ All tests passed\n' : '\n✗ Some tests failed\n'),
|
|
133
|
+
running: false,
|
|
134
|
+
testCompleted: !cancelled,
|
|
135
|
+
latestReportId: cancelled ? null : id,
|
|
136
|
+
status: cancelled ? 'idle' : passed ? 'pass' : 'fail',
|
|
137
|
+
currentRun: cancelled ? null : r.currentRun
|
|
138
|
+
}));
|
|
139
|
+
});
|
|
134
140
|
});
|
|
135
141
|
|
|
136
142
|
s.on('runner-lanes-init', (lanes) => {
|
|
@@ -44,3 +44,16 @@ export function fmtDuration(ms) {
|
|
|
44
44
|
if (ms >= 1000) return (ms / 1000).toFixed(2) + 's';
|
|
45
45
|
return ms + 'ms';
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Trims a Cucumber feature URI to a readable suffix. Dispatched runs report an
|
|
50
|
+
* absolute temp path on the node (…/plum-job-<uuid>/features/Login.feature); show
|
|
51
|
+
* only the part from `features/` onward so the column stays short.
|
|
52
|
+
*/
|
|
53
|
+
export function featureFile(uri) {
|
|
54
|
+
if (!uri) return '';
|
|
55
|
+
const normalized = uri.replace(/\\/g, '/');
|
|
56
|
+
const idx = normalized.lastIndexOf('/features/');
|
|
57
|
+
if (idx !== -1) return normalized.slice(idx + 1);
|
|
58
|
+
return normalized.split('/').pop();
|
|
59
|
+
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { onMount } from 'svelte';
|
|
21
21
|
import { slide } from 'svelte/transition';
|
|
22
22
|
import { fetchReportDetail, screenshotUrl } from '$lib/api/reports';
|
|
23
|
-
import { isScheduled, triggerLabel, fmtDuration, stagger } from '$lib/utils/format';
|
|
23
|
+
import { isScheduled, triggerLabel, fmtDuration, stagger, featureFile } from '$lib/utils/format';
|
|
24
24
|
import Badge from '$lib/components/ui/Badge.svelte';
|
|
25
25
|
import BrowserIcon from '$lib/components/icons/BrowserIcon.svelte';
|
|
26
26
|
|
|
@@ -213,24 +213,6 @@
|
|
|
213
213
|
<BrowserIcon browser={detail.browser ?? 'chromium'} />
|
|
214
214
|
{detail.browser ?? 'chromium'}
|
|
215
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
216
|
</div>
|
|
235
217
|
</div>
|
|
236
218
|
</div>
|
|
@@ -362,7 +344,7 @@
|
|
|
362
344
|
{/if}
|
|
363
345
|
</h2>
|
|
364
346
|
<div class="feature-right">
|
|
365
|
-
<span class="feature-file">{feature.uri}</span>
|
|
347
|
+
<span class="feature-file" title={feature.uri}>{featureFile(feature.uri)}</span>
|
|
366
348
|
<Badge variant={feature.status === 'passed' ? 'pass' : 'fail'}>
|
|
367
349
|
{feature.status}
|
|
368
350
|
</Badge>
|
|
@@ -590,16 +572,6 @@
|
|
|
590
572
|
padding: 0.1rem 0.45rem;
|
|
591
573
|
}
|
|
592
574
|
|
|
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
|
-
|
|
603
575
|
.header-stats {
|
|
604
576
|
display: flex;
|
|
605
577
|
gap: 1.5rem;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
let countdownInterval = null;
|
|
30
30
|
|
|
31
31
|
$: state = $runnerState;
|
|
32
|
-
$: isMulti = state.lanes.length >
|
|
32
|
+
$: isMulti = state.lanes.length > 0;
|
|
33
33
|
|
|
34
34
|
// Auto-scroll terminals
|
|
35
35
|
afterUpdate(() => {
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
>
|
|
174
174
|
<span class="run-sep">·</span>
|
|
175
175
|
<span class="run-detail">{state.currentRun.browser}</span>
|
|
176
|
-
{#if
|
|
176
|
+
{#if state.lanes.length > 1}
|
|
177
177
|
<span class="run-sep">·</span>
|
|
178
178
|
<span class="run-detail">{state.lanes.length} runners</span>
|
|
179
179
|
{/if}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plum-e2e",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/silverlunah/plum.git"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"description": "A detached test automation environment that combines Playwright and Cucumber with a Svelte frontend and an Express backend. It allows users to trigger tests, monitor reports, and schedule test runs through an intuitive UI.",
|
|
9
9
|
"main": "index.js",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"init": "npm install && npm --prefix backend install && npm --prefix backend run init && npm --prefix frontend install",
|
|
11
|
+
"init": "npm install && npm --prefix backend install && npm --prefix backend run init && npm --prefix frontend install && node bin/scaffold-tests.js",
|
|
12
12
|
"format": "prettier --write .",
|
|
13
13
|
"add-license": "npx license-check-and-add add -f license-config.json",
|
|
14
14
|
"prepare": "husky",
|
|
@@ -1,120 +0,0 @@
|
|
|
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
|
-
* Interactively registers a local node runner with the Plum primary server,
|
|
20
|
-
* then starts the node server in the foreground.
|
|
21
|
-
*
|
|
22
|
-
* Usage: node scripts/add-local-runner.js
|
|
23
|
-
* or: npm run add-local-runner (from the backend directory)
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
const readline = require('readline');
|
|
27
|
-
const { spawn } = require('child_process');
|
|
28
|
-
const path = require('path');
|
|
29
|
-
|
|
30
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
31
|
-
|
|
32
|
-
function ask(question, defaultValue) {
|
|
33
|
-
return new Promise((resolve) => {
|
|
34
|
-
const hint = defaultValue ? ` (default: ${defaultValue})` : '';
|
|
35
|
-
rl.question(` ${question}${hint}: `, (answer) => {
|
|
36
|
-
resolve(answer.trim() || defaultValue || '');
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function main() {
|
|
42
|
-
console.log('\nRegister a local node runner with the Plum server');
|
|
43
|
-
console.log('─'.repeat(51));
|
|
44
|
-
console.log('The runner URL will use host.docker.internal so that');
|
|
45
|
-
console.log('the Plum Docker container can reach your host machine.\n');
|
|
46
|
-
|
|
47
|
-
const name = await ask('Runner name', 'local-node');
|
|
48
|
-
const token = await ask('Node auth token (NODE_TOKEN)', process.env.NODE_TOKEN || 'test123');
|
|
49
|
-
const port = await ask('Port the node will run on', '3002');
|
|
50
|
-
const browser = await ask('Default browser (chromium/firefox/webkit)', 'chromium');
|
|
51
|
-
const apiUrl = await ask('Plum server API URL', 'http://localhost:3001');
|
|
52
|
-
|
|
53
|
-
rl.close();
|
|
54
|
-
|
|
55
|
-
const nodeUrl = `http://host.docker.internal:${port}`;
|
|
56
|
-
console.log(`\nRegistering "${name}" at ${nodeUrl} via ${apiUrl}...`);
|
|
57
|
-
|
|
58
|
-
let res;
|
|
59
|
-
try {
|
|
60
|
-
res = await fetch(`${apiUrl}/runners`, {
|
|
61
|
-
method: 'POST',
|
|
62
|
-
headers: { 'Content-Type': 'application/json' },
|
|
63
|
-
body: JSON.stringify({ name, url: nodeUrl, token, browser })
|
|
64
|
-
});
|
|
65
|
-
} catch (e) {
|
|
66
|
-
console.error(`\n✗ Could not reach Plum server at ${apiUrl}`);
|
|
67
|
-
console.error(` ${e.message}`);
|
|
68
|
-
console.error('\nMake sure the Docker stack is running:');
|
|
69
|
-
console.error(' docker compose up -d');
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const body = await res.json().catch(() => ({}));
|
|
74
|
-
|
|
75
|
-
if (!res.ok) {
|
|
76
|
-
console.error(`\n✗ Server returned HTTP ${res.status}`);
|
|
77
|
-
if (body.error) console.error(` ${body.error}`);
|
|
78
|
-
process.exit(1);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (body.error) {
|
|
82
|
-
console.error('\n✗ Error:', body.error);
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const runner = body.runner;
|
|
87
|
-
console.log(`\n✓ Runner "${runner.name}" registered (id: ${runner.id})`);
|
|
88
|
-
console.log(` URL: ${runner.url}`);
|
|
89
|
-
console.log(` Token: ${runner.token}`);
|
|
90
|
-
console.log(` Browser: ${runner.browser}`);
|
|
91
|
-
console.log('\nStarting node server… (Ctrl+C to stop)\n');
|
|
92
|
-
|
|
93
|
-
// Start the node server in the foreground — the script stays alive until the user kills it
|
|
94
|
-
const serverPath = path.resolve(__dirname, '..', 'server.js');
|
|
95
|
-
const child = spawn(process.execPath, [serverPath], {
|
|
96
|
-
env: {
|
|
97
|
-
...process.env,
|
|
98
|
-
NODE_TOKEN: token,
|
|
99
|
-
PLUM_MODE: 'node',
|
|
100
|
-
PORT: port
|
|
101
|
-
},
|
|
102
|
-
stdio: 'inherit',
|
|
103
|
-
cwd: path.resolve(__dirname, '..')
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
child.on('error', (e) => {
|
|
107
|
-
console.error('✗ Failed to start node server:', e.message);
|
|
108
|
-
process.exit(1);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
child.on('close', (code) => {
|
|
112
|
-
process.exit(code ?? 0);
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
main().catch((e) => {
|
|
117
|
-
rl.close();
|
|
118
|
-
console.error('\n✗', e.message);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
});
|