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 (!r.local) {
160
- clack.log.info(
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
- clack.log.info(
173
- pc.dim(
174
- `"${r.name}" is up but was not started by this manager — stop it from its own terminal.`
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
- options.push({ value: 'ping', label: 'Ping' });
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
- const ok = stopNode(r.id);
198
- clack.log.success(ok ? pc.green(`Stopped "${r.name}"`) : pc.dim(`"${r.name}" was not running`));
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
- const s = clack.spinner();
201
- s.start(`Restarting "${r.name}"...`);
202
- stopNode(r.id);
203
- await new Promise((resolve) => setTimeout(resolve, 600));
204
- const entry = startNode({ id: r.id, port, token: r.token });
205
- s.stop(pc.green(`Restarted "${r.name}" (pid ${entry.pid})`));
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plum-e2e",
3
- "version": "2.6.1",
3
+ "version": "2.6.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/silverlunah/plum.git"