plum-e2e 2.1.0 → 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.
@@ -48,7 +48,7 @@ export const runsVersion = writable(0);
48
48
  // Map of taskName → true for every cron job currently executing
49
49
  export const activeCronJobs = writable({});
50
50
 
51
- export function triggerRun(id, testRunId) {
51
+ export function triggerRun(id, testRunId, notify = {}) {
52
52
  const s = get(socket);
53
53
  if (!s) return;
54
54
 
@@ -72,7 +72,9 @@ export function triggerRun(id, testRunId) {
72
72
  workers,
73
73
  browser,
74
74
  runners: selectedRunners,
75
- testRunId: testRunId ?? null
75
+ testRunId: testRunId ?? null,
76
+ notifyDiscord: notify.notifyDiscord ?? false,
77
+ notifySlack: notify.notifySlack ?? false
76
78
  });
77
79
  }
78
80
 
@@ -25,6 +25,7 @@
25
25
  toggleCronJob
26
26
  } from '$lib/api/schedules';
27
27
  import { fetchRunners } from '$lib/api/runners';
28
+ import { fetchIntegrations } from '$lib/api/settings';
28
29
  import { activeCronJobs } from '$lib/stores/runner';
29
30
  import { BROWSERS, TOAST_TIMEOUT_MS } from '$lib/constants';
30
31
  import { stagger } from '$lib/utils/format';
@@ -50,6 +51,7 @@
50
51
 
51
52
  let cronJobs = [];
52
53
  let availableRunners = [];
54
+ let integrations = { discordWebhookUrl: '', slackWebhookUrl: '', notifyPublicUrl: '' };
53
55
  let toast = null;
54
56
 
55
57
  let modalOpen = false;
@@ -64,7 +66,9 @@
64
66
  tags: '',
65
67
  workers: 1,
66
68
  browser: 'chromium',
67
- runnerIds: ['built-in']
69
+ runnerIds: ['built-in'],
70
+ notifyDiscord: false,
71
+ notifySlack: false
68
72
  };
69
73
  let selectedSchedule = '';
70
74
  let useCustomCron = false;
@@ -137,7 +141,9 @@
137
141
  tags: '',
138
142
  workers: 1,
139
143
  browser: 'chromium',
140
- runnerIds: ['built-in']
144
+ runnerIds: ['built-in'],
145
+ notifyDiscord: false,
146
+ notifySlack: false
141
147
  };
142
148
  selectedSchedule = '';
143
149
  useCustomCron = false;
@@ -157,7 +163,9 @@
157
163
  tags: job.tags,
158
164
  workers: job.workers ?? 1,
159
165
  browser: job.browser ?? 'chromium',
160
- runnerIds: prunedIds.length > 0 ? prunedIds : ['built-in']
166
+ runnerIds: prunedIds.length > 0 ? prunedIds : ['built-in'],
167
+ notifyDiscord: job.notifyDiscord ?? false,
168
+ notifySlack: job.notifySlack ?? false
161
169
  };
162
170
  const isPreset = scheduleOptions.some((o) => o.value === job.cronExpression);
163
171
  useCustomCron = !isPreset;
@@ -231,10 +239,15 @@
231
239
  }
232
240
 
233
241
  onMount(async () => {
234
- cronJobs = await fetchCronJobs();
235
- try {
236
- availableRunners = await fetchRunners();
237
- } catch {}
242
+ [cronJobs, availableRunners, integrations] = await Promise.all([
243
+ fetchCronJobs(),
244
+ fetchRunners().catch(() => []),
245
+ fetchIntegrations().catch(() => ({
246
+ discordWebhookUrl: '',
247
+ slackWebhookUrl: '',
248
+ notifyPublicUrl: ''
249
+ }))
250
+ ]);
238
251
  });
239
252
  </script>
240
253
 
@@ -373,6 +386,26 @@
373
386
  </div>
374
387
  </div>
375
388
 
