@mechris3/glassbox 0.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.
Files changed (162) hide show
  1. package/README.md +213 -0
  2. package/dist/bin/glassbox.d.ts +14 -0
  3. package/dist/bin/glassbox.d.ts.map +1 -0
  4. package/dist/bin/glassbox.js +140 -0
  5. package/dist/bin/glassbox.js.map +1 -0
  6. package/dist/src/adapters/base-adapter.d.ts +70 -0
  7. package/dist/src/adapters/base-adapter.d.ts.map +1 -0
  8. package/dist/src/adapters/base-adapter.js +155 -0
  9. package/dist/src/adapters/base-adapter.js.map +1 -0
  10. package/dist/src/adapters/browser-adapter.interface.d.ts +41 -0
  11. package/dist/src/adapters/browser-adapter.interface.d.ts.map +1 -0
  12. package/dist/src/adapters/browser-adapter.interface.js +7 -0
  13. package/dist/src/adapters/browser-adapter.interface.js.map +1 -0
  14. package/dist/src/adapters/cdp-adapter.d.ts +63 -0
  15. package/dist/src/adapters/cdp-adapter.d.ts.map +1 -0
  16. package/dist/src/adapters/cdp-adapter.js +231 -0
  17. package/dist/src/adapters/cdp-adapter.js.map +1 -0
  18. package/dist/src/ai/collectors.d.ts +35 -0
  19. package/dist/src/ai/collectors.d.ts.map +1 -0
  20. package/dist/src/ai/collectors.js +170 -0
  21. package/dist/src/ai/collectors.js.map +1 -0
  22. package/dist/src/ai/queries.d.ts +21 -0
  23. package/dist/src/ai/queries.d.ts.map +1 -0
  24. package/dist/src/ai/queries.js +59 -0
  25. package/dist/src/ai/queries.js.map +1 -0
  26. package/dist/src/ai/types.d.ts +55 -0
  27. package/dist/src/ai/types.d.ts.map +1 -0
  28. package/dist/src/ai/types.js +6 -0
  29. package/dist/src/ai/types.js.map +1 -0
  30. package/dist/src/cdp/connection.d.ts +37 -0
  31. package/dist/src/cdp/connection.d.ts.map +1 -0
  32. package/dist/src/cdp/connection.js +86 -0
  33. package/dist/src/cdp/connection.js.map +1 -0
  34. package/dist/src/cdp/connection.test.d.ts +6 -0
  35. package/dist/src/cdp/connection.test.d.ts.map +1 -0
  36. package/dist/src/cdp/connection.test.js +93 -0
  37. package/dist/src/cdp/connection.test.js.map +1 -0
  38. package/dist/src/cdp/domains.d.ts +116 -0
  39. package/dist/src/cdp/domains.d.ts.map +1 -0
  40. package/dist/src/cdp/domains.js +153 -0
  41. package/dist/src/cdp/domains.js.map +1 -0
  42. package/dist/src/cdp/launcher.d.ts +26 -0
  43. package/dist/src/cdp/launcher.d.ts.map +1 -0
  44. package/dist/src/cdp/launcher.js +175 -0
  45. package/dist/src/cdp/launcher.js.map +1 -0
  46. package/dist/src/config.d.ts +28 -0
  47. package/dist/src/config.d.ts.map +1 -0
  48. package/dist/src/config.js +72 -0
  49. package/dist/src/config.js.map +1 -0
  50. package/dist/src/config.test.d.ts +5 -0
  51. package/dist/src/config.test.d.ts.map +1 -0
  52. package/dist/src/config.test.js +46 -0
  53. package/dist/src/config.test.js.map +1 -0
  54. package/dist/src/errors.d.ts +26 -0
  55. package/dist/src/errors.d.ts.map +1 -0
  56. package/dist/src/errors.js +27 -0
  57. package/dist/src/errors.js.map +1 -0
  58. package/dist/src/index.d.ts +18 -0
  59. package/dist/src/index.d.ts.map +1 -0
  60. package/dist/src/index.js +28 -0
  61. package/dist/src/index.js.map +1 -0
  62. package/dist/src/journey.d.ts +35 -0
  63. package/dist/src/journey.d.ts.map +1 -0
  64. package/dist/src/journey.js +39 -0
  65. package/dist/src/journey.js.map +1 -0
  66. package/dist/src/mcp/server.d.ts +22 -0
  67. package/dist/src/mcp/server.d.ts.map +1 -0
  68. package/dist/src/mcp/server.js +229 -0
  69. package/dist/src/mcp/server.js.map +1 -0
  70. package/dist/src/page-objects/base.page.d.ts +28 -0
  71. package/dist/src/page-objects/base.page.d.ts.map +1 -0
  72. package/dist/src/page-objects/base.page.js +51 -0
  73. package/dist/src/page-objects/base.page.js.map +1 -0
  74. package/dist/src/page-objects/browser-context.page.d.ts +14 -0
  75. package/dist/src/page-objects/browser-context.page.d.ts.map +1 -0
  76. package/dist/src/page-objects/browser-context.page.js +28 -0
  77. package/dist/src/page-objects/browser-context.page.js.map +1 -0
  78. package/dist/src/runner/lifecycle-hooks.d.ts +43 -0
  79. package/dist/src/runner/lifecycle-hooks.d.ts.map +1 -0
  80. package/dist/src/runner/lifecycle-hooks.js +70 -0
  81. package/dist/src/runner/lifecycle-hooks.js.map +1 -0
  82. package/dist/src/runner/run-all-journeys.d.ts +35 -0
  83. package/dist/src/runner/run-all-journeys.d.ts.map +1 -0
  84. package/dist/src/runner/run-all-journeys.js +167 -0
  85. package/dist/src/runner/run-all-journeys.js.map +1 -0
  86. package/dist/src/runner/run-journey.d.ts +17 -0
  87. package/dist/src/runner/run-journey.d.ts.map +1 -0
  88. package/dist/src/runner/run-journey.js +179 -0
  89. package/dist/src/runner/run-journey.js.map +1 -0
  90. package/dist/src/runner/spawn-journey.d.ts +36 -0
  91. package/dist/src/runner/spawn-journey.d.ts.map +1 -0
  92. package/dist/src/runner/spawn-journey.js +40 -0
  93. package/dist/src/runner/spawn-journey.js.map +1 -0
  94. package/dist/src/server/diagnostic-service.d.ts +69 -0
  95. package/dist/src/server/diagnostic-service.d.ts.map +1 -0
  96. package/dist/src/server/diagnostic-service.js +165 -0
  97. package/dist/src/server/diagnostic-service.js.map +1 -0
  98. package/dist/src/server/diagnostic-service.test.d.ts +5 -0
  99. package/dist/src/server/diagnostic-service.test.d.ts.map +1 -0
  100. package/dist/src/server/diagnostic-service.test.js +82 -0
  101. package/dist/src/server/diagnostic-service.test.js.map +1 -0
  102. package/dist/src/server/index.d.ts +11 -0
  103. package/dist/src/server/index.d.ts.map +1 -0
  104. package/dist/src/server/index.js +167 -0
  105. package/dist/src/server/index.js.map +1 -0
  106. package/dist/src/server/journey-discovery.d.ts +16 -0
  107. package/dist/src/server/journey-discovery.d.ts.map +1 -0
  108. package/dist/src/server/journey-discovery.js +38 -0
  109. package/dist/src/server/journey-discovery.js.map +1 -0
  110. package/dist/src/server/settings-storage.d.ts +27 -0
  111. package/dist/src/server/settings-storage.d.ts.map +1 -0
  112. package/dist/src/server/settings-storage.js +27 -0
  113. package/dist/src/server/settings-storage.js.map +1 -0
  114. package/dist/src/server/test-executor.d.ts +64 -0
  115. package/dist/src/server/test-executor.d.ts.map +1 -0
  116. package/dist/src/server/test-executor.js +279 -0
  117. package/dist/src/server/test-executor.js.map +1 -0
  118. package/dist/src/server/websocket.d.ts +51 -0
  119. package/dist/src/server/websocket.d.ts.map +1 -0
  120. package/dist/src/server/websocket.js +28 -0
  121. package/dist/src/server/websocket.js.map +1 -0
  122. package/dist/src/utils/browser-discovery.d.ts +11 -0
  123. package/dist/src/utils/browser-discovery.d.ts.map +1 -0
  124. package/dist/src/utils/browser-discovery.js +102 -0
  125. package/dist/src/utils/browser-discovery.js.map +1 -0
  126. package/dist/src/utils/error-formatter.d.ts +13 -0
  127. package/dist/src/utils/error-formatter.d.ts.map +1 -0
  128. package/dist/src/utils/error-formatter.js +29 -0
  129. package/dist/src/utils/error-formatter.js.map +1 -0
  130. package/dist/src/utils/escape.d.ts +10 -0
  131. package/dist/src/utils/escape.d.ts.map +1 -0
  132. package/dist/src/utils/escape.js +22 -0
  133. package/dist/src/utils/escape.js.map +1 -0
  134. package/dist/src/utils/escape.test.d.ts +5 -0
  135. package/dist/src/utils/escape.test.d.ts.map +1 -0
  136. package/dist/src/utils/escape.test.js +49 -0
  137. package/dist/src/utils/escape.test.js.map +1 -0
  138. package/dist/src/utils/page-object-error.d.ts +5 -0
  139. package/dist/src/utils/page-object-error.d.ts.map +1 -0
  140. package/dist/src/utils/page-object-error.js +22 -0
  141. package/dist/src/utils/page-object-error.js.map +1 -0
  142. package/dist/src/utils/profile-discovery.d.ts +20 -0
  143. package/dist/src/utils/profile-discovery.d.ts.map +1 -0
  144. package/dist/src/utils/profile-discovery.js +68 -0
  145. package/dist/src/utils/profile-discovery.js.map +1 -0
  146. package/dist/src/utils/wait-utils.d.ts +10 -0
  147. package/dist/src/utils/wait-utils.d.ts.map +1 -0
  148. package/dist/src/utils/wait-utils.js +20 -0
  149. package/dist/src/utils/wait-utils.js.map +1 -0
  150. package/dist/src/utils/wait-utils.test.d.ts +5 -0
  151. package/dist/src/utils/wait-utils.test.d.ts.map +1 -0
  152. package/dist/src/utils/wait-utils.test.js +30 -0
  153. package/dist/src/utils/wait-utils.test.js.map +1 -0
  154. package/package.json +60 -0
  155. package/src/server/static/index.html +541 -0
  156. package/ui/dist/3rdpartylicenses.txt +419 -0
  157. package/ui/dist/browser/assets/logo.svg +29 -0
  158. package/ui/dist/browser/index.html +13 -0
  159. package/ui/dist/browser/main-7OJWN7GZ.js +16 -0
  160. package/ui/dist/browser/polyfills-5CFQRCPP.js +2 -0
  161. package/ui/dist/browser/styles-HMZYQRBV.css +1 -0
  162. package/ui/dist/prerendered-routes.json +3 -0
