plum-e2e 1.0.10 → 1.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/.claude/settings.local.json +16 -1
- package/.vscode/settings.json +10 -0
- package/README.md +151 -37
- package/backend/_scaffold/features/LoginPage.feature +45 -3
- package/backend/_scaffold/pages/HomepagePage.ts +7 -0
- package/backend/_scaffold/pages/LoginPage.ts +37 -13
- package/backend/_scaffold/step_definitions/HomepageSteps.ts +6 -0
- package/backend/_scaffold/step_definitions/LoginSteps.ts +30 -4
- package/backend/_scaffold/utils/browser.ts +33 -0
- package/backend/_scaffold/utils/hooks.ts +8 -29
- package/backend/_scaffold/utils/utils.ts +3 -9
- package/backend/config/scripts/create-settings.js +7 -14
- package/backend/config/scripts/create-step.mjs +268 -0
- package/backend/config/scripts/generate-report.js +31 -75
- package/backend/config/scripts/run-tests.js +19 -4
- package/backend/package-lock.json +56 -641
- package/backend/package.json +4 -1
- package/backend/routes/reports.routes.js +6 -10
- package/backend/services/envService.js +4 -10
- package/backend/services/reportService.js +70 -20
- package/backend/services/testService.js +99 -24
- package/backend/tsconfig.json +2 -2
- package/backend/websockets/socketHandler.js +12 -6
- package/bin/plum.js +49 -3
- package/frontend/package-lock.json +436 -135
- package/frontend/package.json +1 -1
- package/frontend/src/app.css +241 -6
- package/frontend/src/app.html +14 -1
- package/frontend/src/lib/api/reports.js +68 -0
- package/frontend/src/lib/api/schedules.js +64 -0
- package/frontend/src/lib/api/tests.js +41 -0
- package/frontend/src/lib/components/layout/Nav.svelte +304 -0
- package/frontend/src/lib/components/layout/PageShell.svelte +28 -0
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +378 -0
- package/frontend/src/lib/components/ui/Badge.svelte +63 -0
- package/frontend/src/lib/components/ui/Button.svelte +117 -0
- package/frontend/src/lib/components/ui/Modal.svelte +140 -0
- package/frontend/src/lib/components/ui/Pagination.svelte +100 -0
- package/frontend/src/lib/components/ui/Terminal.svelte +100 -0
- package/frontend/src/lib/stores/runner.js +55 -0
- package/frontend/src/lib/stores/theme.js +47 -0
- package/frontend/src/routes/+layout.svelte +7 -12
- package/frontend/src/routes/+page.svelte +690 -142
- package/frontend/src/routes/reports/+page.svelte +395 -125
- package/frontend/src/routes/reports/[slug]/+page.svelte +749 -0
- package/frontend/src/routes/scheduled-tests/+page.svelte +267 -303
- package/frontend/svelte.config.js +1 -4
- package/frontend/tailwind.config.js +2 -23
- package/package.json +2 -2
- package/backend/_scaffold/utils/world.ts +0 -25
- package/frontend/src/routes/components/Navigation.svelte +0 -53
|
@@ -17,347 +17,311 @@
|
|
|
17
17
|
|
|
18
18
|
<script>
|
|
19
19
|
import { onMount } from 'svelte';
|
|
20
|
+
import { fly } from 'svelte/transition';
|
|
21
|
+
import { fetchSchedules, fetchCronJobs, saveCronJob, deleteCronJob } from '$lib/api/schedules';
|
|
22
|
+
import Button from '$lib/components/ui/Button.svelte';
|
|
23
|
+
import Badge from '$lib/components/ui/Badge.svelte';
|
|
24
|
+
import Modal from '$lib/components/ui/Modal.svelte';
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
let schedules = [];
|
|
23
|
-
let cronExpression = '';
|
|
24
|
-
let taskName = '';
|
|
25
|
-
let tags = '';
|
|
26
|
-
let successMessage = '';
|
|
27
|
-
let errorMessage = '';
|
|
28
|
-
let isModalOpen = false;
|
|
29
|
-
let isEditing = false; // To track if we are editing a cron job
|
|
30
|
-
let editTaskName = ''; // To store the task name of the cron job being edited
|
|
31
|
-
let isDeleting = false; // To track if the user is confirming deletion
|
|
32
|
-
let taskToDelete = ''; // The task name of the cron job being deleted
|
|
33
|
-
|
|
34
|
-
const cronRegex =
|
|
26
|
+
const CRON_REGEX =
|
|
35
27
|
/^(\*|([0-5]?[0-9])) (\*|([01]?[0-9]|2[0-3])) (\*|([01]?[0-9]|3[01])) (\*|([1-9]|1[0-2])) (\*|[0-6])$/;
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const res = await fetch('http://localhost:3001/schedules');
|
|
41
|
-
const data = await res.json();
|
|
29
|
+
let cronJobs = [];
|
|
30
|
+
let scheduleOptions = [];
|
|
31
|
+
let toast = null; // { type: 'success' | 'error', message }
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
33
|
+
let modalOpen = false;
|
|
34
|
+
let deleteModalOpen = false;
|
|
35
|
+
let isEditing = false;
|
|
36
|
+
let editTaskName = '';
|
|
37
|
+
let taskToDelete = '';
|
|
38
|
+
|
|
39
|
+
let form = { taskName: '', cronExpression: '', tags: '' };
|
|
40
|
+
let formError = '';
|
|
41
|
+
|
|
42
|
+
function showToast(type, message) {
|
|
43
|
+
toast = { type, message };
|
|
44
|
+
setTimeout(() => (toast = null), 4000);
|
|
50
45
|
}
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
47
|
+
function openAddModal() {
|
|
48
|
+
isEditing = false;
|
|
49
|
+
editTaskName = '';
|
|
50
|
+
form = { taskName: '', cronExpression: '', tags: '' };
|
|
51
|
+
formError = '';
|
|
52
|
+
modalOpen = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function openEditModal(job) {
|
|
56
|
+
isEditing = true;
|
|
57
|
+
editTaskName = job.taskName;
|
|
58
|
+
form = { taskName: job.taskName, cronExpression: job.cronExpression, tags: job.tags };
|
|
59
|
+
formError = '';
|
|
60
|
+
modalOpen = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function openDeleteModal(taskName) {
|
|
64
|
+
taskToDelete = taskName;
|
|
65
|
+
deleteModalOpen = true;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
errorMessage = 'Please fill in all fields';
|
|
68
|
+
async function handleSave() {
|
|
69
|
+
if (!form.taskName || !form.cronExpression || !form.tags) {
|
|
70
|
+
formError = 'All fields are required.';
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!cronRegex.test(cronExpression)) {
|
|
75
|
-
errorMessage = 'Invalid cron expression. Please use a valid cron format.';
|
|
73
|
+
if (!CRON_REGEX.test(form.cronExpression)) {
|
|
74
|
+
formError = 'Invalid cron expression.';
|
|
76
75
|
return;
|
|
77
76
|
}
|
|
78
|
-
|
|
79
|
-
// Lowercase " OR " while keeping other tags intact
|
|
80
|
-
const formattedTags = tags.replace(/\sOR\s/gi, (match) => match.toLowerCase());
|
|
81
|
-
|
|
77
|
+
formError = '';
|
|
82
78
|
try {
|
|
83
|
-
|
|
84
|
-
if (isEditing) {
|
|
85
|
-
// Edit cron job
|
|
86
|
-
res = await fetch(`http://localhost:3001/cron-jobs/${editTaskName}`, {
|
|
87
|
-
method: 'PUT',
|
|
88
|
-
headers: {
|
|
89
|
-
'Content-Type': 'application/json'
|
|
90
|
-
},
|
|
91
|
-
body: JSON.stringify({ cronExpression, taskName, tags: formattedTags })
|
|
92
|
-
});
|
|
93
|
-
} else {
|
|
94
|
-
// Add new cron job
|
|
95
|
-
res = await fetch('http://localhost:3001/cron-jobs', {
|
|
96
|
-
method: 'POST',
|
|
97
|
-
headers: {
|
|
98
|
-
'Content-Type': 'application/json'
|
|
99
|
-
},
|
|
100
|
-
body: JSON.stringify({ cronExpression, taskName, tags: formattedTags })
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const data = await res.json();
|
|
79
|
+
const data = await saveCronJob({ ...form, isEditing, editTaskName });
|
|
105
80
|
if (data.message) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
successMessage = ''; // Clear success message after 5 seconds
|
|
110
|
-
}, 5000);
|
|
111
|
-
clearModal();
|
|
81
|
+
showToast('success', `${isEditing ? 'Updated' : 'Added'}: ${form.taskName}`);
|
|
82
|
+
modalOpen = false;
|
|
83
|
+
cronJobs = await fetchCronJobs();
|
|
112
84
|
} else {
|
|
113
|
-
|
|
85
|
+
formError = 'Failed to save. Please try again.';
|
|
114
86
|
}
|
|
115
|
-
} catch
|
|
116
|
-
|
|
117
|
-
console.error('Error saving cron job:', error);
|
|
87
|
+
} catch {
|
|
88
|
+
formError = 'Network error.';
|
|
118
89
|
}
|
|
119
90
|
}
|
|
120
91
|
|
|
121
|
-
|
|
122
|
-
async function deleteCronJob(taskName) {
|
|
92
|
+
async function handleDelete() {
|
|
123
93
|
try {
|
|
124
|
-
const
|
|
125
|
-
method: 'DELETE'
|
|
126
|
-
});
|
|
127
|
-
const data = await res.json();
|
|
94
|
+
const data = await deleteCronJob(taskToDelete);
|
|
128
95
|
if (data.message) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
fetchScheduledTests();
|
|
132
|
-
setTimeout(() => {
|
|
133
|
-
successMessage = '';
|
|
134
|
-
}, 5000);
|
|
96
|
+
showToast('success', `Deleted: ${taskToDelete}`);
|
|
97
|
+
cronJobs = await fetchCronJobs();
|
|
135
98
|
} else {
|
|
136
|
-
|
|
137
|
-
setTimeout(() => {
|
|
138
|
-
errorMessage = '';
|
|
139
|
-
}, 5000);
|
|
99
|
+
showToast('error', 'Could not delete cron job.');
|
|
140
100
|
}
|
|
141
|
-
} catch
|
|
142
|
-
|
|
143
|
-
console.error('Error deleting cron job:', error);
|
|
101
|
+
} catch {
|
|
102
|
+
showToast('error', 'Network error.');
|
|
144
103
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// Open modal to edit cron job
|
|
148
|
-
function openEditModal(cronJob) {
|
|
149
|
-
isEditing = true;
|
|
150
|
-
editTaskName = cronJob.taskName;
|
|
151
|
-
taskName = cronJob.taskName;
|
|
152
|
-
cronExpression = cronJob.cronExpression;
|
|
153
|
-
tags = cronJob.tags;
|
|
154
|
-
isModalOpen = true;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Reset modal values
|
|
158
|
-
function clearModal() {
|
|
159
|
-
isEditing = false;
|
|
160
|
-
editTaskName = '';
|
|
161
|
-
taskName = '';
|
|
162
|
-
cronExpression = '';
|
|
163
|
-
tags = '';
|
|
164
|
-
isModalOpen = false;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Open confirmation modal for deleting cron job
|
|
168
|
-
function openDeleteConfirmation(taskName) {
|
|
169
|
-
isDeleting = true;
|
|
170
|
-
taskToDelete = taskName;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Confirm deletion
|
|
174
|
-
function confirmDelete() {
|
|
175
|
-
deleteCronJob(taskToDelete);
|
|
176
|
-
isDeleting = false;
|
|
104
|
+
deleteModalOpen = false;
|
|
177
105
|
taskToDelete = '';
|
|
178
106
|
}
|
|
179
107
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
isDeleting = false;
|
|
183
|
-
taskToDelete = '';
|
|
108
|
+
function cronLabel(expression) {
|
|
109
|
+
return scheduleOptions.find((s) => s.value === expression)?.label ?? expression;
|
|
184
110
|
}
|
|
185
111
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const match = schedules.find((option) => option.value === cronExpression);
|
|
189
|
-
return match ? match.label : cronExpression;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
onMount(() => {
|
|
193
|
-
fetchScheduledTests();
|
|
194
|
-
fetchSchedules(); // <-- also call this here
|
|
112
|
+
onMount(async () => {
|
|
113
|
+
[cronJobs, scheduleOptions] = await Promise.all([fetchCronJobs(), fetchSchedules()]);
|
|
195
114
|
});
|
|
196
115
|
</script>
|
|
197
116
|
|
|
198
|
-
<!-- Add/
|
|
199
|
-
<
|
|
200
|
-
<
|
|
201
|
-
<div class="
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
117
|
+
<!-- Add/Edit modal -->
|
|
118
|
+
<Modal bind:open={modalOpen} title={isEditing ? 'Edit Cron Job' : 'New Cron Job'}>
|
|
119
|
+
<form on:submit|preventDefault={handleSave} class="modal-form">
|
|
120
|
+
<div class="field">
|
|
121
|
+
<div class="field-label">
|
|
122
|
+
<span>Task Name</span>
|
|
123
|
+
<span class="field-hint">
|
|
124
|
+
{isEditing ? 'Name is the ID — cannot be changed' : 'Use a unique, meaningful name'}
|
|
125
|
+
</span>
|
|
126
|
+
</div>
|
|
127
|
+
<input
|
|
128
|
+
type="text"
|
|
129
|
+
class="field-input"
|
|
130
|
+
bind:value={form.taskName}
|
|
131
|
+
placeholder="nightly-login-suite"
|
|
132
|
+
disabled={isEditing}
|
|
133
|
+
required
|
|
134
|
+
/>
|
|
205
135
|
</div>
|
|
206
136
|
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
<div>
|
|
211
|
-
<div for="taskName" class="label">
|
|
212
|
-
<span class="label-text">Task Name</span>
|
|
213
|
-
<span class="label-text-alt"
|
|
214
|
-
>{isEditing
|
|
215
|
-
? 'Name is used as ID. This cannot be changed.'
|
|
216
|
-
: 'Use a meaningful name'}</span
|
|
217
|
-
>
|
|
218
|
-
</div>
|
|
219
|
-
<input
|
|
220
|
-
type="text"
|
|
221
|
-
id="taskName"
|
|
222
|
-
bind:value={taskName}
|
|
223
|
-
class="input input-bordered w-full"
|
|
224
|
-
placeholder="Task Name"
|
|
225
|
-
required
|
|
226
|
-
disabled={isEditing}
|
|
227
|
-
/>
|
|
137
|
+
<div class="field">
|
|
138
|
+
<div class="field-label">
|
|
139
|
+
<span>Schedule</span>
|
|
228
140
|
</div>
|
|
229
|
-
<
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
>
|
|
240
|
-
|
|
241
|
-
{#each schedules as schedule}
|
|
242
|
-
<option value={schedule.value}>{schedule.label}</option>
|
|
243
|
-
{/each}
|
|
244
|
-
</select>
|
|
245
|
-
</div>
|
|
246
|
-
<div>
|
|
247
|
-
<div for="tags" class="label">
|
|
248
|
-
<span class="label-text">Tags</span>
|
|
249
|
-
<span class="label-text-alt">If using multiple tags, "@test-1 or @test-2".</span>
|
|
250
|
-
</div>
|
|
251
|
-
<input
|
|
252
|
-
type="text"
|
|
253
|
-
id="tags"
|
|
254
|
-
bind:value={tags}
|
|
255
|
-
class="input input-bordered w-full"
|
|
256
|
-
placeholder="Tags"
|
|
257
|
-
required
|
|
258
|
-
/>
|
|
141
|
+
<select class="field-input" bind:value={form.cronExpression} required>
|
|
142
|
+
<option value="" disabled selected>Select a schedule</option>
|
|
143
|
+
{#each scheduleOptions as opt}
|
|
144
|
+
<option value={opt.value}>{opt.label}</option>
|
|
145
|
+
{/each}
|
|
146
|
+
</select>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div class="field">
|
|
150
|
+
<div class="field-label">
|
|
151
|
+
<span>Tags</span>
|
|
152
|
+
<span class="field-hint">Multiple: @test-1 or @test-2</span>
|
|
259
153
|
</div>
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
<!-- Delete Confirmation Modal -->
|
|
268
|
-
<dialog id="delete_confirmation_modal" class="modal" class:modal-open={isDeleting}>
|
|
269
|
-
<div class="modal-box">
|
|
270
|
-
<h2 class="card-title mb-4">Are you sure you want to delete this cron job?</h2>
|
|
271
|
-
<div class="flex justify-center gap-4">
|
|
272
|
-
<button class="btn btn-error" on:click={confirmDelete}>Yes</button>
|
|
273
|
-
<button class="btn" on:click={cancelDelete}>No</button>
|
|
154
|
+
<input
|
|
155
|
+
type="text"
|
|
156
|
+
class="field-input"
|
|
157
|
+
bind:value={form.tags}
|
|
158
|
+
placeholder="@suite-login"
|
|
159
|
+
required
|
|
160
|
+
/>
|
|
274
161
|
</div>
|
|
162
|
+
|
|
163
|
+
{#if formError}
|
|
164
|
+
<p class="form-error">{formError}</p>
|
|
165
|
+
{/if}
|
|
166
|
+
|
|
167
|
+
<Button type="submit" size="md">
|
|
168
|
+
{isEditing ? 'Save Changes' : 'Add Cron Job'}
|
|
169
|
+
</Button>
|
|
170
|
+
</form>
|
|
171
|
+
</Modal>
|
|
172
|
+
|
|
173
|
+
<!-- Delete confirmation modal -->
|
|
174
|
+
<Modal bind:open={deleteModalOpen} title="Delete Cron Job">
|
|
175
|
+
<p class="confirm-text">
|
|
176
|
+
Are you sure you want to delete <strong>{taskToDelete}</strong>? This cannot be undone.
|
|
177
|
+
</p>
|
|
178
|
+
<div class="confirm-actions">
|
|
179
|
+
<Button variant="danger" on:click={handleDelete}>Delete</Button>
|
|
180
|
+
<Button variant="ghost" on:click={() => (deleteModalOpen = false)}>Cancel</Button>
|
|
275
181
|
</div>
|
|
276
|
-
</
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
<svg
|
|
285
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
286
|
-
class="h-6 w-6 shrink-0 stroke-current"
|
|
287
|
-
fill="none"
|
|
288
|
-
viewBox="0 0 24 24"
|
|
289
|
-
>
|
|
290
|
-
<path
|
|
291
|
-
stroke-linecap="round"
|
|
292
|
-
stroke-linejoin="round"
|
|
293
|
-
stroke-width="2"
|
|
294
|
-
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
295
|
-
/>
|
|
296
|
-
</svg>
|
|
297
|
-
<span>{successMessage}</span>
|
|
298
|
-
</div>
|
|
299
|
-
{/if}
|
|
300
|
-
|
|
301
|
-
{#if errorMessage}
|
|
302
|
-
<div role="alert" class="alert alert-error">
|
|
303
|
-
<svg
|
|
304
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
305
|
-
class="h-6 w-6 shrink-0 stroke-current"
|
|
306
|
-
fill="none"
|
|
307
|
-
viewBox="0 0 24 24"
|
|
308
|
-
>
|
|
309
|
-
<path
|
|
310
|
-
stroke-linecap="round"
|
|
311
|
-
stroke-linejoin="round"
|
|
312
|
-
stroke-width="2"
|
|
313
|
-
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
314
|
-
/>
|
|
315
|
-
</svg>
|
|
316
|
-
<span>{errorMessage}</span>
|
|
317
|
-
</div>
|
|
318
|
-
{/if}
|
|
319
|
-
<div class="mt-4">
|
|
320
|
-
{#if scheduledTests.length > 0}
|
|
321
|
-
<div class="overflow-x-auto">
|
|
322
|
-
<table class="table table-zebra">
|
|
323
|
-
<thead>
|
|
324
|
-
<tr>
|
|
325
|
-
<th>Name</th>
|
|
326
|
-
<th>Frequency</th>
|
|
327
|
-
<th>Tags</th>
|
|
328
|
-
<th>Action</th>
|
|
329
|
-
</tr>
|
|
330
|
-
</thead>
|
|
331
|
-
<tbody>
|
|
332
|
-
{#each scheduledTests as scheduledTest}
|
|
333
|
-
<tr>
|
|
334
|
-
<td>{scheduledTest.taskName}</td>
|
|
335
|
-
<td>{getCronLabel(scheduledTest.cronExpression)}</td>
|
|
336
|
-
<td>{scheduledTest.tags}</td>
|
|
337
|
-
<td>
|
|
338
|
-
<button
|
|
339
|
-
class="btn btn-warning btn-sm"
|
|
340
|
-
on:click={() => openEditModal(scheduledTest)}
|
|
341
|
-
>
|
|
342
|
-
Edit
|
|
343
|
-
</button>
|
|
344
|
-
<button
|
|
345
|
-
class="btn btn-error btn-sm"
|
|
346
|
-
on:click={() => openDeleteConfirmation(scheduledTest.taskName)}
|
|
347
|
-
>
|
|
348
|
-
Delete
|
|
349
|
-
</button>
|
|
350
|
-
</td>
|
|
351
|
-
</tr>
|
|
352
|
-
{/each}
|
|
353
|
-
</tbody>
|
|
354
|
-
</table>
|
|
355
|
-
</div>
|
|
356
|
-
{:else}
|
|
357
|
-
<p>No Scheduled Tests Available.</p>
|
|
358
|
-
{/if}
|
|
359
|
-
</div>
|
|
360
|
-
<button class="btn mt-2" on:click={() => (isModalOpen = true)}>Add New Cron Job</button>
|
|
182
|
+
</Modal>
|
|
183
|
+
|
|
184
|
+
<!-- Page -->
|
|
185
|
+
<div class="page-header">
|
|
186
|
+
<div class="header-row">
|
|
187
|
+
<div>
|
|
188
|
+
<h1>Scheduled Tests</h1>
|
|
189
|
+
<p class="subtitle">Manage recurring test runs via cron jobs</p>
|
|
361
190
|
</div>
|
|
191
|
+
<Button on:click={openAddModal}>+ New Job</Button>
|
|
362
192
|
</div>
|
|
363
193
|
</div>
|
|
194
|
+
|
|
195
|
+
{#if toast}
|
|
196
|
+
<div class="toast alert alert-{toast.type}" transition:fly={{ y: -8, duration: 240 }}>
|
|
197
|
+
{toast.message}
|
|
198
|
+
</div>
|
|
199
|
+
{/if}
|
|
200
|
+
|
|
201
|
+
<div class="card" style="padding: 0; overflow: hidden;">
|
|
202
|
+
{#if cronJobs.length === 0}
|
|
203
|
+
<p class="empty">No scheduled tests yet. Create one to get started.</p>
|
|
204
|
+
{:else}
|
|
205
|
+
<div class="table-wrap">
|
|
206
|
+
<table class="data-table">
|
|
207
|
+
<thead>
|
|
208
|
+
<tr>
|
|
209
|
+
<th>Name</th>
|
|
210
|
+
<th>Schedule</th>
|
|
211
|
+
<th>Tags</th>
|
|
212
|
+
<th>Actions</th>
|
|
213
|
+
</tr>
|
|
214
|
+
</thead>
|
|
215
|
+
<tbody>
|
|
216
|
+
{#each cronJobs as job, i}
|
|
217
|
+
<tr style="animation-delay: {i * 45}ms" class="job-row">
|
|
218
|
+
<td class="job-name">{job.taskName}</td>
|
|
219
|
+
<td>
|
|
220
|
+
<Badge variant="schedule">{cronLabel(job.cronExpression)}</Badge>
|
|
221
|
+
</td>
|
|
222
|
+
<td>
|
|
223
|
+
<span class="tag-text">{job.tags}</span>
|
|
224
|
+
</td>
|
|
225
|
+
<td class="actions-cell">
|
|
226
|
+
<Button variant="ghost" size="sm" on:click={() => openEditModal(job)}>Edit</Button>
|
|
227
|
+
<Button variant="danger" size="sm" on:click={() => openDeleteModal(job.taskName)}
|
|
228
|
+
>Delete</Button
|
|
229
|
+
>
|
|
230
|
+
</td>
|
|
231
|
+
</tr>
|
|
232
|
+
{/each}
|
|
233
|
+
</tbody>
|
|
234
|
+
</table>
|
|
235
|
+
</div>
|
|
236
|
+
{/if}
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<style>
|
|
240
|
+
.page-header {
|
|
241
|
+
margin-bottom: 2rem;
|
|
242
|
+
padding-bottom: 1.5rem;
|
|
243
|
+
border-bottom: 1px solid var(--border);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.header-row {
|
|
247
|
+
display: flex;
|
|
248
|
+
align-items: flex-start;
|
|
249
|
+
justify-content: space-between;
|
|
250
|
+
gap: 1rem;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.page-header h1 {
|
|
254
|
+
font-size: 2.5rem;
|
|
255
|
+
margin-bottom: 0.375rem;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.subtitle {
|
|
259
|
+
color: var(--text-muted);
|
|
260
|
+
font-size: 0.9375rem;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.job-row {
|
|
264
|
+
animation: fadeUp 0.32s var(--ease-out) both;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.toast {
|
|
268
|
+
margin-bottom: 1.25rem;
|
|
269
|
+
border-radius: var(--radius-md);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.table-wrap {
|
|
273
|
+
overflow-x: auto;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.job-name {
|
|
277
|
+
font-weight: 400;
|
|
278
|
+
font-size: 0.875rem;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.tag-text {
|
|
282
|
+
font-family: 'JetBrains Mono', monospace;
|
|
283
|
+
font-size: 0.78rem;
|
|
284
|
+
color: var(--text-muted);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.actions-cell {
|
|
288
|
+
display: flex;
|
|
289
|
+
gap: 0.375rem;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.empty {
|
|
293
|
+
padding: 3rem 1.5rem;
|
|
294
|
+
color: var(--text-muted);
|
|
295
|
+
font-size: 0.9375rem;
|
|
296
|
+
text-align: center;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* Modal form */
|
|
300
|
+
.modal-form {
|
|
301
|
+
display: flex;
|
|
302
|
+
flex-direction: column;
|
|
303
|
+
gap: 1rem;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.form-error {
|
|
307
|
+
font-size: 0.8125rem;
|
|
308
|
+
color: var(--fail);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.confirm-text {
|
|
312
|
+
font-size: 0.9375rem;
|
|
313
|
+
color: var(--text-muted);
|
|
314
|
+
line-height: 1.6;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.confirm-text strong {
|
|
318
|
+
color: var(--text);
|
|
319
|
+
font-weight: 500;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.confirm-actions {
|
|
323
|
+
display: flex;
|
|
324
|
+
gap: 0.625rem;
|
|
325
|
+
padding-top: 0.25rem;
|
|
326
|
+
}
|
|
327
|
+
</style>
|
|
@@ -15,14 +15,11 @@
|
|
|
15
15
|
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import adapter from '@sveltejs/adapter-
|
|
18
|
+
import adapter from '@sveltejs/adapter-node';
|
|
19
19
|
|
|
20
20
|
/** @type {import('@sveltejs/kit').Config} */
|
|
21
21
|
const config = {
|
|
22
22
|
kit: {
|
|
23
|
-
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
|
24
|
-
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
|
25
|
-
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
|
26
23
|
adapter: adapter()
|
|
27
24
|
}
|
|
28
25
|
};
|
|
@@ -18,27 +18,6 @@
|
|
|
18
18
|
/** @type {import('tailwindcss').Config} */
|
|
19
19
|
export default {
|
|
20
20
|
content: ['./src/**/*.{html,js,svelte,ts}'],
|
|
21
|
-
theme: {
|
|
22
|
-
|
|
23
|
-
animation: {
|
|
24
|
-
'rise-quick': 'rise 0.6s ease-out' // Quick rising animation
|
|
25
|
-
},
|
|
26
|
-
keyframes: {
|
|
27
|
-
rise: {
|
|
28
|
-
'0%': { transform: 'translateY(20px)', opacity: '0' }, // Start slightly below, invisible
|
|
29
|
-
'100%': { transform: 'translateY(0)', opacity: '1' } // End in place, fully visible
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
plugins: [require('daisyui')],
|
|
35
|
-
daisyui: {
|
|
36
|
-
themes: ['emerald', 'dark'], // false: only light + dark | true: all themes | array: specific themes like this ["light", "dark", "cupcake"]
|
|
37
|
-
base: true, // applies background color and foreground color for root element by default
|
|
38
|
-
styled: true, // include daisyUI colors and design decisions for all components
|
|
39
|
-
utils: true, // adds responsive and modifier utility classes
|
|
40
|
-
prefix: '', // prefix for daisyUI classnames (components, modifiers and responsive class names. Not colors)
|
|
41
|
-
logs: true, // Shows info about daisyUI version and used config in the console when building your CSS
|
|
42
|
-
themeRoot: ':root' // The element that receives theme color CSS variables
|
|
43
|
-
}
|
|
21
|
+
theme: { extend: {} },
|
|
22
|
+
plugins: []
|
|
44
23
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plum-e2e",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/silverlunah/plum.git"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"description": "A detached test automation environment that combines Playwright and Cucumber with a Svelte frontend and an Express backend. It allows users to trigger tests, monitor reports, and schedule test runs through an intuitive UI.",
|
|
9
9
|
"main": "index.js",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"init": "(npm install) && (cd backend && npm install && npm run
|
|
11
|
+
"init": "(npm install) && (cd backend && npm install && npm run init) && (cd frontend && npm install && echo 'Frontend install complete')",
|
|
12
12
|
"format": "prettier --write .",
|
|
13
13
|
"add-license": "npx license-check-and-add add -f license-config.json",
|
|
14
14
|
"prepare": "husky",
|