plum-e2e 1.0.10 → 1.1.1

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.
Files changed (51) hide show
  1. package/.claude/settings.local.json +16 -1
  2. package/.vscode/settings.json +10 -0
  3. package/README.md +151 -37
  4. package/backend/_scaffold/features/LoginPage.feature +45 -3
  5. package/backend/_scaffold/pages/HomepagePage.ts +7 -0
  6. package/backend/_scaffold/pages/LoginPage.ts +37 -13
  7. package/backend/_scaffold/step_definitions/HomepageSteps.ts +6 -0
  8. package/backend/_scaffold/step_definitions/LoginSteps.ts +30 -4
  9. package/backend/_scaffold/utils/browser.ts +33 -0
  10. package/backend/_scaffold/utils/hooks.ts +8 -29
  11. package/backend/_scaffold/utils/utils.ts +3 -9
  12. package/backend/config/scripts/create-settings.js +7 -14
  13. package/backend/config/scripts/create-step.mjs +268 -0
  14. package/backend/config/scripts/generate-report.js +31 -75
  15. package/backend/config/scripts/run-tests.js +19 -4
  16. package/backend/package-lock.json +56 -641
  17. package/backend/package.json +4 -1
  18. package/backend/routes/reports.routes.js +6 -10
  19. package/backend/services/envService.js +4 -10
  20. package/backend/services/reportService.js +70 -20
  21. package/backend/services/testService.js +99 -24
  22. package/backend/tsconfig.json +2 -2
  23. package/backend/websockets/socketHandler.js +12 -6
  24. package/bin/plum.js +49 -3
  25. package/frontend/package-lock.json +436 -135
  26. package/frontend/package.json +1 -1
  27. package/frontend/src/app.css +241 -6
  28. package/frontend/src/app.html +14 -1
  29. package/frontend/src/lib/api/reports.js +68 -0
  30. package/frontend/src/lib/api/schedules.js +64 -0
  31. package/frontend/src/lib/api/tests.js +41 -0
  32. package/frontend/src/lib/components/layout/Nav.svelte +304 -0
  33. package/frontend/src/lib/components/layout/PageShell.svelte +28 -0
  34. package/frontend/src/lib/components/layout/RunnerPanel.svelte +378 -0
  35. package/frontend/src/lib/components/ui/Badge.svelte +63 -0
  36. package/frontend/src/lib/components/ui/Button.svelte +117 -0
  37. package/frontend/src/lib/components/ui/Modal.svelte +140 -0
  38. package/frontend/src/lib/components/ui/Pagination.svelte +100 -0
  39. package/frontend/src/lib/components/ui/Terminal.svelte +100 -0
  40. package/frontend/src/lib/stores/runner.js +55 -0
  41. package/frontend/src/lib/stores/theme.js +47 -0
  42. package/frontend/src/routes/+layout.svelte +7 -12
  43. package/frontend/src/routes/+page.svelte +690 -142
  44. package/frontend/src/routes/reports/+page.svelte +395 -125
  45. package/frontend/src/routes/reports/[slug]/+page.svelte +749 -0
  46. package/frontend/src/routes/scheduled-tests/+page.svelte +267 -303
  47. package/frontend/svelte.config.js +1 -4
  48. package/frontend/tailwind.config.js +2 -23
  49. package/package.json +2 -2
  50. package/backend/_scaffold/utils/world.ts +0 -25
  51. package/frontend/src/routes/components/Navigation.svelte +0 -53
@@ -16,145 +16,415 @@
16
16
  -->
17
17
 
18
18
  <script>
19
- import { onMount } from 'svelte';
19
+ import { onMount, tick } from 'svelte';
20
+ import { fetchReports } from '$lib/api/reports';
21
+ import Badge from '$lib/components/ui/Badge.svelte';
22
+ import Pagination from '$lib/components/ui/Pagination.svelte';
20
23
 
21
24
  let reports = [];
22
25
  let currentPage = 1;
23
- const itemsPerPage = 10;
26
+ const PER_PAGE = 15;
27
+ let animateBar = false;
24
28
 
