plum-e2e 2.8.6 → 2.9.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.
Files changed (57) hide show
  1. package/backend/_scaffold/utils/browser.ts +184 -30
  2. package/backend/_scaffold/utils/hooks.ts +37 -8
  3. package/backend/app.js +0 -5
  4. package/backend/config/scripts/generate-report.js +2 -2
  5. package/backend/constants/socketEvents.js +7 -4
  6. package/backend/lib/reportFilename.js +1 -2
  7. package/backend/lib/{screenshotPoller.js → rrwebPoller.js} +7 -4
  8. package/backend/lib/serverBootstrap.js +14 -0
  9. package/backend/logs/runner-cmtbz5b1l0000mr0110b27w5k.log +8 -0
  10. package/backend/mcp/server.js +3 -47
  11. package/backend/package-lock.json +199 -1
  12. package/backend/package.json +3 -1
  13. package/backend/prisma/migrations/20260828120000_add_recording_and_split_runner_worker_count/migration.sql +39 -0
  14. package/backend/prisma/migrations/20260828140000_add_recording_started_ended_at/migration.sql +5 -0
  15. package/backend/prisma/migrations/20260828150000_strip_screenshot_refs_from_reports/migration.sql +34 -0
  16. package/backend/prisma/migrations/20260828160000_add_backup_include_reports/migration.sql +4 -0
  17. package/backend/prisma/schema.prisma +97 -70
  18. package/backend/routes/backup.routes.js +47 -1
  19. package/backend/routes/reports.routes.js +23 -0
  20. package/backend/server.js +1 -1
  21. package/backend/services/backupCronService.js +1 -1
  22. package/backend/services/backupService.js +134 -44
  23. package/backend/services/cronService.js +23 -15
  24. package/backend/services/nodeExecutionService.js +41 -15
  25. package/backend/services/nodeStreamRegistry.js +24 -0
  26. package/backend/services/reportService.js +167 -83
  27. package/backend/services/runnerService.js +19 -7
  28. package/backend/services/settingsService.js +6 -3
  29. package/backend/services/triggerService.js +20 -13
  30. package/backend/websockets/nodeSocketHandler.js +40 -0
  31. package/backend/websockets/socketHandler.js +9 -7
  32. package/frontend/.svelte-kit/generated/server/internal.js +1 -1
  33. package/frontend/package-lock.json +121 -32
  34. package/frontend/package.json +1 -0
  35. package/frontend/src/lib/api/reports.js +13 -4
  36. package/frontend/src/lib/api/settings.js +16 -1
  37. package/frontend/src/lib/components/layout/RunnerPanel.svelte +9 -24
  38. package/frontend/src/lib/components/reports/ElementInspector.svelte +141 -0
  39. package/frontend/src/lib/components/reports/LiveReplayer.svelte +110 -0
  40. package/frontend/src/lib/components/reports/MultiTabTimeline.svelte +115 -0
  41. package/frontend/src/lib/components/reports/RecordingPlayer.svelte +786 -0
  42. package/frontend/src/lib/components/reports/StepsRail.svelte +109 -0
  43. package/frontend/src/lib/components/ui/CodeViewer.svelte +61 -0
  44. package/frontend/src/lib/constants.js +0 -1
  45. package/frontend/src/lib/copy/reports.js +18 -8
  46. package/frontend/src/lib/copy/settings.js +25 -4
  47. package/frontend/src/lib/socketEvents.js +7 -4
  48. package/frontend/src/lib/stores/runner.js +26 -3
  49. package/frontend/src/lib/styles/tokens.css +7 -0
  50. package/frontend/src/lib/utils/format.js +108 -2
  51. package/frontend/src/lib/utils/inspectElement.js +34 -0
  52. package/frontend/src/routes/reports/+page.svelte +79 -1
  53. package/frontend/src/routes/reports/[id]/+page.svelte +304 -495
  54. package/frontend/src/routes/reports/live/+page.svelte +236 -260
  55. package/frontend/src/routes/settings/+page.svelte +246 -8
  56. package/package.json +1 -1
  57. package/backend/playwright.config.js +0 -85
@@ -18,6 +18,8 @@
18
18
  saveBackupConfig,
19
19
  testBackupS3,
20
20
  runBackupNow,
21
+ fetchS3Backups,
22
+ restoreFromS3,
21
23
  fetchMcpConfig,
