plum-e2e 1.3.6 → 2.1.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 +61 -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/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/schema.prisma +118 -10
- package/backend/routes/auth.routes.js +96 -0
- package/backend/routes/settings.routes.js +44 -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/services/reportService.js +96 -4
- package/backend/services/runnerService.js +16 -1
- package/backend/services/settingsService.js +18 -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 +19 -6
- 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/users.js +52 -0
- package/frontend/src/lib/components/layout/Nav.svelte +116 -4
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +243 -29
- 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 +9 -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 +3 -1
- package/frontend/src/routes/settings/+page.svelte +586 -5
- 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,16 +28,22 @@
|
|
|
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';
|
|
36
37
|
import { API_BASE, BROWSERS } from '$lib/constants';
|
|
37
38
|
import ConfirmModal from '$lib/components/ui/ConfirmModal.svelte';
|
|
38
39
|
|
|
39
40
|
let availableRunners = [];
|
|
41
|
+
let testRuns = [];
|
|
42
|
+
let selectedRun = null; // { id, title, tags: string[] | null }
|
|
43
|
+
let selectedRunLoading = false;
|
|
40
44
|
let browserOpen = false;
|
|
41
45
|
let runnersOpen = false;
|
|
46
|
+
let runPickOpen = false;
|
|
42
47
|
let runAllModalOpen = false;
|
|
43
48
|
|
|
44
49
|
function clickOutside(node) {
|
|
@@ -105,7 +110,7 @@
|
|
|
105
110
|
});
|
|
106
111
|
});
|
|
107
112
|
|
|
108
|
-
const s = io(API_BASE);
|
|
113
|
+
const s = io(API_BASE, { transports: ['websocket'] });
|
|
109
114
|
_socket = s;
|
|
110
115
|
socket.set(s);
|
|
111
116
|
|
|
@@ -189,6 +194,11 @@
|
|
|
189
194
|
$: anyCronRunning = cronJobs.length > 0;
|
|
190
195
|
$: anyRunning = state.running || anyCronRunning;
|
|
191
196
|
|
|
197
|
+
$: if ($runsVersion >= 0)
|
|
198
|
+
fetchRuns({ limit: 200 })
|
|
199
|
+
.then((r) => (testRuns = r.runs))
|
|
200
|
+
.catch(() => {});
|
|
201
|
+
|
|
192
202
|
$: statusColor =
|
|
193
203
|
state.status === 'pass'
|
|
194
204
|
? 'var(--pass)'
|
|
@@ -219,8 +229,33 @@
|
|
|
219
229
|
? (availableRunners.find((r) => r.id === cfg.selectedRunners[0])?.name ?? '1 node')
|
|
220
230
|
: `${cfg.selectedRunners.length} nodes`;
|
|
221
231
|
|
|
232
|
+
async function selectRun(run) {
|
|
233
|
+
runPickOpen = false;
|
|
234
|
+
selectedRun = { id: run.id, title: run.title, tags: null };
|
|
235
|
+
selectedRunLoading = true;
|
|
236
|
+
try {
|
|
237
|
+
const full = await fetchRun(run.id);
|
|
238
|
+
const tags = full.entries
|
|
239
|
+
.filter((e) => e.case?.isAutomated)
|
|
240
|
+
.map((e) => `@${e.case.displayId}`);
|
|
241
|
+
selectedRun = { id: run.id, title: run.title, tags };
|
|
242
|
+
} catch {
|
|
243
|
+
selectedRun = null;
|
|
244
|
+
} finally {
|
|
245
|
+
selectedRunLoading = false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function clearSelectedRun() {
|
|
250
|
+
selectedRun = null;
|
|
251
|
+
}
|
|
252
|
+
|
|
222
253
|
function handleRunClick() {
|
|
223
|
-
if (
|
|
254
|
+
if (selectedRun) {
|
|
255
|
+
if (selectedRunLoading || !selectedRun.tags) return;
|
|
256
|
+
if (selectedRun.tags.length === 0) return;
|
|
257
|
+
triggerRun(selectedRun.tags.join(' or '), selectedRun.id);
|
|
258
|
+
} else if ($runnerConfig.testID.trim() === '') {
|
|
224
259
|
runAllModalOpen = true;
|
|
225
260
|
} else {
|
|
226
261
|
triggerRun();
|
|
@@ -228,7 +263,7 @@
|
|
|
228
263
|
}
|
|
229
264
|
|
|
230
265
|
function handleKeydown(e) {
|
|
231
|
-
if (e.key === 'Enter' && !state.running) handleRunClick();
|
|
266
|
+
if (e.key === 'Enter' && !state.running && !selectedRun) handleRunClick();
|
|
232
267
|
}
|
|
233
268
|
|
|
234
269
|
function adjustWorkers(delta) {
|
|
@@ -312,30 +347,115 @@
|
|
|
312
347
|
|
|
313
348
|
<!-- Middle: tag filter + browser + workers + runner dropdowns -->
|
|
314
349
|
<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
|
-
|
|
350
|
+
<!-- Test Run picker OR tag input -->
|
|
351
|
+
{#if selectedRun}
|
|
352
|
+
<div class="run-chip" class:loading={selectedRunLoading}>
|
|
353
|
+
<svg
|
|
354
|
+
width="9"
|
|
355
|
+
height="10"
|
|
356
|
+
viewBox="0 0 10 12"
|
|
357
|
+
fill="currentColor"
|
|
358
|
+
stroke="none"
|
|
359
|
+
style="opacity:0.6;flex-shrink:0"><polygon points="0,0 10,6 0,12" /></svg
|
|
360
|
+
>
|
|
361
|
+
<span class="run-chip-title">{selectedRun.title}</span>
|
|
362
|
+
{#if selectedRunLoading}
|
|
363
|
+
<span class="run-chip-spinner"></span>
|
|
364
|
+
{:else if selectedRun.tags !== null}
|
|
365
|
+
<span class="run-chip-count" class:none={selectedRun.tags.length === 0}>
|
|
366
|
+
{selectedRun.tags.length} automated
|
|
367
|
+
</span>
|
|
368
|
+
{/if}
|
|
369
|
+
<button class="run-chip-clear" on:click={clearSelectedRun} aria-label="Clear test run">
|
|
370
|
+
<svg width="9" height="9" viewBox="0 0 14 14" fill="none"
|
|
371
|
+
><path
|
|
372
|
+
d="M1 1l12 12M13 1L1 13"
|
|
373
|
+
stroke="currentColor"
|
|
374
|
+
stroke-width="1.5"
|
|
375
|
+
stroke-linecap="round"
|
|
376
|
+
/></svg
|
|
377
|
+
>
|
|
378
|
+
</button>
|
|
379
|
+
</div>
|
|
380
|
+
{:else}
|
|
381
|
+
<div class="input-wrap">
|
|
382
|
+
<svg
|
|
383
|
+
class="input-icon"
|
|
384
|
+
width="12"
|
|
385
|
+
height="12"
|
|
386
|
+
viewBox="0 0 24 24"
|
|
387
|
+
fill="none"
|
|
388
|
+
stroke="currentColor"
|
|
389
|
+
stroke-width="2"
|
|
390
|
+
stroke-linecap="round"
|
|
391
|
+
stroke-linejoin="round"
|
|
392
|
+
>
|
|
393
|
+
<circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
394
|
+
</svg>
|
|
395
|
+
<input
|
|
396
|
+
type="text"
|
|
397
|
+
class="tag-input"
|
|
398
|
+
value={$runnerConfig.testID}
|
|
399
|
+
placeholder="@tag or leave blank for all tests"
|
|
400
|
+
on:input={(e) => runnerConfig.update((c) => ({ ...c, testID: e.currentTarget.value }))}
|
|
401
|
+
on:keydown={handleKeydown}
|
|
402
|
+
disabled={state.running}
|
|
403
|
+
/>
|
|
404
|
+
</div>
|
|
405
|
+
{/if}
|
|
406
|
+
|
|
407
|
+
<!-- Test run dropdown -->
|
|
408
|
+
<div class="ctrl-group">
|
|
409
|
+
<span class="ctrl-label">Test Run</span>
|
|
410
|
+
<div class="dropdown-wrap" use:clickOutside on:clickoutside={() => (runPickOpen = false)}>
|
|
411
|
+
<button
|
|
412
|
+
class="dropdown-trigger"
|
|
413
|
+
class:open={runPickOpen}
|
|
414
|
+
class:has-remote={!!selectedRun}
|
|
415
|
+
on:click={() => {
|
|
416
|
+
if (!state.running) runPickOpen = !runPickOpen;
|
|
417
|
+
}}
|
|
418
|
+
disabled={state.running}
|
|
419
|
+
>
|
|
420
|
+
<span>{selectedRun ? selectedRun.title : 'None'}</span>
|
|
421
|
+
<svg
|
|
422
|
+
width="10"
|
|
423
|
+
height="10"
|
|
424
|
+
viewBox="0 0 24 24"
|
|
425
|
+
fill="none"
|
|
426
|
+
stroke="currentColor"
|
|
427
|
+
stroke-width="2.5"
|
|
428
|
+
stroke-linecap="round"
|
|
429
|
+
class="trigger-chevron"
|
|
430
|
+
class:open={runPickOpen}
|
|
431
|
+
>
|
|
432
|
+
<polyline points="6 9 12 15 18 9" />
|
|
433
|
+
</svg>
|
|
434
|
+
</button>
|
|
435
|
+
{#if runPickOpen}
|
|
436
|
+
<div class="dropdown-menu run-pick-menu" transition:fly={{ y: 6, duration: 130 }}>
|
|
437
|
+
{#if selectedRun}
|
|
438
|
+
<button class="dropdown-item" on:click={clearSelectedRun}>
|
|
439
|
+
<span style="color:var(--text-muted)">✕</span> Clear
|
|
440
|
+
</button>
|
|
441
|
+
<div class="dropdown-divider"></div>
|
|
442
|
+
{/if}
|
|
443
|
+
{#if testRuns.filter((r) => r.status !== 'complete').length === 0}
|
|
444
|
+
<div class="dropdown-empty">No active test runs</div>
|
|
445
|
+
{:else}
|
|
446
|
+
{#each testRuns.filter((r) => r.status !== 'complete') as run}
|
|
447
|
+
<button
|
|
448
|
+
class="dropdown-item"
|
|
449
|
+
class:active={selectedRun?.id === run.id}
|
|
450
|
+
on:click={() => selectRun(run)}
|
|
451
|
+
>
|
|
452
|
+
{run.title}
|
|
453
|
+
</button>
|
|
454
|
+
{/each}
|
|
455
|
+
{/if}
|
|
456
|
+
</div>
|
|
457
|
+
{/if}
|
|
458
|
+
</div>
|
|
339
459
|
</div>
|
|
340
460
|
|
|
341
461
|
<div class="ctrl-divider"></div>
|
|
@@ -473,7 +593,12 @@
|
|
|
473
593
|
class="run-btn"
|
|
474
594
|
class:is-running={state.running}
|
|
475
595
|
on:click={handleRunClick}
|
|
476
|
-
disabled={state.running
|
|
596
|
+
disabled={state.running ||
|
|
597
|
+
selectedRunLoading ||
|
|
598
|
+
(selectedRun && selectedRun.tags?.length === 0)}
|
|
599
|
+
title={selectedRun && selectedRun.tags?.length === 0
|
|
600
|
+
? 'No automated cases in this run'
|
|
601
|
+
: undefined}
|
|
477
602
|
>
|
|
478
603
|
{#if state.running}
|
|
479
604
|
<span class="run-spinner"></span>
|
|
@@ -1187,4 +1312,93 @@
|
|
|
1187
1312
|
.empty-icon {
|
|
1188
1313
|
opacity: 0.4;
|
|
1189
1314
|
}
|
|
1315
|
+
|
|
1316
|
+
/* ── Run chip (selected test run display) ── */
|
|
1317
|
+
.run-chip {
|
|
1318
|
+
display: inline-flex;
|
|
1319
|
+
align-items: center;
|
|
1320
|
+
gap: 0.4rem;
|
|
1321
|
+
height: 28px;
|
|
1322
|
+
padding: 0 0.5rem 0 0.625rem;
|
|
1323
|
+
background: var(--accent-soft);
|
|
1324
|
+
border: 1px solid var(--accent);
|
|
1325
|
+
border-radius: var(--radius-sm);
|
|
1326
|
+
color: var(--accent);
|
|
1327
|
+
font-size: 0.78rem;
|
|
1328
|
+
white-space: nowrap;
|
|
1329
|
+
max-width: 200px;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
.run-chip-title {
|
|
1333
|
+
overflow: hidden;
|
|
1334
|
+
text-overflow: ellipsis;
|
|
1335
|
+
white-space: nowrap;
|
|
1336
|
+
flex: 1;
|
|
1337
|
+
min-width: 0;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
.run-chip-count {
|
|
1341
|
+
font-size: 0.65rem;
|
|
1342
|
+
font-weight: 600;
|
|
1343
|
+
letter-spacing: 0.04em;
|
|
1344
|
+
opacity: 0.75;
|
|
1345
|
+
white-space: nowrap;
|
|
1346
|
+
flex-shrink: 0;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
.run-chip-count.none {
|
|
1350
|
+
color: var(--fail);
|
|
1351
|
+
opacity: 1;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
.run-chip-spinner {
|
|
1355
|
+
width: 9px;
|
|
1356
|
+
height: 9px;
|
|
1357
|
+
border: 1.5px solid color-mix(in srgb, var(--accent) 35%, transparent);
|
|
1358
|
+
border-top-color: var(--accent);
|
|
1359
|
+
border-radius: 50%;
|
|
1360
|
+
animation: spin 0.65s linear infinite;
|
|
1361
|
+
flex-shrink: 0;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
.run-chip-clear {
|
|
1365
|
+
display: flex;
|
|
1366
|
+
align-items: center;
|
|
1367
|
+
justify-content: center;
|
|
1368
|
+
width: 16px;
|
|
1369
|
+
height: 16px;
|
|
1370
|
+
border: none;
|
|
1371
|
+
background: transparent;
|
|
1372
|
+
color: var(--accent);
|
|
1373
|
+
cursor: pointer;
|
|
1374
|
+
border-radius: 2px;
|
|
1375
|
+
opacity: 0.7;
|
|
1376
|
+
padding: 0;
|
|
1377
|
+
flex-shrink: 0;
|
|
1378
|
+
transition: opacity var(--duration-fast);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
.run-chip-clear:hover {
|
|
1382
|
+
opacity: 1;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
.run-pick-menu {
|
|
1386
|
+
min-width: 180px;
|
|
1387
|
+
max-height: 240px;
|
|
1388
|
+
overflow-y: auto;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
.dropdown-divider {
|
|
1392
|
+
height: 1px;
|
|
1393
|
+
background: var(--border);
|
|
1394
|
+
margin: 0.25rem 0;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
.dropdown-empty {
|
|
1398
|
+
font-size: 0.8125rem;
|
|
1399
|
+
color: var(--text-muted);
|
|
1400
|
+
padding: 0.35rem 0.5rem;
|
|
1401
|
+
text-align: center;
|
|
1402
|
+
opacity: 0.7;
|
|
1403
|
+
}
|
|
1190
1404
|
</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) {
|
|
51
52
|
const s = get(socket);
|
|
52
53
|
if (!s) return;
|
|
53
54
|
|
|
@@ -66,7 +67,13 @@ 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
|
+
});
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
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">
|