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
@@ -0,0 +1,110 @@
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
+ <script>
7
+ import { onMount, onDestroy } from 'svelte';
8
+ import Player from 'rrweb-player';
9
+ import 'rrweb-player/dist/style.css';
10
+
11
+ // A growing array — new events are appended by the caller as they stream
12
+ // in over the socket. liveMode lets rrweb-player start from zero events
13
+ // (it otherwise requires a complete Meta+FullSnapshot pair up front).
14
+ export let events = [];
15
+
16
+ const META_EVENT_TYPE = 4;
17
+
18
+ let stage;
19
+ let container;
20
+ let player = null;
21
+ let fedCount = 0;
22
+ let nativeWidth = 0;
23
+ let nativeHeight = 0;
24
+ let resizeObserver;
25
+
26
+ // rrweb-player renders at the recording's native resolution — scale-and-crop
27
+ // it to fill the stage (object-fit: cover) instead of letterboxing. Re-run on
28
+ // every stage resize, since the panel's own layout (tab strips appearing,
29
+ // window resize) can still shift its size after the player is built.
30
+ function updateScale() {
31
+ if (!nativeWidth || !nativeHeight) return;
32
+ const rect = stage.getBoundingClientRect();
33
+ const scale = Math.max(rect.width / nativeWidth, rect.height / nativeHeight);
34
+ container.style.transform = `scale(${scale})`;
35
+ }
36
+
37
+ // getMetaData() (called internally by rrweb-player's own controls on
38
+ // construction) reads events[0].timestamp unconditionally, so the initial
39
+ // batch must be passed in props rather than fed empty and via addEvent.
40
+ function buildPlayer() {
41
+ const rect = stage.getBoundingClientRect();
42
+ const meta = events[0]?.type === META_EVENT_TYPE ? events[0].data : null;
43
+ nativeWidth = meta?.width > 0 ? meta.width : rect.width;
44
+ nativeHeight = meta?.height > 0 ? meta.height : rect.height;
45
+
46
+ player = new Player({
47
+ target: container,
48
+ props: {
49
+ events,
50
+ liveMode: true,
51
+ autoPlay: false,
52
+ showController: false,
53
+ width: nativeWidth,
54
+ height: nativeHeight
55
+ }
56
+ });
57
+ fedCount = events.length;
58
+ player.getReplayer()?.startLive();
59
+ updateScale();
60
+ }
61
+
62
+ // Feed only what's newly arrived since the last run of this block —
63
+ // addEvent() is the incremental API, not a full-array replace.
64
+ $: if (player && events.length > fedCount) {
65
+ for (let i = fedCount; i < events.length; i++) player.addEvent(events[i]);
66
+ fedCount = events.length;
67
+ }
68
+
69
+ onMount(() => {
70
+ buildPlayer();
71
+ resizeObserver = new ResizeObserver(updateScale);
72
+ resizeObserver.observe(stage);
73
+ });
74
+ onDestroy(() => {
75
+ resizeObserver?.disconnect();
76
+ try {
77
+ player?.getReplayer()?.destroy();
78
+ } catch {
79
+ // already torn down
80
+ }
81
+ });
82
+ </script>
83
+
84
+ <div class="live-stage" bind:this={stage}>
85
+ <div class="live-mount" bind:this={container}></div>
86
+ </div>
87
+
88
+ <style>
89
+ .live-stage {
90
+ position: relative;
91
+ flex: 1;
92
+ min-width: 0;
93
+ min-height: 0;
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ background: var(--bg-subtle);
98
+ overflow: hidden;
99
+ }
100
+
101
+ .live-mount {
102
+ display: flex;
103
+ }
104
+
105
+ /* Sandboxed iframe (no allow-scripts/forms/top-navigation) — safe to always
106
+ allow pointer interaction so a taller live page can still be scrolled. */
107
+ .live-mount :global(iframe) {
108
+ pointer-events: auto !important;
109
+ }
110
+ </style>
@@ -0,0 +1,115 @@
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
+ <script>
7
+ import { createEventDispatcher } from 'svelte';
8
+
9
+ // Replaces rrweb-player's own timeline for multi-tab recordings — rrweb's
10
+ // timeline belongs to whichever player is currently mounted and resets on
11
+ // every segment hand-off, so it can't track absolute position across tabs.
12
+ export let from = 0;
13
+ export let to = 0;
14
+ export let position = 0;
15
+ export let stepTimestamps = [];
16
+
17
+ const dispatch = createEventDispatcher();
18
+
19
+ function percentOf(ts) {
20
+ return to > from ? Math.min(100, Math.max(0, ((ts - from) / (to - from)) * 100)) : 0;
21
+ }
22
+
23
+ function fmtClock(ms) {
24
+ if (ms <= 0) return '00:00';
25
+ const totalSeconds = Math.floor(ms / 1000);
26
+ const minutes = Math.floor(totalSeconds / 60);
27
+ const seconds = totalSeconds % 60;
28
+ return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
29
+ }
30
+
31
+ function handleClick(e) {
32
+ const rect = e.currentTarget.getBoundingClientRect();
33
+ const pct = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width));
34
+ dispatch('seek', from + pct * (to - from));
35
+ }
36
+
37
+ $: percent = percentOf(position);
38
+ // Mirrors rrweb-player's own per-step tick marks (its `customEvents` rendering).
39
+ $: tickPercents = stepTimestamps.map(percentOf);
40
+ </script>
41
+
42
+ <div class="combined-timeline">
43
+ <span class="combined-timeline-time">{fmtClock(position - from)}</span>
44
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
45
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
46
+ <div class="combined-timeline-track" on:click={handleClick}>
47
+ <div class="combined-timeline-fill" style="width: {percent}%"></div>
48
+ {#each tickPercents as pct}
49
+ <div class="combined-timeline-tick" style="left: {pct}%"></div>
50
+ {/each}
51
+ <div class="combined-timeline-handle" style="left: {percent}%"></div>
52
+ </div>
53
+ <span class="combined-timeline-time">{fmtClock(to - from)}</span>
54
+ </div>
55
+
56
+ <style>
57
+ /* Matches rrweb-player's own .rr-timeline/.rr-progress/.rr-progress__step/
58
+ .rr-progress__handler (dist/style.css) so this reads as the same control. */
59
+ .combined-timeline {
60
+ position: absolute;
61
+ left: 0;
62
+ right: 0;
63
+ bottom: 38px;
64
+ width: 80%;
65
+ margin: 0 auto;
66
+ display: flex;
67
+ align-items: center;
68
+ }
69
+
70
+ .combined-timeline-time {
71
+ flex-shrink: 0;
72
+ display: inline-block;
73
+ width: 100px;
74
+ text-align: center;
75
+ color: #11103e;
76
+ }
77
+
78
+ .combined-timeline-track {
79
+ position: relative;
80
+ flex: 1;
81
+ height: 12px;
82
+ background: #eee;
83
+ border-radius: 3px;
84
+ cursor: pointer;
85
+ box-sizing: border-box;
86
+ border-top: solid 4px #fff;
87
+ border-bottom: solid 4px #fff;
88
+ }
89
+ .combined-timeline-fill {
90
+ position: absolute;
91
+ left: 0;
92
+ top: 0;
93
+ height: 100%;
94
+ background: #e0e1fe;
95
+ }
96
+ .combined-timeline-tick {
97
+ position: absolute;
98
+ top: 2px;
99
+ width: 10px;
100
+ height: 5px;
101
+ transform: translate(-50%, -50%);
102
+ background: rgb(73, 80, 246);
103
+ pointer-events: none;
104
+ }
105
+ .combined-timeline-handle {
106
+ position: absolute;
107
+ top: 2px;
108
+ width: 20px;
109
+ height: 20px;
110
+ border-radius: 10px;
111
+ transform: translate(-50%, -50%);
112
+ background: rgb(73, 80, 246);
113
+ pointer-events: none;
114
+ }
115
+ </style>