389
+ {#if integrations.discordWebhookUrl || integrations.slackWebhookUrl}
390
+ <div class="field">
391
+ <div class="field-label"><span>Notifications</span></div>
392
+ <div class="notify-checks">
393
+ {#if integrations.discordWebhookUrl}
394
+ <label class="notify-check-option">
395
+ <input type="checkbox" bind:checked={form.notifyDiscord} />
396
+ <span>Discord</span>
397
+ </label>
398
+ {/if}
399
+ {#if integrations.slackWebhookUrl}
400
+ <label class="notify-check-option">
401
+ <input type="checkbox" bind:checked={form.notifySlack} />
402
+ <span>Slack</span>
403
+ </label>
404
+ {/if}
405
+ </div>
406
+ </div>
407
+ {/if}
408
+
376
409
  {#if formError}
377
410
  <p class="form-error">{formError}</p>
378
411
  {/if}
@@ -859,4 +892,29 @@
859
892
  text-overflow: ellipsis;
860
893
  max-width: 180px;
861
894
  }
895
+
896
+ /* Notification checkboxes in modal */
897
+ .notify-checks {
898
+ display: flex;
899
+ flex-direction: row;
900
+ gap: 1rem;
901
+ padding: 0.375rem 0;
902
+ }
903
+
904
+ .notify-check-option {
905
+ display: flex;
906
+ align-items: center;
907
+ gap: 0.5rem;
908
+ font-size: 0.8125rem;
909
+ color: var(--text);
910
+ cursor: pointer;
911
+ }
912
+
913
+ .notify-check-option input[type='checkbox'] {
914
+ accent-color: var(--accent);
915
+ width: 13px;
916
+ height: 13px;
917
+ flex-shrink: 0;
918
+ cursor: pointer;
919
+ }
862
920
  </style>
@@ -19,7 +19,14 @@
19
19
  import { onMount } from 'svelte';
20
20
  import { fly } from 'svelte/transition';
21
21
  import { goto } from '$app/navigation';
22
- import { fetchProject, saveProject, exportBackup, importBackup } from '$lib/api/settings';
22
+ import {
23
+ fetchProject,
24
+ saveProject,
25
+ exportBackup,
26
+ importBackup,
27
+ fetchIntegrations,
28
+ saveIntegrations
29
+ } from '$lib/api/settings';
23
30
  import {
24
31
  fetchRunners,
25
32
  createRunner,
@@ -43,7 +50,7 @@
43
50
  import Toast from '$lib/components/ui/Toast.svelte';
44
51
  import ConfirmModal from '$lib/components/ui/ConfirmModal.svelte';
45
52
 
46
- /** @type {'project' | 'runners' | 'repository' | 'account' | 'users' | 'backup'} */
53
+ /** @type {'project' | 'runners' | 'repository' | 'integrations' | 'account' | 'users' | 'backup'} */
47
54
  let section =
48
55
  (typeof sessionStorage !== 'undefined' && sessionStorage.getItem('plum:settings:section')) ||
49
56
  'project';
@@ -84,6 +91,9 @@
84
91
  let exporting = false;
85
92
  let fileInput;
86
93
 
94
+ let integrations = { discordWebhookUrl: '', slackWebhookUrl: '', notifyPublicUrl: '' };
95
+ let integrationsSaving = false;
96
+
87
97
  let runners = [];
88
98
  let runnerForm = { name: '', url: '', token: '', browser: 'chromium' };
89
99
  let runnerFormError = '';
@@ -121,6 +131,9 @@
121
131
  testSuitePrefix: prefixes.testSuitePrefix
122
132
  };
123
133
  } catch {}
134
+ try {
135
+ integrations = await fetchIntegrations();
136
+ } catch {}
124
137
  if ($auth.user) {
125
138
  profileForm = { name: $auth.user.name, email: $auth.user.email };
126
139
  }
@@ -394,10 +407,23 @@
394
407
  goto('/login');
395
408
  }
396
409
 
410
+ async function handleSaveIntegrations() {
411
+ integrationsSaving = true;
412
+ try {
413
+ integrations = await saveIntegrations(integrations);
414
+ showToast('success', 'Integration settings saved.');
415
+ } catch {
416
+ showToast('error', 'Failed to save integration settings.');
417
+ } finally {
418
+ integrationsSaving = false;
419
+ }
420
+ }
421
+
397
422
  $: navItems = [
398
423
  { id: 'project', label: 'Project' },
399
424
  { id: 'runners', label: 'Runners' },
400
425
  { id: 'repository', label: 'Repository' },
426
+ { id: 'integrations', label: 'Integrations' },
401
427
  { id: 'account', label: 'Account' },
402
428
  ...($auth.user?.role === 'admin' ? [{ id: 'users', label: 'Users' }] : []),
403
429
  { id: 'backup', label: 'Backup' }
@@ -811,6 +837,70 @@
811
837
  </div>
812
838
  </div>
813
839
 
840
+ <!-- INTEGRATIONS -->
841
+ {:else if section === 'integrations'}
842
+ <div class="content-section" transition:fly={{ y: 6, duration: 180 }}>
843
+ <div class="content-header">
844
+ <h2>Integrations</h2>
845
+ <p class="content-desc">
846
+ Connect Discord and Slack to receive run notifications with pass/fail results and report
847
+ links.
848
+ </p>
849
+ </div>
850
+
851
+ <div class="card settings-card">
852
+ <p class="card-title">Webhooks</p>
853
+
854
+ <div class="field">
855
+ <label class="field-label" for="discord-url">
856
+ <span>Discord Webhook URL</span>
857
+ <span class="field-hint">Leave blank to disable Discord notifications</span>
858
+ </label>
859
+ <input
860
+ id="discord-url"
861
+ type="url"
862
+ class="field-input"
863
+ bind:value={integrations.discordWebhookUrl}
864
+ placeholder="https://discord.com/api/webhooks/…"
865
+ />
866
+ </div>
867
+
868
+ <div class="field">
869
+ <label class="field-label" for="slack-url">
870
+ <span>Slack Webhook URL</span>
871
+ <span class="field-hint">Leave blank to disable Slack notifications</span>
872
+ </label>
873
+ <input
874
+ id="slack-url"
875
+ type="url"
876
+ class="field-input"
877
+ bind:value={integrations.slackWebhookUrl}
878
+ placeholder="https://hooks.slack.com/services/…"
879
+ />
880
+ </div>
881
+
882
+ <div class="field">
883
+ <label class="field-label" for="public-url">
884
+ <span>Public URL</span>
885
+ <span class="field-hint"
886
+ >Base URL of this Plum instance, used to link reports in notifications</span
887
+ >
888
+ </label>
889
+ <input
890
+ id="public-url"
891
+ type="url"
892
+ class="field-input"
893
+ bind:value={integrations.notifyPublicUrl}
894
+ placeholder="https://plum.yourcompany.com"
895
+ />
896
+ </div>
897
+
898
+ <Button on:click={handleSaveIntegrations} disabled={integrationsSaving}>
899
+ {integrationsSaving ? 'Saving…' : 'Save Integrations'}
900
+ </Button>
901
+ </div>
902
+ </div>
903
+
814
904
  <!-- ACCOUNT -->
815
905
  {:else if section === 'account'}
816
906
  <div class="content-section" transition:fly={{ y: 6, duration: 180 }}>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plum-e2e",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/silverlunah/plum.git"