plum-e2e 2.6.1 → 2.6.3
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/README.md
CHANGED
|
@@ -69,6 +69,7 @@ Full documentation is available at:
|
|
|
69
69
|
| [Initializing the Project](https://outline.silverlunah.com/s/12bf21d1-02ba-49e9-b0df-908976407afd/doc/initializing-the-project-ilfc8LUyO7) | What `plum init` generates, config files explained |
|
|
70
70
|
| [Writing Tests](https://outline.silverlunah.com/s/12bf21d1-02ba-49e9-b0df-908976407afd/doc/writing-tests-XeHJQdtH49) | Feature files, page objects, step definitions, best practices |
|
|
71
71
|
| [Running Tests Locally](https://outline.silverlunah.com/s/12bf21d1-02ba-49e9-b0df-908976407afd/doc/running-tests-locally-GGhFcqaAQ8) | `plum run-test` flags, parallel runs, debugging tips |
|
|
72
|
+
| [Retrying Flaky Tests](https://outline.silverlunah.com/s/12bf21d1-02ba-49e9-b0df-908976407afd/doc/retrying-flaky-tests-NXwRF5SXru) | Auto-retry failed scenarios, global setting, report badges |
|
|
72
73
|
| [Setting Up the Server](https://outline.silverlunah.com/s/12bf21d1-02ba-49e9-b0df-908976407afd/doc/setting-up-the-server-vj0Ab1kJVs) | Production server setup, reverse proxy (Nginx/Caddy), Docker |
|
|
73
74
|
| [Setting Up Nodes](https://outline.silverlunah.com/s/12bf21d1-02ba-49e9-b0df-908976407afd/doc/setting-up-nodes-dtmekJGJia) | Runner nodes, systemd service, managing nodes |
|
|
74
75
|
| [Integrations](https://outline.silverlunah.com/s/12bf21d1-02ba-49e9-b0df-908976407afd/doc/integrations-qfiqfmdP0j) | Discord & Slack webhook notifications, CI/external triggers |
|
|
@@ -94,6 +95,7 @@ Full documentation is available at:
|
|
|
94
95
|
| `plum run-test @tag` | Run tests matching a tag |
|
|
95
96
|
| `plum run-test --parallel N` | Run tests across N parallel workers |
|
|
96
97
|
| `plum run-test --browser <b>` | Run in `chromium` (default) or `firefox` |
|
|
98
|
+
| `plum run-test --help` | Show usage for `run-test` |
|
|
97
99
|
| `plum create-step` | Interactively scaffold a new step definition |
|
|
98
100
|
| `plum manage-runners` | Open the interactive runner management menu |
|
|
99
101
|
|
|
@@ -88,6 +88,17 @@ async function deleteRunner(id) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Stops/restarts a runner over the network via the primary's control routes,
|
|
93
|
+
* which hit the runner's own /api/shutdown|restart endpoints — works for any
|
|
94
|
+
* reachable runner, not just ones whose process this manager owns by PID.
|
|
95
|
+
*/
|
|
96
|
+
async function controlRunner(id, action) {
|
|
97
|
+
const res = await fetch(`${API_URL}/runners/${id}/${action}`, { method: 'POST' });
|
|
98
|
+
const body = await res.json().catch(() => ({}));
|
|
99
|
+
if (!res.ok || body.ok === false) throw new Error(body.error || `HTTP ${res.status}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
/**
|
|
92
103
|
* Resolves the display + control state for every runner: reachability (ping),
|
|
93
104
|
* whether we own a live process for it, and whether we can control it at all.
|
|
@@ -156,12 +167,8 @@ function prepareNodeEnv() {
|
|
|
156
167
|
async function runAction(r) {
|
|
157
168
|
const options = [];
|
|
158
169
|
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
pc.dim(`"${r.name}" runs on another machine — it can be pinged but not controlled here.`)
|
|
162
|
-
);
|
|
163
|
-
options.push({ value: 'ping', label: 'Ping' });
|
|
164
|
-
} else if (r.managed) {
|
|
170
|
+
if (r.managed) {
|
|
171
|
+
// Local, and this manager owns its process — control it directly by PID.
|
|
165
172
|
options.push(
|
|
166
173
|
{ value: 'stop', label: pc.red('Stop') },
|
|
167
174
|
{ value: 'restart', label: pc.yellow('Restart') },
|
|
@@ -169,14 +176,18 @@ async function runAction(r) {
|
|
|
169
176
|
{ value: 'ping', label: 'Ping' }
|
|
170
177
|
);
|
|
171
178
|
} else if (r.online) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
179
|
+
// Remote, or local but started outside this manager (no PID to own) —
|
|
180
|
+
// either way the runner's own /api/shutdown|restart endpoints are
|
|
181
|
+
// reachable over the network via the primary's control routes.
|
|
182
|
+
options.push(
|
|
183
|
+
{ value: 'stop', label: pc.red('Stop') },
|
|
184
|
+
{ value: 'restart', label: pc.yellow('Restart') },
|
|
185
|
+
{ value: 'ping', label: 'Ping' }
|
|
176
186
|
);
|
|
177
|
-
|
|
178
|
-
} else {
|
|
187
|
+
} else if (r.local) {
|
|
179
188
|
options.push({ value: 'start', label: pc.green('Start') }, { value: 'ping', label: 'Ping' });
|
|
189
|
+
} else {
|
|
190
|
+
options.push({ value: 'ping', label: 'Ping' });
|
|
180
191
|
}
|
|
181
192
|
|
|
182
193
|
options.push(
|
|
@@ -194,15 +205,39 @@ async function runAction(r) {
|
|
|
194
205
|
const entry = startNode({ id: r.id, port, token: r.token });
|
|
195
206
|
clack.log.success(pc.green(`Started "${r.name}" on port ${port} (pid ${entry.pid})`));
|
|
196
207
|
} else if (action === 'stop') {
|
|
197
|
-
|
|
198
|
-
|
|
208
|
+
if (r.managed) {
|
|
209
|
+
const ok = stopNode(r.id);
|
|
210
|
+
clack.log.success(
|
|
211
|
+
ok ? pc.green(`Stopped "${r.name}"`) : pc.dim(`"${r.name}" was not running`)
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
const s = clack.spinner();
|
|
215
|
+
s.start(`Stopping "${r.name}"...`);
|
|
216
|
+
try {
|
|
217
|
+
await controlRunner(r.id, 'stop');
|
|
218
|
+
s.stop(pc.green(`Stopped "${r.name}"`));
|
|
219
|
+
} catch (e) {
|
|
220
|
+
s.stop(pc.red(`Could not stop "${r.name}": ${e.message}`));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
199
223
|
} else if (action === 'restart') {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
224
|
+
if (r.managed) {
|
|
225
|
+
const s = clack.spinner();
|
|
226
|
+
s.start(`Restarting "${r.name}"...`);
|
|
227
|
+
stopNode(r.id);
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
229
|
+
const entry = startNode({ id: r.id, port, token: r.token });
|
|
230
|
+
s.stop(pc.green(`Restarted "${r.name}" (pid ${entry.pid})`));
|
|
231
|
+
} else {
|
|
232
|
+
const s = clack.spinner();
|
|
233
|
+
s.start(`Restarting "${r.name}"...`);
|
|
234
|
+
try {
|
|
235
|
+
await controlRunner(r.id, 'restart');
|
|
236
|
+
s.stop(pc.green(`Restarted "${r.name}"`));
|
|
237
|
+
} catch (e) {
|
|
238
|
+
s.stop(pc.red(`Could not restart "${r.name}": ${e.message}`));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
206
241
|
} else if (action === 'log') {
|
|
207
242
|
const entry = runnerProcess.loadRegistry()[r.id];
|
|
208
243
|
clack.note(entry?.logFile ?? '(no log file)', 'Log file');
|
package/bin/plum.js
CHANGED
|
@@ -1096,6 +1096,23 @@ switch (command) {
|
|
|
1096
1096
|
break;
|
|
1097
1097
|
|
|
1098
1098
|
case 'run-test': {
|
|
1099
|
+
const runHelpArgs = process.argv.slice(3);
|
|
1100
|
+
if (anyFlags(runHelpArgs, ['--help', '-h'])) {
|
|
1101
|
+
console.log(
|
|
1102
|
+
[
|
|
1103
|
+
'',
|
|
1104
|
+
`${pc.bold('Usage:')} plum run-test [tag] [options]`,
|
|
1105
|
+
'',
|
|
1106
|
+
` ${pc.cyan('plum run-test')} run all tests`,
|
|
1107
|
+
` ${pc.cyan('plum run-test @tag')} run tests matching a tag`,
|
|
1108
|
+
` ${pc.cyan('plum run-test --parallel N')} run tests across N parallel workers`,
|
|
1109
|
+
` ${pc.cyan('plum run-test --browser firefox')} run in a specific browser (chromium/firefox)`,
|
|
1110
|
+
''
|
|
1111
|
+
].join('\n')
|
|
1112
|
+
);
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1099
1116
|
console.log('--------------------------------------\n');
|
|
1100
1117
|
console.log('🚀 Running tests locally...');
|
|
1101
1118
|
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { runsVersion } from '$lib/stores/runner';
|
|
31
31
|
import { auth } from '$lib/stores/auth';
|
|
32
32
|
import Button from '$lib/components/ui/Button.svelte';
|
|
33
|
+
import Modal from '$lib/components/ui/Modal.svelte';
|
|
33
34
|
import Toast from '$lib/components/ui/Toast.svelte';
|
|
34
35
|
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
|
35
36
|
import AutomatedBadge from '$lib/components/ui/AutomatedBadge.svelte';
|
|
@@ -106,6 +107,10 @@
|
|
|
106
107
|
let entryNote = '';
|
|
107
108
|
let expandedExecEntries = new Set();
|
|
108
109
|
|
|
110
|
+
let editRunOpen = false;
|
|
111
|
+
let editRunForm = {};
|
|
112
|
+
let editRunSaving = false;
|
|
113
|
+
|
|
109
114
|
function toggleExecSteps(entryId) {
|
|
110
115
|
if (expandedExecEntries.has(entryId)) expandedExecEntries.delete(entryId);
|
|
111
116
|
else expandedExecEntries.add(entryId);
|
|
@@ -203,6 +208,20 @@
|
|
|
203
208
|
run = { ...run, entries: run.entries.filter((e) => e.id !== entryId) };
|
|
204
209
|
}
|
|
205
210
|
|
|
211
|
+
async function handleUpdateRun() {
|
|
212
|
+
editRunSaving = true;
|
|
213
|
+
try {
|
|
214
|
+
const updated = await updateRun(runId, editRunForm);
|
|
215
|
+
run = { ...run, ...updated };
|
|
216
|
+
editRunOpen = false;
|
|
217
|
+
showToast('success', 'Run updated.');
|
|
218
|
+
} catch (e) {
|
|
219
|
+
showToast('error', e.message);
|
|
220
|
+
} finally {
|
|
221
|
+
editRunSaving = false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
206
225
|
async function handleSaveRun() {
|
|
207
226
|
saving = true;
|
|
208
227
|
try {
|
|
@@ -309,6 +328,21 @@
|
|
|
309
328
|
|
|
310
329
|
<Toast {toast} />
|
|
311
330
|
|
|
331
|
+
<Modal bind:open={editRunOpen} title="Edit Run">
|
|
332
|
+
<div class="form-fields">
|
|
333
|
+
<div class="field">
|
|
334
|
+
<label class="field-label" for="er-title">Title</label>
|
|
335
|
+
<input id="er-title" type="text" class="field-input" bind:value={editRunForm.title} />
|
|
336
|
+
</div>
|
|
337
|
+
<div class="modal-actions">
|
|
338
|
+
<Button on:click={handleUpdateRun} disabled={editRunSaving}>
|
|
339
|
+
{editRunSaving ? 'Saving…' : 'Save'}
|
|
340
|
+
</Button>
|
|
341
|
+
<Button variant="ghost" on:click={() => (editRunOpen = false)}>Cancel</Button>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
</Modal>
|
|
345
|
+
|
|
312
346
|
<div class="breadcrumb">
|
|
313
347
|
<a href="/test-repository" class="bc-link">Test Repository</a>
|
|
314
348
|
<span class="bc-sep">›</span>
|
|
@@ -322,6 +356,28 @@
|
|
|
322
356
|
<div class="run-header-left">
|
|
323
357
|
<h1 class="run-title">{run.title}</h1>
|
|
324
358
|
<span class="run-status-badge {run.status}">{run.status}</span>
|
|
359
|
+
<button
|
|
360
|
+
class="icon-btn"
|
|
361
|
+
title="Edit run"
|
|
362
|
+
on:click={() => {
|
|
363
|
+
editRunForm = { title: run.title };
|
|
364
|
+
editRunOpen = true;
|
|
365
|
+
}}
|
|
366
|
+
>
|
|
367
|
+
<svg
|
|
368
|
+
width="14"
|
|
369
|
+
height="14"
|
|
370
|
+
viewBox="0 0 24 24"
|
|
371
|
+
fill="none"
|
|
372
|
+
stroke="currentColor"
|
|
373
|
+
stroke-width="2"
|
|
374
|
+
stroke-linecap="round"
|
|
375
|
+
stroke-linejoin="round"
|
|
376
|
+
><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" /><path
|
|
377
|
+
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
|
378
|
+
/></svg
|
|
379
|
+
>
|
|
380
|
+
</button>
|
|
325
381
|
</div>
|
|
326
382
|
<div class="run-header-actions">
|
|
327
383
|
{#if isLocked}
|
|
@@ -1427,4 +1483,15 @@
|
|
|
1427
1483
|
display: none;
|
|
1428
1484
|
}
|
|
1429
1485
|
}
|
|
1486
|
+
|
|
1487
|
+
.form-fields {
|
|
1488
|
+
display: flex;
|
|
1489
|
+
flex-direction: column;
|
|
1490
|
+
gap: 0.875rem;
|
|
1491
|
+
}
|
|
1492
|
+
.modal-actions {
|
|
1493
|
+
display: flex;
|
|
1494
|
+
gap: 0.5rem;
|
|
1495
|
+
padding-top: 0.25rem;
|
|
1496
|
+
}
|
|
1430
1497
|
</style>
|