25
- async function fetchReports() {
26
- const response = await fetch('http://localhost:3001/reports');
27
- const data = await response.json();
29
+ $: totalPages = Math.ceil(reports.length / PER_PAGE);
30
+ $: paginated = reports.slice((currentPage - 1) * PER_PAGE, currentPage * PER_PAGE);
31
+ $: passCount = reports.filter((r) => r.status === 'PASS').length;
32
+ $: failCount = reports.length - passCount;
33
+ $: passRate = reports.length ? Math.round((passCount / reports.length) * 100) : 0;
34
+ $: trend = reports.slice(0, 12).reverse();
28
35
 
29
- reports = data.reports.map((fileName) => {
30
- // Updated regex to capture the trigger type and tags inside parentheses
31
- const match = fileName.match(
32
- /(PASS|FAIL)_cucumber_report_([^_]+)_\(([^)]+)\)_(\d{4})_(\d{2})_(\d{2})T(\d{2})_(\d{2})_(\d{2})_\d{3}Z\.html/
33
- );
34
- if (!match)
35
- return {
36
- fileName,
37
- status: 'Unknown',
38
- triggerType: 'Invalid Trigger',
39
- tags: 'Invalid Tags',
40
- date: 'Invalid Date'
41
- };
36
+ function triggerLabel(type) {
37
+ if (type === 'manual-trigger') return 'Manual';
38
+ if (type === 'command-line-trigger' || type === 'undefined') return 'CLI';
39
+ return 'Scheduled';
40
+ }
42
41
 
43
- const [_, status, triggerType, tagsString, year, month, day, hour, minute, second] = match;
42
+ function triggerVariant(type) {
43
+ if (type === 'manual-trigger') return 'tag';
44
+ if (type === 'command-line-trigger' || type === 'undefined') return 'neutral';
45
+ return 'schedule';
46
+ }
44
47
 
45
- // Format date properly
46
- const rawDate = new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`);
47
- const formattedDate = rawDate.toLocaleString();
48
+ onMount(async () => {
49
+ try {
50
+ reports = await fetchReports();
51
+ await tick();
52
+ animateBar = true;
53
+ } catch (e) {
54
+ console.error('Failed to fetch reports', e);
55
+ }
56
+ });
57
+ </script>
48
58
 
49
- return { fileName, status, triggerType, tags: tagsString, date: formattedDate };
50
- });
59
+ <div class="page-header">
60
+ <div class="header-top">
61
+ <div>
62
+ <h1>Reports</h1>
63
+ <p class="subtitle">
64
+ {reports.length} run{reports.length !== 1 ? 's' : ''} recorded
65
+ </p>
66
+ </div>
67
+
68
+ {#if reports.length > 0}
69
+ <div class="rate-display">
70
+ <span
71
+ class="rate-number"
72
+ class:pass={passRate >= 80}
73
+ class:warn={passRate < 80 && passRate >= 50}
74
+ class:fail={passRate < 50}
75
+ >
76
+ {passRate}%
77
+ </span>
78
+ <span class="rate-label">passing</span>
79
+ </div>
80
+ {/if}
81
+ </div>
82
+
83
+ {#if reports.length > 0}
84
+ <div class="stats-bar">
85
+ <div class="pass-bar-track">
86
+ <div class="pass-bar-fill" style="width: {animateBar ? passRate + '%' : '0'}"></div>
87
+ </div>
88
+ <div class="bar-legend">
89
+ <span class="legend-pass">{passCount} passed</span>
90
+ <span class="legend-fail">{failCount} failed</span>
91
+ </div>
92
+ </div>
93
+
94
+ <div class="trend-row">
95
+ <span class="trend-label">Recent</span>
96
+ <div class="trend-dots">
97
+ {#each trend as r, i}
98
+ <span
99
+ class="trend-dot"
100
+ class:pass={r.status === 'PASS'}
101
+ class:fail={r.status !== 'PASS'}
102
+ style="animation-delay: {i * 35}ms"
103
+ title="{r.status} · {r.tags} · {r.date}"
104
+ ></span>
105
+ {/each}
106
+ </div>
107
+ <span class="trend-hint">← older · newer →</span>
108
+ </div>
109
+ {/if}
110
+ </div>
111
+
112
+ {#if reports.length === 0}
113
+ <p class="empty">No reports yet. Run a test to generate one.</p>
114
+ {:else}
115
+ <div class="report-list">
116
+ {#each paginated as report, i}
117
+ <a
118
+ class="report-item"
119
+ class:is-pass={report.status === 'PASS'}
120
+ class:is-fail={report.status !== 'PASS'}
121
+ href="/reports/{encodeURIComponent(report.fileName)}"
122
+ style="animation-delay: {i * 45}ms"
123
+ >
124
+ <div class="item-left">
125
+ <span
126
+ class="status-mark"
127
+ class:pass={report.status === 'PASS'}
128
+ class:fail={report.status !== 'PASS'}
129
+ >
130
+ {report.status === 'PASS' ? '✓' : '✗'}
131
+ </span>
132
+ <div class="item-meta">
133
+ <span class="item-tags">{report.tags}</span>
134
+ <div class="item-badges">
135
+ <Badge variant={triggerVariant(report.triggerType)}>
136
+ {triggerLabel(report.triggerType)}
137
+ </Badge>
138
+ <Badge variant="neutral">
139
+ {report.runners} runner{report.runners !== 1 ? 's' : ''}
140
+ </Badge>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ <div class="item-right">
145
+ <span class="item-date">{report.date}</span>
146
+ <svg
147
+ class="item-arrow"
148
+ width="14"
149
+ height="14"
150
+ viewBox="0 0 24 24"
151
+ fill="none"
152
+ stroke="currentColor"
153
+ stroke-width="2"
154
+ stroke-linecap="round"
155
+ >
156
+ <line x1="5" y1="12" x2="19" y2="12" />
157
+ <polyline points="12 5 19 12 12 19" />
158
+ </svg>
159
+ </div>
160
+ </a>
161
+ {/each}
162
+ </div>
163
+
164
+ {#if totalPages > 1}
165
+ <div class="pagination-wrap">
166
+ <Pagination
167
+ current={currentPage}
168
+ total={totalPages}
169
+ on:change={(e) => (currentPage = e.detail)}
170
+ />
171
+ </div>
172
+ {/if}
173
+ {/if}
174
+
175
+ <style>
176
+ .page-header {
177
+ margin-bottom: 2rem;
178
+ padding-bottom: 1.5rem;
179
+ border-bottom: 1px solid var(--border);
51
180
  }
52
181
 
53
- // Calculate the start and end indices for pagination
54
- function paginatedReports() {
55
- const startIndex = (currentPage - 1) * itemsPerPage;
56
- const endIndex = startIndex + itemsPerPage;
57
- return reports.slice(startIndex, endIndex);
182
+ .header-top {
183
+ display: flex;
184
+ align-items: flex-end;
185
+ justify-content: space-between;
186
+ gap: 1rem;
187
+ margin-bottom: 1.25rem;
58
188
  }
59
189
 
60
- function goToPage(pageNumber) {
61
- if (pageNumber < 1 || pageNumber > totalPages()) return;
62
- currentPage = pageNumber;
190
+ h1 {
191
+ font-size: 2.5rem;
192
+ margin-bottom: 0.25rem;
63
193
  }
64
194
 
65
- function totalPages() {
66
- return Math.ceil(reports.length / itemsPerPage);
195
+ .subtitle {
196
+ color: var(--text-muted);
197
+ font-size: 0.875rem;
67
198
  }
68
199
 
69
- onMount(fetchReports);
70
- </script>
200
+ /* ── Pass rate ── */
201
+ .rate-display {
202
+ display: flex;
203
+ flex-direction: column;
204
+ align-items: flex-end;
205
+ gap: 0.1rem;
206
+ }
71
207
 
72
- <div class="flex justify-center items-center w-full my-4">
73
- <div class="card bg-base-300 rounded-box p-4">
74
- <div class="card-body text-left">
75
- <h2 class="card-title sticky top-0 bg-base-300 z-10">Reports</h2>
76
- <div class="mt-4">
77
- {#if reports.length > 0}
78
- <div class="overflow-x-auto">
79
- <table class="table">
80
- <thead>
81
- <tr>
82
- <th>Status</th>
83
- <th>Type</th>
84
- <th>Tags</th>
85
- <th>Date</th>
86
- </tr>
87
- </thead>
88
- <tbody>
89
- {#each paginatedReports() as report}
90
- <tr
91
- on:click={() =>
92
- window.open(`http://localhost:3001/reports/${report.fileName}`, '_blank')}
93
- style="cursor: pointer;"
94
- >
95
- <td>
96
- <span
97
- class="badge"
98
- class:badge-success={report.status === 'PASS'}
99
- class:badge-error={report.status === 'FAIL'}
100
- >
101
- {report.status}
102
- </span>
103
- </td>
104
- <td>
105
- {#if report.triggerType === 'manual-trigger'}
106
- <span class="badge badge-primary">Manual Trigger</span>
107
- {:else if report.triggerType === 'command-line-trigger'}
108
- <span class="badge badge-primary">CLI Trigger</span>
109
- {:else}
110
- <span class="badge badge-secondary">Scheduled: {report.triggerType}</span>
111
- {/if}
112
- </td>
113
- <td>
114
- <span class="badge badge-neutral">
115
- {report.tags}
116
- </span>
117
- </td>
118
- <td>
119
- {report.date}
120
- </td>
121
- </tr>
122
- {/each}
123
- </tbody>
124
- </table>
125
- </div>
126
- <!-- Pagination Controls -->
127
- <div class="flex justify-center mt-4">
128
- <div class="btn-group">
129
- <button
130
- class="btn btn-ghost"
131
- on:click={() => goToPage(currentPage - 1)}
132
- disabled={currentPage === 1}
133
- >
134
- Previous
135
- </button>
136
- {#each Array(totalPages()) as _, i}
137
- <button
138
- class="btn mx-1"
139
- on:click={() => goToPage(i + 1)}
140
- class:btn-active={currentPage === i + 1}
141
- >
142
- {i + 1}
143
- </button>
144
- {/each}
145
- <button
146
- class="btn btn-primary"
147
- on:click={() => goToPage(currentPage + 1)}
148
- disabled={currentPage === totalPages()}
149
- >
150
- Next
151
- </button>
152
- </div>
153
- </div>
154
- {:else}
155
- <p>No reports available.</p>
156
- {/if}
157
- </div>
158
- </div>
159
- </div>
160
- </div>
208
+ .rate-number {
209
+ font-family: var(--font-display);
210
+ font-size: 2.75rem;
211
+ line-height: 1;
212
+ font-weight: 400;
213
+ }
214
+
215
+ .rate-number.pass {
216
+ color: var(--pass);
217
+ }
218
+ .rate-number.warn {
219
+ color: var(--warn);
220
+ }
221
+ .rate-number.fail {
222
+ color: var(--fail);
223
+ }
224
+
225
+ .rate-label {
226
+ font-size: 0.75rem;
227
+ color: var(--text-muted);
228
+ letter-spacing: 0.05em;
229
+ text-transform: uppercase;
230
+ }
231
+
232
+ /* ── Stats bar ── */
233
+ .stats-bar {
234
+ margin-bottom: 1rem;
235
+ }
236
+
237
+ .pass-bar-track {
238
+ height: 3px;
239
+ background: var(--fail-soft);
240
+ border-radius: 100px;
241
+ overflow: hidden;
242
+ margin-bottom: 0.5rem;
243
+ }
244
+
245
+ .pass-bar-fill {
246
+ height: 100%;
247
+ background: var(--pass);
248
+ border-radius: 100px;
249
+ transition: width 0.9s var(--ease-out) 0.1s;
250
+ }
251
+
252
+ .bar-legend {
253
+ display: flex;
254
+ gap: 1rem;
255
+ }
256
+
257
+ .legend-pass {
258
+ font-size: 0.75rem;
259
+ color: var(--pass);
260
+ font-weight: 500;
261
+ }
262
+
263
+ .legend-fail {
264
+ font-size: 0.75rem;
265
+ color: var(--fail);
266
+ font-weight: 500;
267
+ }
268
+
269
+ /* ── Trend ── */
270
+ .trend-row {
271
+ display: flex;
272
+ align-items: center;
273
+ gap: 0.75rem;
274
+ }
275
+
276
+ .trend-label {
277
+ font-size: 0.7rem;
278
+ font-weight: 500;
279
+ letter-spacing: 0.07em;
280
+ text-transform: uppercase;
281
+ color: var(--text-muted);
282
+ flex-shrink: 0;
283
+ }
284
+
285
+ .trend-dots {
286
+ display: flex;
287
+ align-items: center;
288
+ gap: 5px;
289
+ }
290
+
291
+ .trend-dot {
292
+ width: 9px;
293
+ height: 9px;
294
+ border-radius: 50%;
295
+ transition: transform var(--duration-fast);
296
+ animation: fadeUp 0.3s var(--ease-out) both;
297
+ }
298
+
299
+ .trend-dot:hover {
300
+ transform: scale(1.4);
301
+ }
302
+
303
+ .trend-dot.pass {
304
+ background: var(--pass);
305
+ }
306
+ .trend-dot.fail {
307
+ background: var(--fail);
308
+ }
309
+
310
+ .trend-hint {
311
+ font-size: 0.68rem;
312
+ color: var(--text-muted);
313
+ opacity: 0.6;
314
+ }
315
+
316
+ /* ── Report list ── */
317
+ .report-list {
318
+ display: flex;
319
+ flex-direction: column;
320
+ gap: 0.5rem;
321
+ }
322
+
323
+ .report-item {
324
+ display: flex;
325
+ align-items: center;
326
+ justify-content: space-between;
327
+ gap: 1rem;
328
+ padding: 0.875rem 1.25rem;
329
+ border: 1px solid var(--border);
330
+ border-radius: var(--radius-md);
331
+ background: var(--bg-elevated);
332
+ text-decoration: none;
333
+ color: inherit;
334
+ border-left-width: 3px;
335
+ transition:
336
+ background var(--duration-fast),
337
+ transform var(--duration-fast) var(--ease-out),
338
+ box-shadow var(--duration-fast);
339
+ animation: fadeUp 0.32s var(--ease-out) both;
340
+ }
341
+
342
+ .report-item:hover {
343
+ background: var(--bg-subtle);
344
+ transform: translateX(2px);
345
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
346
+ }
347
+
348
+ .report-item.is-pass {
349
+ border-left-color: var(--pass);
350
+ }
351
+ .report-item.is-fail {
352
+ border-left-color: var(--fail);
353
+ }
354
+
355
+ .item-left {
356
+ display: flex;
357
+ align-items: center;
358
+ gap: 0.875rem;
359
+ min-width: 0;
360
+ }
361
+
362
+ .status-mark {
363
+ font-size: 0.875rem;
364
+ font-weight: 600;
365
+ flex-shrink: 0;
366
+ width: 20px;
367
+ text-align: center;
368
+ }
369
+
370
+ .status-mark.pass {
371
+ color: var(--pass);
372
+ }
373
+ .status-mark.fail {
374
+ color: var(--fail);
375
+ }
376
+
377
+ .item-meta {
378
+ display: flex;
379
+ align-items: center;
380
+ gap: 0.625rem;
381
+ min-width: 0;
382
+ }
383
+
384
+ .item-tags {
385
+ font-family: 'JetBrains Mono', monospace;
386
+ font-size: 0.8rem;
387
+ color: var(--text);
388
+ white-space: nowrap;
389
+ }
390
+
391
+ .item-badges {
392
+ flex-shrink: 0;
393
+ }
394
+
395
+ .item-right {
396
+ display: flex;
397
+ align-items: center;
398
+ gap: 0.75rem;
399
+ flex-shrink: 0;
400
+ }
401
+
402
+ .item-date {
403
+ font-size: 0.8rem;
404
+ color: var(--text-muted);
405
+ white-space: nowrap;
406
+ }
407
+
408
+ .item-arrow {
409
+ color: var(--text-muted);
410
+ transition:
411
+ transform var(--duration-fast) var(--ease-out),
412
+ color var(--duration-fast);
413
+ }
414
+
415
+ .report-item:hover .item-arrow {
416
+ transform: translateX(3px);
417
+ color: var(--text);
418
+ }
419
+
420
+ .pagination-wrap {
421
+ margin-top: 1.25rem;
422
+ }
423
+
424
+ .empty {
425
+ color: var(--text-muted);
426
+ font-size: 0.9375rem;
427
+ padding: 3rem 0;
428
+ text-align: center;
429
+ }
430
+ </style>