22
24
  generateMcpKey as generateMcpKeyApi
23
25
  } from '$lib/api/settings';
@@ -214,8 +216,10 @@
214
216
  IMPORT_BLOCK_TITLE,
215
217
  IMPORT_BLOCK_DESC,
216
218
  CHOOSE_FILE_LABEL,
217
- BACKUP_DISCLAIMER_PREFIX,
218
- BACKUP_DISCLAIMER_SUFFIX,
219
+ INCLUDE_REPORTS_LABEL,
220
+ INCLUDE_REPORTS_HINT,
221
+ includeReportsDisclaimer,
222
+ saveIncludeReportsLabel,
219
223
  S3_STORAGE_CARD_TITLE,
220
224
  S3_STORAGE_DESC_PREFIX,
221
225
  ENDPOINT_URL_LABEL,
@@ -240,6 +244,15 @@
240
244
  S3_CONNECTION_FAILED,
241
245
  BACKUP_CONFIG_SAVED_TOAST,
242
246
  BACKUP_CONFIG_SAVE_FAILED,
247
+ RESTORE_FROM_S3_CARD_TITLE,
248
+ RESTORE_FROM_S3_DESC,
249
+ CONFIGURE_S3_FIRST_RESTORE_MESSAGE,
250
+ NO_S3_BACKUPS_MESSAGE,
251
+ RESTORE_CONFIRM_TITLE,
252
+ restoreConfirmBody,
253
+ RESTORE_SUCCESS_TOAST,
254
+ RESTORE_FAILED_FALLBACK,
255
+ LIST_S3_BACKUPS_FAILED,
243
256
  SCHEDULED_BACKUP_CARD_TITLE,
244
257
  CONFIGURE_S3_FIRST_MESSAGE,
245
258
  ENABLE_SCHEDULED_BACKUP_LABEL,
@@ -264,7 +277,10 @@
264
277
  saveS3ConfigLabel,
265
278
  uploadedToLabel,
266
279
  uploadS3NowLabel,
267
- saveScheduleLabel
280
+ saveScheduleLabel,
281
+ restoreLabel,
282
+ refreshingLabel,
283
+ backupSizeLabel
268
284
  } from '$lib/copy/settings';
269
285
 
270
286
  /** @type {'project' | 'runners' | 'repository' | 'integrations' | 'mcp' | 'account' | 'users' | 'backup'} */
@@ -335,9 +351,11 @@
335
351
  backupS3Bucket: '',
336
352
  backupS3AccessKey: '',
337
353
  backupS3SecretKey: '',
338
- backupS3Prefix: ''
354
+ backupS3Prefix: '',
355
+ backupIncludeReports: false
339
356
  };
340
357
  let backupConfigSaving = false;
358
+ let includeReportsSaving = false;
341
359
  let backupS3SecretKeySet = false;
342
360
  let backupTestingS3 = false;
343
361
  let backupRunningNow = false;
@@ -346,6 +364,13 @@
346
364
  let backupLastRunAt = null;
347
365
  let backupLastStatus = '';
348
366
 
367
+ let s3Backups = [];
368
+ let s3BackupsLoaded = false;
369
+ let loadingS3Backups = false;
370
+ let restoringKey = null;
371
+ let restoreConfirmOpen = false;
372
+ let restoreTarget = null;
373
+
349
374
  let runners = [];
350
375
  let runnerForm = { name: '', url: '', token: '', browser: 'chromium' };
351
376
  let runnerFormError = '';
@@ -406,7 +431,8 @@
406
431
  backupS3Bucket: bc.backupS3Bucket,
407
432
  backupS3AccessKey: bc.backupS3AccessKey,
408
433
  backupS3SecretKey: '',
409
- backupS3Prefix: bc.backupS3Prefix
434
+ backupS3Prefix: bc.backupS3Prefix,
435
+ backupIncludeReports: bc.backupIncludeReports
410
436
  };
411
437
  } catch {}
412
438
  if ($auth.user) {
@@ -802,6 +828,23 @@
802
828
  }
803
829
  }
804
830
 