@@ -0,0 +1,541 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Glassbox</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <style>
8
+ :root, :root[data-theme="dark"] {
9
+ --bg-deep: #0d1117;
10
+ --bg-panel: #151b23;
11
+ --bg-surface: #1c2128;
12
+ --bg-elevated: #252c35;
13
+ --accent-primary: #f0b832;
14
+ --accent-hover: #f5c94a;
15
+ --text-primary: #f0f3f6;
16
+ --text-secondary: #c9d1d9;
17
+ --text-muted: #8b949e;
18
+ --color-success: #3fb950;
19
+ --color-danger: #f85149;
20
+ --color-breakpoint: #f85149;
21
+ --border-color: #3d444d;
22
+ --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
23
+ --font-mono: 'SF Mono', 'Fira Code', Consolas, monospace;
24
+ }
25
+
26
+ * { box-sizing: border-box; margin: 0; padding: 0; }
27
+ body {
28
+ font-family: var(--font-sans);
29
+ font-size: 14px;
30
+ background: var(--bg-deep);
31
+ color: var(--text-primary);
32
+ height: 100vh;
33
+ display: grid;
34
+ grid-template-rows: auto auto 1fr auto;
35
+ overflow: hidden;
36
+ }
37
+
38
+ header {
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: space-between;
42
+ padding: 8px 16px;
43
+ background: var(--bg-panel);
44
+ border-bottom: 1px solid var(--border-color);
45
+ }
46
+ .logo { font-weight: 700; font-size: 16px; color: var(--accent-primary); }
47
+ .connection { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted); }
48
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-danger); }
49
+ .dot.connected { background: var(--color-success); }
50
+
51
+ main {
52
+ display: grid;
53
+ grid-template-columns: 240px 1fr;
54
+ overflow: hidden;
55
+ min-height: 0;
56
+ }
57
+
58
+ .sidebar {
59
+ background: var(--bg-panel);
60
+ border-right: 1px solid var(--border-color);
61
+ overflow-y: auto;
62
+ padding: 12px 0;
63
+ }
64
+ .sidebar h3 {
65
+ font-size: 11px;
66
+ text-transform: uppercase;
67
+ letter-spacing: 0.5px;
68
+ color: var(--text-muted);
69
+ padding: 0 12px 8px;
70
+ }
71
+ .content {
72
+ display: flex;
73
+ flex-direction: column;
74
+ min-height: 0;
75
+ overflow: hidden;
76
+ }
77
+ .journey-item {
78
+ padding: 6px 12px;
79
+ cursor: pointer;
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 8px;
83
+ font-size: 13px;
84
+ }
85
+ .journey-item:hover { background: var(--bg-surface); }
86
+ .journey-item input { accent-color: var(--accent-primary); }
87
+
88
+ .content {
89
+ display: flex;
90
+ flex-direction: column;
91
+ overflow: hidden;
92
+ }
93
+
94
+ .toolbar {
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 4px;
98
+ padding: 8px 16px;
99
+ background: var(--bg-panel);
100
+ border-bottom: 1px solid var(--border-color);
101
+ }
102
+ .toolbar button {
103
+ width: 32px; height: 32px;
104
+ border: none; background: none;
105
+ border-radius: 4px;
106
+ font-size: 14px;
107
+ cursor: pointer;
108
+ color: var(--text-primary);
109
+ display: flex; align-items: center; justify-content: center;
110
+ }
111
+ .toolbar button:hover:not(:disabled) { background: var(--bg-elevated); }
112
+ .toolbar button:disabled { opacity: 0.3; cursor: not-allowed; }
113
+ .toolbar button.play { color: var(--color-success); }
114
+ .toolbar button.stop { color: var(--color-danger); }
115
+ .toolbar .spacer { flex: 1; }
116
+ .toolbar .status { font-size: 12px; color: var(--text-muted); font-family: var(--font-mono); }
117
+
118
+ .tabs {
119
+ display: flex; gap: 4px;
120
+ padding: 4px 16px;
121
+ border-bottom: 1px solid var(--border-color);
122
+ background: var(--bg-panel);
123
+ }
124
+ .tabs button {
125
+ padding: 4px 12px;
126
+ border: none; background: none;
127
+ border-radius: 4px 4px 0 0;
128
+ color: var(--text-secondary);
129
+ cursor: pointer; font-size: 13px;
130
+ }
131
+ .tabs button.active { color: var(--accent-primary); border-bottom: 2px solid var(--accent-primary); }
132
+
133
+ .log-panel {
134
+ flex: 1;
135
+ overflow-y: auto;
136
+ padding: 8px 16px;
137
+ font-family: var(--font-mono);
138
+ font-size: 12px;
139
+ line-height: 1.7;
140
+ }
141
+ .action-line { display: flex; gap: 8px; padding: 2px 0; align-items: center; }
142
+ .action-line .idx { color: var(--text-muted); min-width: 24px; text-align: right; }
143
+ .action-line .desc { flex: 1; }
144
+ .action-line .icon { width: 16px; text-align: center; }
145
+ .action-line.running { color: var(--accent-primary); }
146
+ .action-line.complete { color: var(--text-secondary); }
147
+ .action-line.failed { color: var(--color-danger); }
148
+ .action-line.breakpoint { color: var(--color-breakpoint); }
149
+ .action-line.paused-at { background: rgba(248, 81, 73, 0.08); border-left: 2px solid var(--color-breakpoint); padding-left: 6px; }
150
+
151
+ .bp-pin {
152
+ width: 10px; height: 10px;
153
+ border-radius: 50%;
154
+ border: 1.5px solid var(--border-color);
155
+ background: none;
156
+ cursor: pointer;
157
+ flex-shrink: 0;
158
+ transition: background 0.1s, border-color 0.1s;
159
+ }
160
+ .bp-pin:hover { border-color: var(--color-breakpoint); }
161
+ .bp-pin.active { background: var(--color-breakpoint); border-color: var(--color-breakpoint); }
162
+
163
+ .console-line { padding: 1px 0; white-space: pre-wrap; word-break: break-all; }
164
+ .console-line.error { color: var(--color-danger); }
165
+ .console-line.separator { color: var(--text-muted); text-align: center; padding: 8px 0; }
166
+
167
+ footer {
168
+ display: flex; align-items: center; justify-content: space-between;
169
+ padding: 4px 16px;
170
+ background: var(--bg-panel);
171
+ border-top: 1px solid var(--border-color);
172
+ font-size: 12px;
173
+ }
174
+ .results { display: flex; gap: 12px; font-family: var(--font-mono); }
175
+ .results .pass { color: var(--color-success); }
176
+ .results .fail { color: var(--color-danger); }
177
+ </style>
178
+ </head>
179
+ <body>
180
+ <header>
181
+ <span class="logo">Glassbox</span>
182
+ <div class="connection">
183
+ <button id="btn-settings" title="Settings" style="background:none;border:none;cursor:pointer;font-size:16px;margin-right:8px;">⚙️</button>
184
+ <span class="dot" id="conn-dot"></span>
185
+ <span id="conn-text">Disconnected</span>
186
+ </div>
187
+ </header>
188
+
189
+ <aside id="settings-panel" style="max-height:0;overflow:hidden;padding:0 16px;background:var(--bg-surface);border-bottom:1px solid var(--border-color);font-size:12px;transition:max-height 0.15s,padding 0.15s;">
190
+ <div style="display:flex;gap:16px;align-items:center;">
191
+ <label style="display:flex;align-items:center;gap:6px;">
192
+ <span style="color:var(--text-muted);">URL</span>
193
+ <input type="text" id="target-url" value="http://localhost:3000" style="width:200px;padding:3px 6px;background:var(--bg-panel);border:1px solid var(--border-color);border-radius:3px;color:var(--text-primary);font-size:12px;font-family:var(--font-mono);" />
194
+ </label>
195
+ <label style="display:flex;align-items:center;gap:6px;">
196
+ <span style="color:var(--text-muted);">Viewport</span>
197
+ <input type="number" id="vp-width" value="1280" style="width:52px;padding:3px 4px;background:var(--bg-panel);border:1px solid var(--border-color);border-radius:3px;color:var(--text-primary);font-size:12px;" />
198
+ <span style="color:var(--text-muted);">×</span>
199
+ <input type="number" id="vp-height" value="720" style="width:52px;padding:3px 4px;background:var(--bg-panel);border:1px solid var(--border-color);border-radius:3px;color:var(--text-primary);font-size:12px;" />
200
+ </label>
201
+ <label style="display:flex;align-items:center;gap:4px;cursor:pointer;color:var(--text-secondary);">
202
+ <input type="checkbox" id="chk-devtools" /> DevTools
203
+ </label>
204
+ <label style="display:flex;align-items:center;gap:4px;cursor:pointer;color:var(--text-secondary);">
205
+ <input type="checkbox" id="chk-headed" checked /> Headed
206
+ </label>
207
+ <label style="display:flex;align-items:center;gap:4px;cursor:pointer;color:var(--text-secondary);">
208
+ <input type="checkbox" id="chk-keep-open" /> Keep open
209
+ </label>
210
+ </div>
211
+ </aside>
212
+
213
+ <main>
214
+ <div class="sidebar">
215
+ <h3>Journeys</h3>
216
+ <div id="journey-list"></div>
217
+ </div>
218
+ <div class="content">
219
+ <div class="toolbar">
220
+ <button class="play" id="btn-play" title="Run">▶</button>
221
+ <button id="btn-pause" title="Pause" disabled>⏸</button>
222
+ <button id="btn-resume" title="Resume" disabled>⏵</button>
223
+ <button id="btn-step" title="Step" disabled>⏭</button>
224
+ <button class="stop" id="btn-stop" title="Stop" disabled>⏹</button>
225
+ <span class="spacer"></span>
226
+ <label style="font-size:12px;color:var(--text-muted);display:flex;align-items:center;gap:6px;">
227
+ Delay
228
+ <input type="range" id="delay-slider" min="0" max="5000" value="0" style="width:100px;accent-color:var(--accent-primary);" />
229
+ <span id="delay-value" style="font-family:var(--font-mono);font-size:11px;color:var(--text-secondary);min-width:42px;">0ms</span>
230
+ </label>
231
+ <span class="status" id="fsm-state">idle</span>
232
+ </div>
233
+ <div class="tabs">
234
+ <button class="active" id="tab-actions">Actions</button>
235
+ <button id="tab-console">Console</button>
236
+ </div>
237
+ <div class="log-panel" id="actions-panel"></div>
238
+ <div class="log-panel" id="console-panel" style="display:none"></div>
239
+ </div>
240
+ </main>
241
+
242
+ <footer>
243
+ <span id="footer-info"></span>
244
+ <div class="results" id="results"></div>
245
+ </footer>
246
+
247
+ <script>
248
+ // State
249
+ let journeys = [];
250
+ let selectedIds = new Set();
251
+ let fsmState = 'idle';
252
+ let ws = null;
253
+ let passed = 0, failed = 0;
254
+
255
+ // Elements
256
+ const journeyList = document.getElementById('journey-list');
257
+ const actionsPanel = document.getElementById('actions-panel');
258
+ const consolePanel = document.getElementById('console-panel');
259
+ const connDot = document.getElementById('conn-dot');
260
+ const connText = document.getElementById('conn-text');
261
+ const fsmLabel = document.getElementById('fsm-state');
262
+ const resultsEl = document.getElementById('results');
263
+ const btnPlay = document.getElementById('btn-play');
264
+ const btnPause = document.getElementById('btn-pause');
265
+ const btnResume = document.getElementById('btn-resume');
266
+ const btnStep = document.getElementById('btn-step');
267
+ const btnStop = document.getElementById('btn-stop');
268
+ const tabActions = document.getElementById('tab-actions');
269
+ const tabConsole = document.getElementById('tab-console');
270
+
271
+ // Tabs
272
+ tabActions.onclick = () => { tabActions.classList.add('active'); tabConsole.classList.remove('active'); actionsPanel.style.display=''; consolePanel.style.display='none'; };
273
+ tabConsole.onclick = () => { tabConsole.classList.add('active'); tabActions.classList.remove('active'); consolePanel.style.display=''; actionsPanel.style.display='none'; };
274
+
275
+ // --- Settings persistence ---
276
+ const settingsFields = {
277
+ 'target-url': 'targetUrl',
278
+ 'vp-width': 'viewportWidth',
279
+ 'vp-height': 'viewportHeight',
280
+ 'chk-devtools': 'devtools',
281
+ 'chk-headed': 'headed',
282
+ 'chk-keep-open': 'keepBrowserOpen',
283
+ 'delay-slider': 'actionDelay',
284
+ };
285
+
286
+ /** Load saved settings from the server and apply to UI. */
287
+ async function loadSettings() {
288
+ try {
289
+ const res = await fetch('/api/settings');
290
+ const saved = await res.json();
291
+ if (saved.targetUrl) document.getElementById('target-url').value = saved.targetUrl;
292
+ if (saved.viewportWidth) document.getElementById('vp-width').value = saved.viewportWidth;
293
+ if (saved.viewportHeight) document.getElementById('vp-height').value = saved.viewportHeight;
294
+ if (saved.devtools !== undefined) document.getElementById('chk-devtools').checked = saved.devtools;
295
+ if (saved.headed !== undefined) document.getElementById('chk-headed').checked = saved.headed;
296
+ if (saved.keepBrowserOpen !== undefined) document.getElementById('chk-keep-open').checked = saved.keepBrowserOpen;
297
+ if (saved.actionDelay !== undefined) {
298
+ document.getElementById('delay-slider').value = saved.actionDelay;
299
+ delayValue.textContent = saved.actionDelay + 'ms';
300
+ }
301
+ if (saved.settingsPanelOpen) {
302
+ settingsPanel.style.maxHeight = '50px';
303
+ settingsPanel.style.padding = '8px 16px';
304
+ }
305
+ } catch { /* No saved settings yet, use defaults */ }
306
+ }
307
+
308
+ /** Save current UI settings to the server. Debounced. */
309
+ let saveTimeout = null;
310
+ function saveSettings() {
311
+ clearTimeout(saveTimeout);
312
+ saveTimeout = setTimeout(() => {
313
+ const settings = {
314
+ targetUrl: document.getElementById('target-url').value,
315
+ viewportWidth: parseInt(document.getElementById('vp-width').value, 10),
316
+ viewportHeight: parseInt(document.getElementById('vp-height').value, 10),
317
+ devtools: document.getElementById('chk-devtools').checked,
318
+ headed: document.getElementById('chk-headed').checked,
319
+ keepBrowserOpen: document.getElementById('chk-keep-open').checked,
320
+ actionDelay: parseInt(document.getElementById('delay-slider').value, 10),
321
+ settingsPanelOpen: settingsPanel.style.maxHeight !== '0px' && settingsPanel.style.maxHeight !== '0',
322
+ };
323
+ fetch('/api/settings', {
324
+ method: 'PUT',
325
+ headers: { 'Content-Type': 'application/json' },
326
+ body: JSON.stringify(settings),
327
+ });
328
+ }, 500);
329
+ }
330
+
331
+ // Auto-save on any settings change
332
+ Object.keys(settingsFields).forEach(id => {
333
+ const el = document.getElementById(id);
334
+ el.addEventListener('input', saveSettings);
335
+ el.addEventListener('change', saveSettings);
336
+ });
337
+
338
+ loadSettings();
339
+
340
+ // Load journeys
341
+ fetch('/api/journeys').then(r => r.json()).then(data => {
342
+ journeys = data;
343
+ selectedIds = new Set(data.map(j => j.id));
344
+ renderJourneys();
345
+ });
346
+
347
+ function renderJourneys() {
348
+ journeyList.innerHTML = journeys.map(j => `
349
+ <label class="journey-item">
350
+ <input type="checkbox" ${selectedIds.has(j.id) ? 'checked' : ''} data-id="${j.id}" />
351
+ ${j.name}
352
+ </label>
353
+ `).join('');
354
+ journeyList.querySelectorAll('input').forEach(el => {
355
+ el.onchange = () => {
356
+ if (el.checked) selectedIds.add(el.dataset.id);
357
+ else selectedIds.delete(el.dataset.id);
358
+ };
359
+ });
360
+ }
361
+
362
+ // WebSocket
363
+ function connect() {
364
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
365
+ ws = new WebSocket(`${proto}//${location.host}`);
366
+ ws.onopen = () => { connDot.classList.add('connected'); connText.textContent = 'Connected'; };
367
+ ws.onclose = () => { connDot.classList.remove('connected'); connText.textContent = 'Disconnected'; setTimeout(connect, 3000); };
368
+ ws.onmessage = (e) => handleMessage(JSON.parse(e.data));
369
+ }
370
+ connect();
371
+
372
+ function handleMessage(msg) {
373
+ switch (msg.type) {
374
+ case 'action':
375
+ renderAction(msg);
376
+ if (msg.status === 'breakpoint') setState('paused');
377
+ break;
378
+ case 'test-start':
379
+ appendConsole(`── ${msg.journey} ──`, 'separator');
380
+ break;
381
+ case 'test-end':
382
+ if (msg.status === 'passed') passed++;
383
+ else failed++;
384
+ renderResults();
385
+ setState(msg.status === 'failed' ? 'errored' : 'idle');
386
+ break;
387
+ case 'run-end':
388
+ setState('idle');
389
+ break;
390
+ case 'log':
391
+ appendConsole(msg.message, 'log');
392
+ break;
393
+ case 'error':
394
+ appendConsole(msg.message, 'error');
395
+ break;
396
+ case 'ui-control':
397
+ if (msg.action === 'pause') setState('paused');
398
+ else if (msg.action === 'resume') setState('running');
399
+ break;
400
+ }
401
+ }
402
+
403
+ function renderAction(msg) {
404
+ const existing = document.getElementById(`action-${msg.index}`);
405
+ if (existing) {
406
+ existing.className = `action-line ${mapStatus(msg.status)}`;
407
+ existing.querySelector('.icon').textContent = statusIcon(msg.status);
408
+ // Highlight paused action
409
+ if (msg.status === 'breakpoint') {
410
+ document.querySelectorAll('.action-line.paused-at').forEach(el => el.classList.remove('paused-at'));
411
+ existing.classList.add('paused-at');
412
+ }
413
+ return;
414
+ }
415
+ const div = document.createElement('div');
416
+ div.id = `action-${msg.index}`;
417
+ div.className = `action-line ${mapStatus(msg.status)}`;
418
+ div.innerHTML = `<button class="bp-pin" data-index="${msg.index}" title="Toggle breakpoint"></button><span class="idx">${msg.index}</span><span class="desc">${msg.description || ''}</span><span class="icon">${statusIcon(msg.status)}</span>`;
419
+ // Breakpoint pin click handler
420
+ div.querySelector('.bp-pin').onclick = (e) => {
421
+ e.stopPropagation();
422
+ toggleBreakpoint(msg.index, div.querySelector('.bp-pin'));
423
+ };
424
+ if (msg.status === 'breakpoint') {
425
+ document.querySelectorAll('.action-line.paused-at').forEach(el => el.classList.remove('paused-at'));
426
+ div.classList.add('paused-at');
427
+ }
428
+ actionsPanel.appendChild(div);
429
+ actionsPanel.scrollTop = actionsPanel.scrollHeight;
430
+ }
431
+
432
+ function mapStatus(s) { return s === 'start' ? 'running' : s; }
433
+ function statusIcon(s) {
434
+ if (s === 'start') return '⏳';
435
+ if (s === 'complete') return '✓';
436
+ if (s === 'breakpoint') return '⏸';
437
+ return '';
438
+ }
439
+
440
+ // Breakpoints
441
+ let breakpoints = new Set();
442
+
443
+ function toggleBreakpoint(index, pinEl) {
444
+ if (breakpoints.has(index)) {
445
+ breakpoints.delete(index);
446
+ pinEl.classList.remove('active');
447
+ } else {
448
+ breakpoints.add(index);
449
+ pinEl.classList.add('active');
450
+ }
451
+ // Send to server for the current/next run
452
+ const currentJourney = journeys.find(j => selectedIds.has(j.id));
453
+ if (currentJourney) {
454
+ fetch('/api/breakpoints', {
455
+ method: 'POST',
456
+ headers: { 'Content-Type': 'application/json' },
457
+ body: JSON.stringify({ journey: currentJourney.path, breakpoints: [...breakpoints] })
458
+ });
459
+ }
460
+ }
461
+
462
+ function appendConsole(text, type) {
463
+ const div = document.createElement('div');
464
+ div.className = `console-line ${type}`;
465
+ div.textContent = text;
466
+ consolePanel.appendChild(div);
467
+ consolePanel.scrollTop = consolePanel.scrollHeight;
468
+ }
469
+
470
+ function renderResults() {
471
+ resultsEl.innerHTML = `<span class="pass">✓ ${passed}</span><span class="fail">✗ ${failed}</span>`;
472
+ }
473
+
474
+ // FSM
475
+ function setState(state) {
476
+ fsmState = state;
477
+ fsmLabel.textContent = state;
478
+ btnPlay.disabled = !(state === 'idle' || state === 'completed' || state === 'errored');
479
+ btnPause.disabled = state !== 'running';
480
+ btnResume.disabled = state !== 'paused';
481
+ btnStep.disabled = state !== 'paused';
482
+ btnStop.disabled = !(state === 'running' || state === 'paused');
483
+ }
484
+
485
+ // Controls
486
+ const settingsPanel = document.getElementById('settings-panel');
487
+ const btnSettings = document.getElementById('btn-settings');
488
+ const delaySlider = document.getElementById('delay-slider');
489
+ const delayValue = document.getElementById('delay-value');
490
+
491
+ btnSettings.onclick = () => {
492
+ const open = settingsPanel.style.maxHeight === '0px' || settingsPanel.style.maxHeight === '0';
493
+ settingsPanel.style.maxHeight = open ? '50px' : '0';
494
+ settingsPanel.style.padding = open ? '8px 16px' : '0 16px';
495
+ saveSettings();
496
+ };
497
+
498
+ // Delay slider — updates the running process in real-time
499
+ delaySlider.oninput = () => {
500
+ const ms = parseInt(delaySlider.value, 10);
501
+ delayValue.textContent = ms + 'ms';
502
+ fetch('/api/config', {
503
+ method: 'PATCH',
504
+ headers: { 'Content-Type': 'application/json' },
505
+ body: JSON.stringify({ actionDelay: ms })
506
+ });
507
+ };
508
+
509
+ /** Collect current settings from the UI into a config object. */
510
+ function getRunConfig() {
511
+ return {
512
+ targetUrl: document.getElementById('target-url').value || 'http://localhost:3000',
513
+ actionDelay: parseInt(delaySlider.value, 10),
514
+ headless: !document.getElementById('chk-headed').checked,
515
+ devtools: document.getElementById('chk-devtools').checked,
516
+ keepBrowserOpen: document.getElementById('chk-keep-open').checked,
517
+ viewportWidth: parseInt(document.getElementById('vp-width').value, 10) || 1280,
518
+ viewportHeight: parseInt(document.getElementById('vp-height').value, 10) || 720,
519
+ };
520
+ }
521
+
522
+ btnPlay.onclick = () => {
523
+ actionsPanel.innerHTML = '';
524
+ consolePanel.innerHTML = '';
525
+ passed = 0; failed = 0; renderResults();
526
+ const selected = journeys.filter(j => selectedIds.has(j.id));
527
+ const config = getRunConfig();
528
+ fetch('/api/tests/run', {
529
+ method: 'POST',
530
+ headers: { 'Content-Type': 'application/json' },
531
+ body: JSON.stringify({ journeys: selected.map(j => j.path), config })
532
+ });
533
+ setState('running');
534
+ };
535
+ btnPause.onclick = () => { fetch('/api/tests/pause', { method: 'POST' }); setState('paused'); };
536
+ btnResume.onclick = () => { fetch('/api/tests/resume', { method: 'POST' }); setState('running'); };
537
+ btnStep.onclick = () => { fetch('/api/tests/step', { method: 'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({count:1}) }); };
538
+ btnStop.onclick = () => { fetch('/api/tests/stop', { method: 'POST' }); setState('idle'); };
539
+ </script>
540
+ </body>
541
+ </html>