plum-e2e 1.3.7 → 2.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/README.md +111 -3
- package/backend/app.js +5 -0
- package/backend/config/scripts/create-test.mjs +172 -0
- package/backend/config/scripts/generate-report.js +2 -1
- package/backend/lib/runnerProcess.js +50 -4
- package/backend/logs/runner-cmqneqerz0000qq01i5ap2rvl.log +22 -0
- package/backend/logs/runner-cmqnfv7kr0000r101aeocm8eu.log +20 -0
- package/backend/logs/runner-cmqnfvb560001r101qoi0phau.log +43 -0
- package/backend/logs/runner-cmqnfvlm20002r101gsyqb837.log +20 -0
- package/backend/logs/runner-cmqnfvqfy0003r101fh41pzx3.log +20 -0
- package/backend/logs/runner-cmqnfvvwo0004r101q4dtqxd2.log +20 -0
- package/backend/middleware/jwtAuth.js +33 -0
- package/backend/middleware/requireAdmin.js +25 -0
- package/backend/package.json +2 -0
- package/backend/prisma/migrations/20260618000000_add_test_repository/migration.sql +133 -0
- package/backend/prisma/migrations/20260618000001_add_user_roles/migration.sql +3 -0
- package/backend/prisma/migrations/20260618000002_drop_automated_tag/migration.sql +2 -0
- package/backend/prisma/migrations/20260618000003_entry_assignee/migration.sql +2 -0
- package/backend/prisma/migrations/20260621000000_add_notifications/migration.sql +8 -0
- package/backend/prisma/schema.prisma +123 -10
- package/backend/routes/auth.routes.js +96 -0
- package/backend/routes/node.routes.js +9 -0
- package/backend/routes/runners.routes.js +10 -0
- package/backend/routes/settings.routes.js +71 -8
- package/backend/routes/test-cases.routes.js +80 -0
- package/backend/routes/test-runs.routes.js +122 -0
- package/backend/routes/test-suites.routes.js +92 -0
- package/backend/routes/users.routes.js +67 -0
- package/backend/scripts/create-test.js +7 -6
- package/backend/scripts/manage-runners.mjs +49 -8
- package/backend/server.js +22 -1
- package/backend/services/cronService.js +91 -7
- package/backend/services/notificationService.js +163 -0
- package/backend/services/reportService.js +96 -4
- package/backend/services/settingsService.js +46 -2
- package/backend/services/testCaseService.js +139 -0
- package/backend/services/testRunService.js +203 -0
- package/backend/services/testSuiteService.js +191 -0
- package/backend/services/userService.js +114 -0
- package/backend/websockets/socketHandler.js +96 -7
- package/bin/plum.js +105 -9
- package/frontend/src/lib/api/auth.js +69 -0
- package/frontend/src/lib/api/repository.js +256 -0
- package/frontend/src/lib/api/schedules.js +5 -1
- package/frontend/src/lib/api/settings.js +15 -0
- package/frontend/src/lib/api/users.js +52 -0
- package/frontend/src/lib/components/layout/Nav.svelte +116 -4
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +321 -31
- package/frontend/src/lib/components/ui/Modal.svelte +8 -1
- package/frontend/src/lib/constants.js +2 -0
- package/frontend/src/lib/stores/auth.js +60 -0
- package/frontend/src/lib/stores/runner.js +11 -2
- package/frontend/src/routes/+layout.svelte +32 -4
- package/frontend/src/routes/+page.svelte +1 -1
- package/frontend/src/routes/login/+page.svelte +209 -0
- package/frontend/src/routes/scheduled-tests/+page.svelte +65 -7
- package/frontend/src/routes/settings/+page.svelte +677 -6
- package/frontend/src/routes/setup/+page.svelte +249 -0
- package/frontend/src/routes/test-repository/+page.svelte +1379 -0
- package/frontend/src/routes/test-repository/runs/[id]/+page.svelte +1549 -0
- package/frontend/src/routes/test-repository/suites/[id]/+page.svelte +1490 -0
- package/package.json +1 -1
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
<script>
|
|
19
19
|
import { onMount, onDestroy } from 'svelte';
|
|
20
20
|
import { fly, slide } from 'svelte/transition';
|
|
21
|
-
import { goto } from '$app/navigation';
|
|
22
21
|
import { io } from 'socket.io-client';
|
|
23
22
|
import {
|
|
24
23
|
socket,
|
|
@@ -29,17 +28,27 @@
|
|
|
29
28
|
triggerRun,
|
|
30
29
|
testsVersion,
|
|
31
30
|
reportsVersion,
|
|
31
|
+
runsVersion,
|
|
32
32
|
activeCronJobs
|
|
33
33
|
} from '$lib/stores/runner';
|
|
34
34
|
import { fetchLatestReportId, reportUrl } from '$lib/api/reports';
|
|
35
35
|
import { fetchRunners } from '$lib/api/runners';
|
|
36
|
+
import { fetchRuns, fetchRun } from '$lib/api/repository';
|
|
37
|
+
import { fetchIntegrations } from '$lib/api/settings';
|
|
36
38
|
import { API_BASE, BROWSERS } from '$lib/constants';
|
|
37
39
|
import ConfirmModal from '$lib/components/ui/ConfirmModal.svelte';
|
|
38
40
|
|
|
39
41
|
let availableRunners = [];
|
|
42
|
+
let testRuns = [];
|
|
43
|
+
let selectedRun = null; // { id, title, tags: string[] | null }
|
|
44
|
+
let selectedRunLoading = false;
|
|
40
45
|
let browserOpen = false;
|
|
41
46
|
let runnersOpen = false;
|
|
47
|
+
let runPickOpen = false;
|
|
42
48
|
let runAllModalOpen = false;
|
|
49
|
+
let integrations = { discordWebhookUrl: '', slackWebhookUrl: '', notifyPublicUrl: '' };
|
|
50
|
+
let notifyDiscord = false;
|
|
51
|
+
let notifySlack = false;
|
|
43
52
|
|
|
44
53
|
function clickOutside(node) {
|
|
45
54
|
function handle(e) {
|
|
@@ -82,6 +91,10 @@
|
|
|
82
91
|
})
|
|
83
92
|
.catch(() => {});
|
|
84
93
|
|
|
94
|
+
fetchIntegrations()
|
|
95
|
+
.then((i) => (integrations = i))
|
|
96
|
+
.catch(() => {});
|
|
97
|
+
|
|
85
98
|
_unsubConfig = runnerConfig.subscribe((v) => {
|
|
86
99
|
try {
|
|
87
100
|
localStorage.setItem('plum:runnerConfig', JSON.stringify(v));
|
|
@@ -105,7 +118,7 @@
|
|
|
105
118
|
});
|
|
106
119
|
});
|
|
107
120
|
|
|
108
|
-
const s = io(API_BASE);
|
|
121
|
+
const s = io(API_BASE, { transports: ['websocket'] });
|
|
109
122
|
_socket = s;
|
|
110
123
|
socket.set(s);
|
|
111
124
|
|
|
@@ -189,6 +202,11 @@
|
|
|
189
202
|
$: anyCronRunning = cronJobs.length > 0;
|
|
190
203
|
$: anyRunning = state.running || anyCronRunning;
|
|
191
204
|
|
|
205
|
+
$: if ($runsVersion >= 0)
|
|
206
|
+
fetchRuns({ limit: 200 })
|
|
207
|
+
.then((r) => (testRuns = r.runs))
|
|
208
|
+
.catch(() => {});
|
|
209
|
+
|
|
192
210
|
$: statusColor =
|
|
193
211
|
state.status === 'pass'
|
|
194
212
|
? 'var(--pass)'
|
|
@@ -219,16 +237,42 @@
|
|
|
219
237
|
? (availableRunners.find((r) => r.id === cfg.selectedRunners[0])?.name ?? '1 node')
|
|
220
238
|
: `${cfg.selectedRunners.length} nodes`;
|
|
221
239
|
|
|
240
|
+
async function selectRun(run) {
|
|
241
|
+
runPickOpen = false;
|
|
242
|
+
selectedRun = { id: run.id, title: run.title, tags: null };
|
|
243
|
+
selectedRunLoading = true;
|
|
244
|
+
try {
|
|
245
|
+
const full = await fetchRun(run.id);
|
|
246
|
+
const tags = full.entries
|
|
247
|
+
.filter((e) => e.case?.isAutomated)
|
|
248
|
+
.map((e) => `@${e.case.displayId}`);
|
|
249
|
+
selectedRun = { id: run.id, title: run.title, tags };
|
|
250
|
+
} catch {
|
|
251
|
+
selectedRun = null;
|
|
252
|
+
} finally {
|
|
253
|
+
selectedRunLoading = false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function clearSelectedRun() {
|
|
258
|
+
selectedRun = null;
|
|
259
|
+
}
|
|
260
|
+
|
|
222
261
|
function handleRunClick() {
|
|
223
|
-
|
|
262
|
+
const notify = { notifyDiscord, notifySlack };
|
|
263
|
+
if (selectedRun) {
|
|
264
|
+
if (selectedRunLoading || !selectedRun.tags) return;
|
|
265
|
+
if (selectedRun.tags.length === 0) return;
|
|
266
|
+
triggerRun(selectedRun.tags.join(' or '), selectedRun.id, notify);
|
|
267
|
+
} else if ($runnerConfig.testID.trim() === '') {
|
|
224
268
|
runAllModalOpen = true;
|
|
225
269
|
} else {
|
|
226
|
-
triggerRun();
|
|
270
|
+
triggerRun(undefined, undefined, notify);
|
|
227
271
|
}
|
|
228
272
|
}
|
|
229
273
|
|
|
230
274
|
function handleKeydown(e) {
|
|
231
|
-
if (e.key === 'Enter' && !state.running) handleRunClick();
|
|
275
|
+
if (e.key === 'Enter' && !state.running && !selectedRun) handleRunClick();
|
|
232
276
|
}
|
|
233
277
|
|
|
234
278
|
function adjustWorkers(delta) {
|
|
@@ -259,7 +303,7 @@
|
|
|
259
303
|
confirmLabel="Run all tests"
|
|
260
304
|
on:confirm={() => {
|
|
261
305
|
runAllModalOpen = false;
|
|
262
|
-
triggerRun();
|
|
306
|
+
triggerRun(undefined, undefined, { notifyDiscord, notifySlack });
|
|
263
307
|
}}
|
|
264
308
|
>
|
|
265
309
|
No tag or filter is set. This will run <strong>every test</strong> in the suite, which may take a while.
|
|
@@ -312,30 +356,115 @@
|
|
|
312
356
|
|
|
313
357
|
<!-- Middle: tag filter + browser + workers + runner dropdowns -->
|
|
314
358
|
<div class="bar-center">
|
|
315
|
-
<!--
|
|
316
|
-
|
|
317
|
-
<
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
359
|
+
<!-- Test Run picker OR tag input -->
|
|
360
|
+
{#if selectedRun}
|
|
361
|
+
<div class="run-chip" class:loading={selectedRunLoading}>
|
|
362
|
+
<svg
|
|
363
|
+
width="9"
|
|
364
|
+
height="10"
|
|
365
|
+
viewBox="0 0 10 12"
|
|
366
|
+
fill="currentColor"
|
|
367
|
+
stroke="none"
|
|
368
|
+
style="opacity:0.6;flex-shrink:0"><polygon points="0,0 10,6 0,12" /></svg
|
|
369
|
+
>
|
|
370
|
+
<span class="run-chip-title">{selectedRun.title}</span>
|
|
371
|
+
{#if selectedRunLoading}
|
|
372
|
+
<span class="run-chip-spinner"></span>
|
|
373
|
+
{:else if selectedRun.tags !== null}
|
|
374
|
+
<span class="run-chip-count" class:none={selectedRun.tags.length === 0}>
|
|
375
|
+
{selectedRun.tags.length} automated
|
|
376
|
+
</span>
|
|
377
|
+
{/if}
|
|
378
|
+
<button class="run-chip-clear" on:click={clearSelectedRun} aria-label="Clear test run">
|
|
379
|
+
<svg width="9" height="9" viewBox="0 0 14 14" fill="none"
|
|
380
|
+
><path
|
|
381
|
+
d="M1 1l12 12M13 1L1 13"
|
|
382
|
+
stroke="currentColor"
|
|
383
|
+
stroke-width="1.5"
|
|
384
|
+
stroke-linecap="round"
|
|
385
|
+
/></svg
|
|
386
|
+
>
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
{:else}
|
|
390
|
+
<div class="input-wrap">
|
|
391
|
+
<svg
|
|
392
|
+
class="input-icon"
|
|
393
|
+
width="12"
|
|
394
|
+
height="12"
|
|
395
|
+
viewBox="0 0 24 24"
|
|
396
|
+
fill="none"
|
|
397
|
+
stroke="currentColor"
|
|
398
|
+
stroke-width="2"
|
|
399
|
+
stroke-linecap="round"
|
|
400
|
+
stroke-linejoin="round"
|
|
401
|
+
>
|
|
402
|
+
<circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
403
|
+
</svg>
|
|
404
|
+
<input
|
|
405
|
+
type="text"
|
|
406
|
+
class="tag-input"
|
|
407
|
+
value={$runnerConfig.testID}
|
|
408
|
+
placeholder="@tag or leave blank for all tests"
|
|
409
|
+
on:input={(e) => runnerConfig.update((c) => ({ ...c, testID: e.currentTarget.value }))}
|
|
410
|
+
on:keydown={handleKeydown}
|
|
411
|
+
disabled={state.running}
|
|
412
|
+
/>
|
|
413
|
+
</div>
|
|
414
|
+
{/if}
|
|
415
|
+
|
|
416
|
+
<!-- Test run dropdown -->
|
|
417
|
+
<div class="ctrl-group">
|
|
418
|
+
<span class="ctrl-label">Test Run</span>
|
|
419
|
+
<div class="dropdown-wrap" use:clickOutside on:clickoutside={() => (runPickOpen = false)}>
|
|
420
|
+
<button
|
|
421
|
+
class="dropdown-trigger"
|
|
422
|
+
class:open={runPickOpen}
|
|
423
|
+
class:has-remote={!!selectedRun}
|
|
424
|
+
on:click={() => {
|
|
425
|
+
if (!state.running) runPickOpen = !runPickOpen;
|
|
426
|
+
}}
|
|
427
|
+
disabled={state.running}
|
|
428
|
+
>
|
|
429
|
+
<span>{selectedRun ? selectedRun.title : 'None'}</span>
|
|
430
|
+
<svg
|
|
431
|
+
width="10"
|
|
432
|
+
height="10"
|
|
433
|
+
viewBox="0 0 24 24"
|
|
434
|
+
fill="none"
|
|
435
|
+
stroke="currentColor"
|
|
436
|
+
stroke-width="2.5"
|
|
437
|
+
stroke-linecap="round"
|
|
438
|
+
class="trigger-chevron"
|
|
439
|
+
class:open={runPickOpen}
|
|
440
|
+
>
|
|
441
|
+
<polyline points="6 9 12 15 18 9" />
|
|
442
|
+
</svg>
|
|
443
|
+
</button>
|
|
444
|
+
{#if runPickOpen}
|
|
445
|
+
<div class="dropdown-menu run-pick-menu" transition:fly={{ y: 6, duration: 130 }}>
|
|
446
|
+
{#if selectedRun}
|
|
447
|
+
<button class="dropdown-item" on:click={clearSelectedRun}>
|
|
448
|
+
<span style="color:var(--text-muted)">✕</span> Clear
|
|
449
|
+
</button>
|
|
450
|
+
<div class="dropdown-divider"></div>
|
|
451
|
+
{/if}
|
|
452
|
+
{#if testRuns.filter((r) => r.status !== 'complete').length === 0}
|
|
453
|
+
<div class="dropdown-empty">No active test runs</div>
|
|
454
|
+
{:else}
|
|
455
|
+
{#each testRuns.filter((r) => r.status !== 'complete') as run}
|
|
456
|
+
<button
|
|
457
|
+
class="dropdown-item"
|
|
458
|
+
class:active={selectedRun?.id === run.id}
|
|
459
|
+
on:click={() => selectRun(run)}
|
|
460
|
+
>
|
|
461
|
+
{run.title}
|
|
462
|
+
</button>
|
|
463
|
+
{/each}
|
|
464
|
+
{/if}
|
|
465
|
+
</div>
|
|
466
|
+
{/if}
|
|
467
|
+
</div>
|
|
339
468
|
</div>
|
|
340
469
|
|
|
341
470
|
<div class="ctrl-divider"></div>
|
|
@@ -466,6 +595,35 @@
|
|
|
466
595
|
</div>
|
|
467
596
|
{/if}
|
|
468
597
|
|
|
598
|
+
{#if integrations.discordWebhookUrl || integrations.slackWebhookUrl}
|
|
599
|
+
<div class="ctrl-divider"></div>
|
|
600
|
+
<div class="ctrl-group">
|
|
601
|
+
<span class="ctrl-label">Notify</span>
|
|
602
|
+
<div class="notify-toggles">
|
|
603
|
+
{#if integrations.discordWebhookUrl}
|
|
604
|
+
<button
|
|
605
|
+
type="button"
|
|
606
|
+
class="notify-btn"
|
|
607
|
+
class:active={notifyDiscord}
|
|
608
|
+
on:click={() => (notifyDiscord = !notifyDiscord)}
|
|
609
|
+
title={notifyDiscord ? 'Discord notification on' : 'Discord notification off'}
|
|
610
|
+
disabled={state.running}>Discord</button
|
|
611
|
+
>
|
|
612
|
+
{/if}
|
|
613
|
+
{#if integrations.slackWebhookUrl}
|
|
614
|
+
<button
|
|
615
|
+
type="button"
|
|
616
|
+
class="notify-btn"
|
|
617
|
+
class:active={notifySlack}
|
|
618
|
+
on:click={() => (notifySlack = !notifySlack)}
|
|
619
|
+
title={notifySlack ? 'Slack notification on' : 'Slack notification off'}
|
|
620
|
+
disabled={state.running}>Slack</button
|
|
621
|
+
>
|
|
622
|
+
{/if}
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
{/if}
|
|
626
|
+
|
|
469
627
|
<div class="ctrl-divider"></div>
|
|
470
628
|
|
|
471
629
|
<!-- Run button -->
|
|
@@ -473,7 +631,12 @@
|
|
|
473
631
|
class="run-btn"
|
|
474
632
|
class:is-running={state.running}
|
|
475
633
|
on:click={handleRunClick}
|
|
476
|
-
disabled={state.running
|
|
634
|
+
disabled={state.running ||
|
|
635
|
+
selectedRunLoading ||
|
|
636
|
+
(selectedRun && selectedRun.tags?.length === 0)}
|
|
637
|
+
title={selectedRun && selectedRun.tags?.length === 0
|
|
638
|
+
? 'No automated cases in this run'
|
|
639
|
+
: undefined}
|
|
477
640
|
>
|
|
478
641
|
{#if state.running}
|
|
479
642
|
<span class="run-spinner"></span>
|
|
@@ -1029,6 +1192,44 @@
|
|
|
1029
1192
|
}
|
|
1030
1193
|
}
|
|
1031
1194
|
|
|
1195
|
+
/* Notification toggles */
|
|
1196
|
+
.notify-toggles {
|
|
1197
|
+
display: flex;
|
|
1198
|
+
gap: 0.25rem;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
.notify-btn {
|
|
1202
|
+
height: 26px;
|
|
1203
|
+
padding: 0 0.5rem;
|
|
1204
|
+
border-radius: var(--radius-sm);
|
|
1205
|
+
border: 1px solid var(--border);
|
|
1206
|
+
background: transparent;
|
|
1207
|
+
color: var(--text-muted);
|
|
1208
|
+
font-size: 0.75rem;
|
|
1209
|
+
font-family: inherit;
|
|
1210
|
+
cursor: pointer;
|
|
1211
|
+
transition:
|
|
1212
|
+
background var(--duration-fast),
|
|
1213
|
+
color var(--duration-fast),
|
|
1214
|
+
border-color var(--duration-fast);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
.notify-btn:hover:not(:disabled) {
|
|
1218
|
+
color: var(--text);
|
|
1219
|
+
border-color: var(--text-muted);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
.notify-btn.active {
|
|
1223
|
+
background: var(--accent);
|
|
1224
|
+
border-color: var(--accent);
|
|
1225
|
+
color: #fff;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
.notify-btn:disabled {
|
|
1229
|
+
opacity: 0.4;
|
|
1230
|
+
cursor: default;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1032
1233
|
/* Expand button */
|
|
1033
1234
|
.expand-btn {
|
|
1034
1235
|
display: flex;
|
|
@@ -1187,4 +1388,93 @@
|
|
|
1187
1388
|
.empty-icon {
|
|
1188
1389
|
opacity: 0.4;
|
|
1189
1390
|
}
|
|
1391
|
+
|
|
1392
|
+
/* ── Run chip (selected test run display) ── */
|
|
1393
|
+
.run-chip {
|
|
1394
|
+
display: inline-flex;
|
|
1395
|
+
align-items: center;
|
|
1396
|
+
gap: 0.4rem;
|
|
1397
|
+
height: 28px;
|
|
1398
|
+
padding: 0 0.5rem 0 0.625rem;
|
|
1399
|
+
background: var(--accent-soft);
|
|
1400
|
+
border: 1px solid var(--accent);
|
|
1401
|
+
border-radius: var(--radius-sm);
|
|
1402
|
+
color: var(--accent);
|
|
1403
|
+
font-size: 0.78rem;
|
|
1404
|
+
white-space: nowrap;
|
|
1405
|
+
max-width: 200px;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
.run-chip-title {
|
|
1409
|
+
overflow: hidden;
|
|
1410
|
+
text-overflow: ellipsis;
|
|
1411
|
+
white-space: nowrap;
|
|
1412
|
+
flex: 1;
|
|
1413
|
+
min-width: 0;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
.run-chip-count {
|
|
1417
|
+
font-size: 0.65rem;
|
|
1418
|
+
font-weight: 600;
|
|
1419
|
+
letter-spacing: 0.04em;
|
|
1420
|
+
opacity: 0.75;
|
|
1421
|
+
white-space: nowrap;
|
|
1422
|
+
flex-shrink: 0;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
.run-chip-count.none {
|
|
1426
|
+
color: var(--fail);
|
|
1427
|
+
opacity: 1;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
.run-chip-spinner {
|
|
1431
|
+
width: 9px;
|
|
1432
|
+
height: 9px;
|
|
1433
|
+
border: 1.5px solid color-mix(in srgb, var(--accent) 35%, transparent);
|
|
1434
|
+
border-top-color: var(--accent);
|
|
1435
|
+
border-radius: 50%;
|
|
1436
|
+
animation: spin 0.65s linear infinite;
|
|
1437
|
+
flex-shrink: 0;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
.run-chip-clear {
|
|
1441
|
+
display: flex;
|
|
1442
|
+
align-items: center;
|
|
1443
|
+
justify-content: center;
|
|
1444
|
+
width: 16px;
|
|
1445
|
+
height: 16px;
|
|
1446
|
+
border: none;
|
|
1447
|
+
background: transparent;
|
|
1448
|
+
color: var(--accent);
|
|
1449
|
+
cursor: pointer;
|
|
1450
|
+
border-radius: 2px;
|
|
1451
|
+
opacity: 0.7;
|
|
1452
|
+
padding: 0;
|
|
1453
|
+
flex-shrink: 0;
|
|
1454
|
+
transition: opacity var(--duration-fast);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
.run-chip-clear:hover {
|
|
1458
|
+
opacity: 1;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
.run-pick-menu {
|
|
1462
|
+
min-width: 180px;
|
|
1463
|
+
max-height: 240px;
|
|
1464
|
+
overflow-y: auto;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
.dropdown-divider {
|
|
1468
|
+
height: 1px;
|
|
1469
|
+
background: var(--border);
|
|
1470
|
+
margin: 0.25rem 0;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
.dropdown-empty {
|
|
1474
|
+
font-size: 0.8125rem;
|
|
1475
|
+
color: var(--text-muted);
|
|
1476
|
+
padding: 0.35rem 0.5rem;
|
|
1477
|
+
text-align: center;
|
|
1478
|
+
opacity: 0.7;
|
|
1479
|
+
}
|
|
1190
1480
|
</style>
|
|
@@ -22,12 +22,18 @@
|
|
|
22
22
|
export let open = false;
|
|
23
23
|
export let title = '';
|
|
24
24
|
|
|
25
|
+
let mousedownOnBackdrop = false;
|
|
26
|
+
|
|
25
27
|
function close() {
|
|
26
28
|
open = false;
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
function onBackdropMousedown(e) {
|
|
32
|
+
mousedownOnBackdrop = e.target === e.currentTarget;
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
function onBackdrop(e) {
|
|
30
|
-
if (e.target === e.currentTarget) close();
|
|
36
|
+
if (e.target === e.currentTarget && mousedownOnBackdrop) close();
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
function onKeydown(e) {
|
|
@@ -41,6 +47,7 @@
|
|
|
41
47
|
<div
|
|
42
48
|
class="backdrop"
|
|
43
49
|
role="presentation"
|
|
50
|
+
on:mousedown={onBackdropMousedown}
|
|
44
51
|
on:click={onBackdrop}
|
|
45
52
|
on:keydown={(e) => e.key === 'Escape' && close()}
|
|
46
53
|
transition:fade={{ duration: 200 }}
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
import { writable } from 'svelte/store';
|
|
19
|
+
|
|
20
|
+
const TOKEN_KEY = 'plum:token';
|
|
21
|
+
const USER_KEY = 'plum:user';
|
|
22
|
+
|
|
23
|
+
function createAuthStore() {
|
|
24
|
+
let initialToken = null;
|
|
25
|
+
let initialUser = null;
|
|
26
|
+
try {
|
|
27
|
+
initialToken = typeof localStorage !== 'undefined' ? localStorage.getItem(TOKEN_KEY) : null;
|
|
28
|
+
const raw = typeof localStorage !== 'undefined' ? localStorage.getItem(USER_KEY) : null;
|
|
29
|
+
initialUser = raw ? JSON.parse(raw) : null;
|
|
30
|
+
} catch {}
|
|
31
|
+
|
|
32
|
+
const { subscribe, set, update } = writable({ token: initialToken, user: initialUser });
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
subscribe,
|
|
36
|
+
login(token, user) {
|
|
37
|
+
try {
|
|
38
|
+
localStorage.setItem(TOKEN_KEY, token);
|
|
39
|
+
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
|
40
|
+
} catch {}
|
|
41
|
+
set({ token, user });
|
|
42
|
+
},
|
|
43
|
+
logout() {
|
|
44
|
+
try {
|
|
45
|
+
localStorage.removeItem(TOKEN_KEY);
|
|
46
|
+
localStorage.removeItem(USER_KEY);
|
|
47
|
+
} catch {}
|
|
48
|
+
set({ token: null, user: null });
|
|
49
|
+
},
|
|
50
|
+
getToken() {
|
|
51
|
+
try {
|
|
52
|
+
return localStorage.getItem(TOKEN_KEY);
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const auth = createAuthStore();
|
|
@@ -43,11 +43,12 @@ export const builtInEnabled = writable(true);
|
|
|
43
43
|
|
|
44
44
|
export const testsVersion = writable(0);
|
|
45
45
|
export const reportsVersion = writable(0);
|
|
46
|
+
export const runsVersion = writable(0);
|
|
46
47
|
|
|
47
48
|
// Map of taskName → true for every cron job currently executing
|
|
48
49
|
export const activeCronJobs = writable({});
|
|
49
50
|
|
|
50
|
-
export function triggerRun(id) {
|
|
51
|
+
export function triggerRun(id, testRunId, notify = {}) {
|
|
51
52
|
const s = get(socket);
|
|
52
53
|
if (!s) return;
|
|
53
54
|
|
|
@@ -66,7 +67,15 @@ export function triggerRun(id) {
|
|
|
66
67
|
});
|
|
67
68
|
panelExpanded.set(true);
|
|
68
69
|
|
|
69
|
-
s.emit('run-test', {
|
|
70
|
+
s.emit('run-test', {
|
|
71
|
+
tag: runId,
|
|
72
|
+
workers,
|
|
73
|
+
browser,
|
|
74
|
+
runners: selectedRunners,
|
|
75
|
+
testRunId: testRunId ?? null,
|
|
76
|
+
notifyDiscord: notify.notifyDiscord ?? false,
|
|
77
|
+
notifySlack: notify.notifySlack ?? false
|
|
78
|
+
});
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
export function cancelRun() {
|
|
@@ -17,13 +17,41 @@
|
|
|
17
17
|
|
|
18
18
|
<script>
|
|
19
19
|
import '../app.css';
|
|
20
|
+
import { onMount } from 'svelte';
|
|
21
|
+
import { goto } from '$app/navigation';
|
|
22
|
+
import { page } from '$app/stores';
|
|
20
23
|
import Nav from '$lib/components/layout/Nav.svelte';
|
|
21
24
|
import PageShell from '$lib/components/layout/PageShell.svelte';
|
|
22
25
|
import RunnerPanel from '$lib/components/layout/RunnerPanel.svelte';
|
|
26
|
+
import { auth } from '$lib/stores/auth';
|
|
27
|
+
import { checkNeedsSetup } from '$lib/api/auth';
|
|
28
|
+
|
|
29
|
+
const PUBLIC_ROUTES = ['/login', '/setup'];
|
|
30
|
+
|
|
31
|
+
let ready = true;
|
|
32
|
+
|
|
33
|
+
onMount(async () => {
|
|
34
|
+
const pathname = $page.url.pathname;
|
|
35
|
+
if (PUBLIC_ROUTES.includes(pathname)) return;
|
|
36
|
+
|
|
37
|
+
const token = $auth.token;
|
|
38
|
+
if (!token) {
|
|
39
|
+
try {
|
|
40
|
+
const needsSetup = await checkNeedsSetup();
|
|
41
|
+
goto(needsSetup ? '/setup' : '/login');
|
|
42
|
+
} catch {
|
|
43
|
+
goto('/login');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
23
47
|
</script>
|
|
24
48
|
|
|
25
|
-
|
|
26
|
-
<PageShell>
|
|
49
|
+
{#if $page.url.pathname === '/login' || $page.url.pathname === '/setup'}
|
|
27
50
|
<slot />
|
|
28
|
-
|
|
29
|
-
<
|
|
51
|
+
{:else}
|
|
52
|
+
<Nav />
|
|
53
|
+
<PageShell>
|
|
54
|
+
<slot />
|
|
55
|
+
</PageShell>
|
|
56
|
+
<RunnerPanel />
|
|
57
|
+
{/if}
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
$: visibleTests = filtered.reduce((n, s) => n + s.tests.length, 0);
|
|
107
107
|
</script>
|
|
108
108
|
|
|
109
|
-
<svelte:head><title>
|
|
109
|
+
<svelte:head><title>Automated Tests — Plum</title></svelte:head>
|
|
110
110
|
|
|
111
111
|
<div class="page-header">
|
|
112
112
|
<div class="header-top">
|