831
+ async function handleSaveIncludeReports() {
832
+ includeReportsSaving = true;
833
+ try {
834
+ const payload = { ...backupConfig };
835
+ if (!payload.backupS3SecretKey) delete payload.backupS3SecretKey;
836
+ const result = await saveBackupConfig(payload);
837
+ if (result.error) throw new Error(result.error);
838
+ if (backupConfig.backupS3SecretKey) backupS3SecretKeySet = true;
839
+ backupConfig = { ...backupConfig, backupS3SecretKey: '' };
840
+ showToast('success', BACKUP_CONFIG_SAVED_TOAST);
841
+ } catch (e) {
842
+ showToast('error', e.message || BACKUP_CONFIG_SAVE_FAILED);
843
+ } finally {
844
+ includeReportsSaving = false;
845
+ }
846
+ }
847
+
805
848
  async function handleTestS3() {
806
849
  backupTestingS3 = true;
807
850
  backupS3TestResult = null;
@@ -845,6 +888,48 @@
845
888
  backupS3SecretKeySet
846
889
  );
847
890
 
891
+ // Fetch the list once, the first time S3 looks configured — the Refresh
892
+ // button covers reloading after a new upload or a bucket/prefix change.
893
+ $: if (s3Configured && !s3BackupsLoaded) {
894
+ s3BackupsLoaded = true;
895
+ loadS3Backups();
896
+ }
897
+
898
+ async function loadS3Backups() {
899
+ loadingS3Backups = true;
900
+ try {
901
+ const result = await fetchS3Backups();
902
+ if (result.error) throw new Error(result.error);
903
+ s3Backups = result.backups ?? [];
904
+ } catch (e) {
905
+ showToast('error', e.message || LIST_S3_BACKUPS_FAILED);
906
+ } finally {
907
+ loadingS3Backups = false;
908
+ }
909
+ }
910
+
911
+ function openRestoreConfirm(backup) {
912
+ restoreTarget = backup;
913
+ restoreConfirmOpen = true;
914
+ }
915
+
916
+ async function handleRestoreFromS3() {
917
+ if (!restoreTarget) return;
918
+ restoringKey = restoreTarget.key;
919
+ try {
920
+ const result = await restoreFromS3(restoreTarget.key);
921
+ if (result.error) throw new Error(result.error);
922
+ showToast('success', RESTORE_SUCCESS_TOAST);
923
+ project = await fetchProject();
924
+ restoreConfirmOpen = false;
925
+ restoreTarget = null;
926
+ } catch (e) {
927
+ showToast('error', e.message || RESTORE_FAILED_FALLBACK);
928
+ } finally {
929
+ restoringKey = null;
930
+ }
931
+ }
932
+
848
933
  $: mcpConfigSnippet = JSON.stringify(
849
934
  {
850
935
  mcpServers: {
@@ -1822,10 +1907,36 @@
1822
1907
  </div>
1823
1908
  </div>
1824
1909
  </div>
1910
+
1911
+ <div class="include-reports-row">
1912
+ <label class="field-label backup-toggle-label" for="include-reports">
1913
+ <span>
1914
+ {INCLUDE_REPORTS_LABEL}
1915
+ <span class="field-hint">{INCLUDE_REPORTS_HINT}</span>
1916
+ </span>
1917
+ <button
1918
+ id="include-reports"
1919
+ class="toggle-btn"
1920
+ class:active={backupConfig.backupIncludeReports}
1921
+ on:click={() =>
1922
+ (backupConfig.backupIncludeReports = !backupConfig.backupIncludeReports)}
1923
+ role="switch"
1924
+ aria-checked={backupConfig.backupIncludeReports}
1925
+ >
1926
+ <span class="toggle-thumb"></span>
1927
+ </button>
1928
+ </label>
1929
+ <Button
1930
+ variant="ghost"
1931
+ on:click={handleSaveIncludeReports}
1932
+ disabled={includeReportsSaving}
1933
+ >
1934
+ {saveIncludeReportsLabel(includeReportsSaving)}
1935
+ </Button>
1936
+ </div>
1937
+
1825
1938
  <p class="backup-disclaimer">
1826
- {BACKUP_DISCLAIMER_PREFIX}
1827
- <code>pg_dump</code>
1828
- {BACKUP_DISCLAIMER_SUFFIX}
1939
+ {includeReportsDisclaimer(backupConfig.backupIncludeReports)}
1829
1940
  </p>
1830
1941
  </div>
1831
1942
 
@@ -1947,6 +2058,64 @@
1947
2058
  {/if}
1948
2059
  </div>
1949
2060
 
2061
+ <!-- Restore from S3 -->
2062
+ <ConfirmModal
2063
+ bind:open={restoreConfirmOpen}
2064
+ title={RESTORE_CONFIRM_TITLE}
2065
+ confirmLabel={restoreLabel(false)}
2066
+ loading={!!restoringKey}
2067
+ on:confirm={handleRestoreFromS3}
2068
+ >
2069
+ {#if restoreTarget}
2070
+ {restoreConfirmBody(restoreTarget.key)}
2071
+ {/if}
2072
+ </ConfirmModal>
2073
+
2074
+ <div class="card settings-card" class:card-disabled={!s3Configured}>
2075
+ <div class="card-title-row">
2076
+ <p class="card-title">{RESTORE_FROM_S3_CARD_TITLE}</p>
2077
+ {#if s3Configured}
2078
+ <Button variant="ghost" on:click={loadS3Backups} disabled={loadingS3Backups}>
2079
+ {refreshingLabel(loadingS3Backups)}
2080
+ </Button>
2081
+ {/if}
2082
+ </div>
2083
+
2084
+ {#if !s3Configured}
2085
+ <p class="backup-block-desc">{CONFIGURE_S3_FIRST_RESTORE_MESSAGE}</p>
2086
+ {:else}
2087
+ <p class="backup-block-desc" style="margin-bottom: 1rem;">{RESTORE_FROM_S3_DESC}</p>
2088
+
2089
+ {#if s3Backups.length === 0}
2090
+ <p class="backup-block-desc">
2091
+ {loadingS3Backups ? refreshingLabel(true) : NO_S3_BACKUPS_MESSAGE}
2092
+ </p>
2093
+ {:else}
2094
+ <ul class="s3-backup-list">
2095
+ {#each s3Backups as backup (backup.key)}
2096
+ <li class="s3-backup-row">
2097
+ <div class="s3-backup-info">
2098
+ <span class="s3-backup-key">{backup.key}</span>
2099
+ <span class="s3-backup-meta">
2100
+ {new Date(backup.lastModified).toLocaleString()} · {backupSizeLabel(
2101
+ backup.size
2102
+ )}
2103
+ </span>
2104
+ </div>
2105
+ <Button
2106
+ variant="ghost"
2107
+ on:click={() => openRestoreConfirm(backup)}
2108
+ disabled={restoringKey === backup.key}
2109
+ >
2110
+ {restoreLabel(restoringKey === backup.key)}
2111
+ </Button>
2112
+ </li>
2113
+ {/each}
2114
+ </ul>
2115
+ {/if}
2116
+ {/if}
2117
+ </div>
2118
+
1950
2119
  <!-- Scheduled backup -->
1951
2120
  <div class="card settings-card" class:card-disabled={!s3Configured}>
1952
2121
  <p class="card-title">{SCHEDULED_BACKUP_CARD_TITLE}</p>
@@ -2390,6 +2559,23 @@
2390
2559
  border-radius: 3px;
2391
2560
  }
2392
2561
 
2562
+ .include-reports-row {
2563
+ display: flex;
2564
+ align-items: center;
2565
+ gap: 1.25rem;
2566
+ margin-top: 1.25rem;
2567
+ padding-top: 1.25rem;
2568
+ border-top: 1px solid var(--border);
2569
+ }
2570
+ .include-reports-row .backup-toggle-label {
2571
+ flex: 1;
2572
+ }
2573
+ .include-reports-row .field-label > span {
2574
+ display: flex;
2575
+ flex-direction: column;
2576
+ gap: 0.15rem;
2577
+ }
2578
+
2393
2579
  .backup-disclaimer {
2394
2580
  margin-top: 1rem;
2395
2581
  padding: 0.625rem 0.875rem;
@@ -2422,6 +2608,58 @@
2422
2608
  color: var(--fail);
2423
2609
  }
2424
2610
 
2611
+ .card-title-row {
2612
+ display: flex;
2613
+ align-items: center;
2614
+ justify-content: space-between;
2615
+ gap: 1rem;
2616
+ margin-bottom: 0.75rem;
2617
+ }
2618
+ .card-title-row .card-title {
2619
+ margin-bottom: 0;
2620
+ }
2621
+
2622
+ .s3-backup-list {
2623
+ display: flex;
2624
+ flex-direction: column;
2625
+ gap: 0.5rem;
2626
+ list-style: none;
2627
+ margin: 0;
2628
+ padding: 0;
2629
+ }
2630
+
2631
+ .s3-backup-row {
2632
+ display: flex;
2633
+ align-items: center;
2634
+ justify-content: space-between;
2635
+ gap: 1rem;
2636
+ padding: 0.625rem 0.875rem;
2637
+ background: var(--bg-subtle);
2638
+ border: 1px solid var(--border);
2639
+ border-radius: var(--radius-sm);
2640
+ }
2641
+
2642
+ .s3-backup-info {
2643
+ display: flex;
2644
+ flex-direction: column;
2645
+ gap: 0.2rem;
2646
+ min-width: 0;
2647
+ }
2648
+
2649
+ .s3-backup-key {
2650
+ font-family: 'JetBrains Mono', monospace;
2651
+ font-size: 0.8125rem;
2652
+ color: var(--text);
2653
+ overflow: hidden;
2654
+ text-overflow: ellipsis;
2655
+ white-space: nowrap;
2656
+ }
2657
+
2658
+ .s3-backup-meta {
2659
+ font-size: 0.75rem;
2660
+ color: var(--text-muted);
2661
+ }
2662
+
2425
2663
  .backup-last-run {
2426
2664
  font-size: 0.8125rem;
2427
2665
  color: var(--text-muted);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plum-e2e",
3
- "version": "2.8.6",
3
+ "version": "2.9.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/silverlunah/plum.git"
@@ -1,85 +0,0 @@
1
- /*
2
- * This file is part of Plum.
3
- * Licensed under the MIT License. See LICENSE file in the project root for details.
4
- */
5
-
6
- // @ts-check
7
- import { defineConfig, devices } from '@playwright/test';
8
-
9
- /**
10
- * Read environment variables from file.
11
- * https://github.com/motdotla/dotenv
12
- */
13
- import dotenv from 'dotenv';
14
- import path from 'path';
15
- dotenv.config({ path: path.resolve(__dirname, '.env') });
16
-
17
- /**
18
- * @see https://playwright.dev/docs/test-configuration
19
- */
20
- export default defineConfig({
21
- testDir: './tests',
22
- /* Run tests in files in parallel */
23
- fullyParallel: true,
24
- /* Fail the build on CI if you accidentally left test.only in the source code. */
25
- forbidOnly: !!process.env.CI,
26
- /* Retry on CI only */
27
- retries: process.env.CI ? 2 : 0,
28
- /* Opt out of parallel tests on CI. */
29
- workers: process.env.CI ? 1 : undefined,
30
- /* Reporter to use. See https://playwright.dev/docs/test-reporters */
31
- reporter: 'html',
32
- /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
33
- use: {
34
- /* Base URL to use in actions like `await page.goto('/')`. */
35
- // baseURL: 'http://127.0.0.1:3000',
36
-
37
- /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
38
- trace: 'on-first-retry'
39
- },
40
-
41
- /* Configure projects for major browsers */
42
- projects: [
43
- {
44
- name: 'chromium',
45
- use: { ...devices['Desktop Chrome'] }
46
- },
47
-
48
- {
49
- name: 'firefox',
50
- use: { ...devices['Desktop Firefox'] }
51
- },
52
-
53
- {
54
- name: 'webkit',
55
- use: { ...devices['Desktop Safari'] }
56
- }
57
-
58
- /* Test against mobile viewports. */
59
- // {
60
- // name: 'Mobile Chrome',
61
- // use: { ...devices['Pixel 5'] },
62
- // },
63
- // {
64
- // name: 'Mobile Safari',
65
- // use: { ...devices['iPhone 12'] },
66
- // },
67
-
68
- /* Test against branded browsers. */
69
- // {
70
- // name: 'Microsoft Edge',
71
- // use: { ...devices['Desktop Edge'], channel: 'msedge' },
72
- // },
73
- // {
74
- // name: 'Google Chrome',
75
- // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
76
- // },
77
- ]
78
-
79
- /* Run your local dev server before starting the tests */
80
- // webServer: {
81
- // command: 'npm run start',
82
- // url: 'http://127.0.0.1:3000',
83
- // reuseExistingServer: !process.env.CI,
84
- // },
85
- });