lexxit-automation-framework 2.0.20 → 3.0.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 (84) hide show
  1. package/README.md +246 -57
  2. package/dist-obf/actions/baseHandler.js +1 -0
  3. package/dist-obf/actions/browserManager.js +1 -0
  4. package/dist-obf/actions/checkboxHandler.js +1 -0
  5. package/dist-obf/actions/clickHandler.js +1 -0
  6. package/dist-obf/actions/customcodehandler.js +1 -0
  7. package/dist-obf/actions/dropdownHandler.js +1 -0
  8. package/dist-obf/actions/radiobuttonHandler.js +1 -0
  9. package/dist-obf/actions/textHandler.js +1 -0
  10. package/dist-obf/api/server.js +1 -0
  11. package/dist-obf/config.js +1 -0
  12. package/dist-obf/executor/functionMap.js +1 -0
  13. package/dist-obf/executor/mapping.js +1 -0
  14. package/dist-obf/executor/scriptExecutor.js +1 -0
  15. package/dist-obf/executor/stepExecutor.js +1 -0
  16. package/dist-obf/runner/testRunner.js +1 -0
  17. package/dist-obf/sse/sseManager.js +1 -0
  18. package/dist-obf/store/executionStore.js +1 -0
  19. package/dist-obf/store/testDataStore.js +1 -0
  20. package/dist-obf/types/types.js +1 -1
  21. package/dist-obf/utils/healingService.js +1 -0
  22. package/dist-obf/utils/locatorService.js +1 -0
  23. package/dist-obf/utils/logger.js +1 -1
  24. package/dist-obf/utils/metadataService.js +1 -0
  25. package/dist-obf/utils/waitConditions.js +1 -0
  26. package/dist-obf/validator/validator.js +1 -0
  27. package/npmignore +8 -0
  28. package/package.json +32 -1
  29. package/public/dashboard.html +591 -0
  30. package/src/actions/baseHandler.ts +351 -0
  31. package/src/actions/browserManager.ts +432 -0
  32. package/src/actions/checkboxHandler.ts +276 -0
  33. package/src/actions/clickHandler.ts +513 -0
  34. package/src/actions/customcodehandler.ts +251 -0
  35. package/src/actions/dropdownHandler.ts +501 -0
  36. package/src/actions/radiobuttonHandler.ts +286 -0
  37. package/src/actions/textHandler.ts +498 -0
  38. package/src/api/server.ts +210 -0
  39. package/src/config.ts +7 -0
  40. package/src/executor/functionMap.ts +153 -0
  41. package/src/executor/mapping.ts +70 -0
  42. package/src/executor/scriptExecutor.ts +162 -0
  43. package/src/executor/stepExecutor.ts +289 -0
  44. package/src/runner/testRunner.ts +78 -0
  45. package/src/sse/sseManager.ts +130 -0
  46. package/src/store/executionStore.ts +152 -0
  47. package/src/store/testDataStore.ts +46 -0
  48. package/src/types/types.ts +159 -0
  49. package/src/utils/healingService.ts +210 -0
  50. package/src/utils/locatorService.ts +73 -0
  51. package/src/utils/logger.ts +27 -0
  52. package/src/utils/metadataService.ts +137 -0
  53. package/src/utils/waitConditions.ts +141 -0
  54. package/src/validator/validator.ts +140 -0
  55. package/tsconfig.json +16 -0
  56. package/bin/lexxit-automation-framework.js +0 -224
  57. package/dist-obf/app.js +0 -1
  58. package/dist-obf/controllers/controller.js +0 -1
  59. package/dist-obf/core/BrowserManager.js +0 -1
  60. package/dist-obf/core/PlaywrightEngine.js +0 -1
  61. package/dist-obf/core/ScreenshotManager.js +0 -1
  62. package/dist-obf/core/TestData.js +0 -1
  63. package/dist-obf/core/TestExecutor.js +0 -1
  64. package/dist-obf/core/handlers/AllHandlers.js +0 -1
  65. package/dist-obf/core/handlers/BaseHandler.js +0 -1
  66. package/dist-obf/core/handlers/ClickHandler.js +0 -1
  67. package/dist-obf/core/handlers/CustomCodeHandler.js +0 -1
  68. package/dist-obf/core/handlers/DropdownHandler.js +0 -1
  69. package/dist-obf/core/handlers/InputHandler.js +0 -1
  70. package/dist-obf/core/registry/ActionRegistry.js +0 -1
  71. package/dist-obf/installer/frameworkLauncher.js +0 -1
  72. package/dist-obf/public/dashboard.html +0 -591
  73. package/dist-obf/public/execution.html +0 -510
  74. package/dist-obf/public/queue-monitor.html +0 -408
  75. package/dist-obf/queue/ExecutionQueue.js +0 -1
  76. package/dist-obf/routes/api.routes.js +0 -1
  77. package/dist-obf/server.js +0 -1
  78. package/dist-obf/utils/chromefinder.js +0 -1
  79. package/dist-obf/utils/elementHighlight.js +0 -1
  80. package/dist-obf/utils/locatorHelper.js +0 -1
  81. package/dist-obf/utils/response.js +0 -1
  82. package/dist-obf/utils/responseFormatter.js +0 -1
  83. package/dist-obf/utils/sseManager.js +0 -1
  84. package/scripts/postinstall.js +0 -52
@@ -1,591 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>PW — Execution Dashboard</title>
7
- <style>
8
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
9
- :root{
10
- --bg:#0a0d14;--surface:#111827;--s2:#1a2236;
11
- --b:#1e2d45;--b2:#2d3f5e;
12
- --t:#e2e8f0;--m:#64748b;--m2:#94a3b8;
13
- --pass:#22c55e;--pb:#052e16;--pbd:#14532d;
14
- --fail:#ef4444;--fb:#2d0a0a;--fbd:#7f1d1d;
15
- --skip:#f59e0b;--sb:#2d1d00;--sbd:#78350f;
16
- --run:#3b82f6;--rb:#0c1f3d;--rbd:#1e3a6e;
17
- --cancel:#a855f7;--cnb:#1a0a2e;--cnbd:#4a1d7e;
18
- --r:10px;--rs:6px;
19
- }
20
- html,body{height:100%;background:var(--bg);color:var(--t);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
21
-
22
- header{background:var(--surface);border-bottom:1px solid var(--b);padding:12px 24px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100}
23
- .h-left{display:flex;align-items:center;gap:12px}
24
- .h-logo{font-size:1.4rem}
25
- .h-title{font-size:.95rem;font-weight:700}
26
- .h-sub{font-size:.68rem;color:var(--m);font-family:monospace;margin-top:2px}
27
- .h-right{display:flex;align-items:center;gap:10px}
28
-
29
- .badge{display:inline-flex;align-items:center;gap:6px;padding:5px 12px;border-radius:99px;font-size:.72rem;font-weight:700;letter-spacing:.05em;border:1px solid transparent}
30
- .badge .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
31
- .badge-idle {background:#1a1a2e;color:var(--m);border-color:var(--b)}
32
- .badge-idle .dot{background:var(--m);animation:none}
33
- .badge-running{background:var(--rb);color:var(--run);border-color:var(--rbd)}
34
- .badge-running .dot{background:var(--run);animation:pulse .9s ease-in-out infinite}
35
- .badge-pass {background:var(--pb);color:var(--pass);border-color:var(--pbd)}
36
- .badge-pass .dot{background:var(--pass);animation:none}
37
- .badge-fail {background:var(--fb);color:var(--fail);border-color:var(--fbd)}
38
- .badge-fail .dot{background:var(--fail);animation:none}
39
- .badge-cancel {background:var(--cnb);color:var(--cancel);border-color:var(--cnbd)}
40
- .badge-cancel .dot{background:var(--cancel);animation:none}
41
- @keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.8)}}
42
-
43
- .btn-icon{display:inline-flex;align-items:center;gap:6px;padding:5px 12px;border-radius:99px;font-size:.72rem;font-weight:600;border:1px solid;cursor:pointer;transition:all .2s;text-decoration:none;background:none}
44
- .btn-cancel{color:var(--fail);border-color:var(--fbd);background:var(--fb)}
45
- .btn-cancel:hover{background:#3d0a0a;border-color:var(--fail)}
46
- .btn-cancel:disabled{opacity:.4;cursor:not-allowed}
47
- .btn-newwin{color:var(--m2);border-color:var(--b);background:var(--s2)}
48
- .btn-newwin:hover{color:var(--t);border-color:var(--b2)}
49
-
50
- .layout{display:grid;grid-template-columns:1fr 300px;height:calc(100vh - 53px);overflow:hidden}
51
- .main{overflow-y:auto;padding:18px 22px;display:flex;flex-direction:column;gap:14px}
52
- .sidebar{background:var(--surface);border-left:1px solid var(--b);display:flex;flex-direction:column;overflow:hidden}
53
-
54
- .prog-labels{display:flex;justify-content:space-between;font-size:.7rem;color:var(--m);margin-bottom:5px}
55
- .prog-track{background:var(--s2);border-radius:99px;height:5px;border:1px solid var(--b);overflow:hidden}
56
- .prog-fill{height:100%;border-radius:99px;background:linear-gradient(90deg,#4f46e5,var(--run));width:0%;transition:width .5s cubic-bezier(.4,0,.2,1)}
57
- .prog-fill.done{background:linear-gradient(90deg,#14532d,var(--pass))}
58
- .prog-fill.fail{background:linear-gradient(90deg,#7f1d1d,var(--fail))}
59
- .prog-fill.cancelled{background:linear-gradient(90deg,#3b0764,var(--cancel))}
60
-
61
- .stats{display:grid;grid-template-columns:repeat(5,1fr);gap:10px}
62
- .stat{background:var(--s2);border:1px solid var(--b);border-radius:var(--r);padding:12px;text-align:center}
63
- .stat-v{font-size:1.7rem;font-weight:800;line-height:1}
64
- .stat-l{font-size:.6rem;color:var(--m);margin-top:4px;text-transform:uppercase;letter-spacing:.1em}
65
- .stat.total .stat-v{color:var(--t)}
66
- .stat.pass .stat-v{color:var(--pass)}
67
- .stat.fail .stat-v{color:var(--fail)}
68
- .stat.skip .stat-v{color:var(--skip)}
69
- .stat.dur .stat-v{color:var(--run)}
70
-
71
- .script-hdr{background:var(--s2);border:1px solid var(--b);border-radius:var(--r);padding:10px 14px;display:flex;align-items:center;gap:8px}
72
- .script-name{font-size:.85rem;font-weight:600}
73
- .script-meta{font-size:.68rem;color:var(--m);margin-top:2px}
74
-
75
- .steps{display:flex;flex-direction:column;gap:6px}
76
- .step-card{background:var(--s2);border:1px solid var(--b);border-left:3px solid var(--b2);border-radius:var(--r);overflow:hidden;animation:si .2s ease}
77
- @keyframes si{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
78
- .step-card.PASS {border-left-color:var(--pass)}
79
- .step-card.FAIL {border-left-color:var(--fail)}
80
- .step-card.SKIP {border-left-color:var(--skip);opacity:.8}
81
- .step-card.RUNNING {border-left-color:var(--run)}
82
-
83
- .step-row{display:grid;grid-template-columns:34px 1fr auto auto;align-items:center;gap:10px;padding:9px 12px;cursor:pointer;transition:background .15s}
84
- .step-row:hover{background:rgba(255,255,255,.03)}
85
-
86
- .snum{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.68rem;font-weight:700;background:var(--surface);border:1px solid var(--b2);color:var(--m);flex-shrink:0}
87
- .step-card.PASS .snum{background:var(--pb);border-color:var(--pass);color:var(--pass)}
88
- .step-card.FAIL .snum{background:var(--fb);border-color:var(--fail);color:var(--fail)}
89
- .step-card.SKIP .snum{background:var(--sb);border-color:var(--skip);color:var(--skip)}
90
- .step-card.RUNNING .snum{background:var(--rb);border-color:var(--run);color:var(--run)}
91
-
92
- .sname{font-size:.82rem;font-weight:500;color:#dde4f0}
93
- .sscript{font-size:.65rem;color:var(--m);font-family:monospace;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:460px}
94
-
95
- .spill{padding:2px 8px;border-radius:99px;font-size:.65rem;font-weight:700;flex-shrink:0}
96
- .spill-PASS {background:var(--pb);color:var(--pass);border:1px solid var(--pbd)}
97
- .spill-FAIL {background:var(--fb);color:var(--fail);border:1px solid var(--fbd)}
98
- .spill-SKIP {background:var(--sb);color:var(--skip);border:1px solid var(--sbd)}
99
- .spill-RUNNING {background:var(--rb);color:var(--run);border:1px solid var(--rbd)}
100
-
101
- .sdur{font-size:.65rem;color:var(--m);min-width:60px;text-align:right;flex-shrink:0}
102
-
103
- .sdet{display:none;padding:0 12px 10px;border-top:1px solid var(--b)}
104
- .sdet.open{display:block}
105
- .sdet-comment{font-size:.75rem;line-height:1.5;padding:7px 10px;border-radius:var(--rs);background:var(--surface);border:1px solid var(--b2);color:var(--m2);margin-bottom:6px}
106
- .sdet-comment.is-fail {background:var(--fb);border-color:var(--fbd);color:#fca5a5}
107
- .sdet-comment.is-skip {background:var(--sb);border-color:var(--sbd);color:#fcd34d}
108
- .sdet-comment.is-cancel{background:var(--cnb);border-color:var(--cnbd);color:#d8b4fe}
109
- .step-sc{width:100%;border-radius:var(--rs);border:1px solid var(--b2);cursor:zoom-in;margin-top:6px;max-height:220px;object-fit:cover}
110
-
111
- .empty{text-align:center;padding:50px 20px;color:var(--m)}
112
- .empty-icon{font-size:2.5rem;margin-bottom:10px}
113
- .empty-sub{font-size:.75rem;margin-top:6px;opacity:.6}
114
-
115
- .sb-sec{padding:14px;border-bottom:1px solid var(--b);flex-shrink:0}
116
- .sb-title{font-size:.62rem;font-weight:700;color:var(--m);text-transform:uppercase;letter-spacing:.12em;margin-bottom:8px}
117
-
118
- /* ── Step overview list in sidebar ── */
119
- .mini-list{display:flex;flex-direction:column;gap:3px;max-height:240px;overflow-y:auto}
120
- .mini-step{display:flex;align-items:center;gap:7px;padding:5px 8px;border-radius:var(--rs);font-size:.7rem;background:var(--s2);border:1px solid var(--b);transition:border-color .2s}
121
- .mini-step.PASS {border-left:2px solid var(--pass)}
122
- .mini-step.FAIL {border-left:2px solid var(--fail)}
123
- .mini-step.SKIP {border-left:2px solid var(--skip);opacity:.8}
124
- .mini-step.RUNNING {border-left:2px solid var(--run)}
125
- .mini-dot{width:6px;height:6px;border-radius:50%;background:var(--b2);flex-shrink:0}
126
- .mini-step.PASS .mini-dot{background:var(--pass)}
127
- .mini-step.FAIL .mini-dot{background:var(--fail)}
128
- .mini-step.SKIP .mini-dot{background:var(--skip)}
129
- .mini-step.RUNNING .mini-dot{background:var(--run);animation:pulse .9s infinite}
130
- .mini-step-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;color:var(--m2)}
131
- .mini-badge{font-size:.58rem;font-weight:700;padding:1px 5px;border-radius:4px;flex-shrink:0}
132
- .mini-badge.PASS {background:var(--pb);color:var(--pass)}
133
- .mini-badge.FAIL {background:var(--fb);color:var(--fail)}
134
- .mini-badge.SKIP {background:var(--sb);color:var(--skip)}
135
- .mini-badge.RUNNING{background:var(--rb);color:var(--run)}
136
-
137
- .sum-box{background:var(--s2);border:1px solid var(--b);border-radius:var(--r)}
138
- .sum-row{display:flex;justify-content:space-between;padding:7px 12px;font-size:.75rem;border-bottom:1px solid var(--b)}
139
- .sum-row:last-child{border-bottom:none}
140
- .sum-lbl{color:var(--m)}
141
- .sum-val{font-weight:600}
142
-
143
- .log-box{flex:1;overflow-y:auto;padding:8px 12px;font-family:'Cascadia Code','Fira Code',monospace;font-size:.66rem;line-height:1.7;background:#060b12}
144
- .ll{color:var(--m);word-break:break-word}
145
- .lp{color:var(--pass)}.lf{color:var(--fail)}.ls{color:var(--skip)}.lr{color:var(--run)}.ly{color:#d2a8ff}
146
-
147
- #modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.9);z-index:1000;align-items:center;justify-content:center;backdrop-filter:blur(5px)}
148
- #modal.open{display:flex}
149
- .modal-inner{position:relative}
150
- .modal-inner img{max-width:92vw;max-height:90vh;border-radius:8px;border:1px solid var(--b2)}
151
- .modal-close{position:absolute;top:-30px;right:0;background:none;border:none;color:#fff;font-size:1.4rem;cursor:pointer;opacity:.7}
152
- .modal-close:hover{opacity:1}
153
-
154
- ::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--b2);border-radius:2px}
155
- </style>
156
- </head>
157
- <body>
158
-
159
- <header>
160
- <div class="h-left">
161
- <!-- <span class="h-logo">🎭</span> -->
162
- <div>
163
- <div class="h-title">Lexxit — Live Execution</div>
164
- <div class="h-sub" id="execLabel">Connecting…</div>
165
- </div>
166
- </div>
167
- <div class="h-right">
168
- <a class="btn-icon btn-newwin" id="newWinBtn" href="#" target="_blank">↗ New Tab</a>
169
- <button class="btn-icon btn-cancel" id="cancelBtn" onclick="cancelExecution()" disabled>⏹ Cancel</button>
170
- <div class="badge badge-idle" id="statusBadge"><span class="dot"></span><span id="statusText">CONNECTING</span></div>
171
- </div>
172
- </header>
173
-
174
- <div class="layout">
175
- <div class="main">
176
- <div>
177
- <div class="prog-labels"><span id="progLeft">Waiting for execution…</span><span id="progRight">0%</span></div>
178
- <div class="prog-track"><div class="prog-fill" id="progFill"></div></div>
179
- </div>
180
- <div class="stats">
181
- <div class="stat total"><div class="stat-v" id="sTotal">—</div><div class="stat-l">Total Steps</div></div>
182
- <div class="stat pass" ><div class="stat-v" id="sPass">0</div><div class="stat-l">Passed</div></div>
183
- <div class="stat fail" ><div class="stat-v" id="sFail">0</div><div class="stat-l">Failed</div></div>
184
- <div class="stat skip" ><div class="stat-v" id="sSkip">0</div><div class="stat-l">Skipped</div></div>
185
- <div class="stat dur" ><div class="stat-v" id="sDur">0s</div><div class="stat-l">Duration</div></div>
186
- </div>
187
- <div class="script-hdr" id="scriptHdr" style="display:none">
188
- <span>📋</span>
189
- <div><div class="script-name" id="scriptName">—</div><div class="script-meta" id="scriptMeta">—</div></div>
190
- </div>
191
- <div class="steps" id="stepsContainer">
192
- <div class="empty" id="emptyState">
193
- <div class="empty-icon">⏳</div>
194
- <div>Waiting for execution to begin…</div>
195
- <div class="empty-sub">Steps will appear here as they execute</div>
196
- </div>
197
- </div>
198
- </div>
199
-
200
- <div class="sidebar">
201
- <div class="sb-sec">
202
- <div class="sb-title">📊 Step Overview</div>
203
- <div class="mini-list" id="miniList"></div>
204
- </div>
205
- <div class="sb-sec" id="sumSection" style="display:none">
206
- <div class="sb-title">🏁 Summary</div>
207
- <div class="sum-box">
208
- <div class="sum-row"><span class="sum-lbl">Status</span> <span class="sum-val" id="sumStatus">—</span></div>
209
- <div class="sum-row"><span class="sum-lbl">Total Steps</span> <span class="sum-val" id="sumTotal">—</span></div>
210
- <div class="sum-row"><span class="sum-lbl">Passed</span> <span class="sum-val" style="color:var(--pass)" id="sumPass">—</span></div>
211
- <div class="sum-row"><span class="sum-lbl">Failed</span> <span class="sum-val" style="color:var(--fail)" id="sumFail">—</span></div>
212
- <div class="sum-row"><span class="sum-lbl">Skipped</span> <span class="sum-val" style="color:var(--skip)" id="sumSkip">—</span></div>
213
- <div class="sum-row"><span class="sum-lbl">Duration</span> <span class="sum-val" id="sumDur">—</span></div>
214
- </div>
215
- </div>
216
- <div class="sb-sec" style="flex-shrink:0"><div class="sb-title">📋 Live Log</div></div>
217
- <div class="log-box" id="logBox"></div>
218
- </div>
219
- </div>
220
-
221
- <div id="modal" onclick="closeModal()">
222
- <div class="modal-inner" onclick="event.stopPropagation()">
223
- <button class="modal-close" onclick="closeModal()">✕</button>
224
- <img id="modalImg" src="" alt="screenshot" />
225
- </div>
226
- </div>
227
-
228
- <script>
229
- // URL patterns:
230
- // /dashboard/:execId → single-script live view (original behaviour)
231
- // /dashboard/:execId/:scriptUid → filtered view — only events for that script
232
- const segments = location.pathname.split('/').filter(Boolean);
233
- // segments[0] = 'dashboard', segments[1] = execId, segments[2] = optional scriptUid
234
- const execId = segments[1] || segments[segments.length - 1]; // robust fallback
235
- const filterScriptUid = segments[2] || null; // null = no filter
236
-
237
- const $ = id => document.getElementById(id);
238
-
239
- let totalSteps = 0, passed = 0, failed = 0, skipped = 0;
240
- let startTime = Date.now(), timerInt, isDone = false;
241
-
242
- // Set header info
243
- $('execLabel').textContent = filterScriptUid
244
- ? 'Script: ' + filterScriptUid.slice(0, 8) + '…'
245
- : 'ID: ' + execId;
246
- $('newWinBtn').href = location.href;
247
-
248
- // Show "Back to Execution" button only in filtered (multi-script) mode
249
- if (filterScriptUid) {
250
- const backBtn = document.createElement('a');
251
- backBtn.className = 'btn-icon btn-newwin';
252
- backBtn.href = `/execution/${execId}`;
253
- backBtn.textContent = '← All Scripts';
254
- backBtn.style.marginRight = '4px';
255
- $('newWinBtn').parentNode.insertBefore(backBtn, $('newWinBtn'));
256
- }
257
-
258
- const esc = s => String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
259
-
260
- function setStatus(text, cls) {
261
- const b = $('statusBadge');
262
- b.className = 'badge ' + cls;
263
- b.querySelector('span:last-child').textContent = text;
264
- }
265
-
266
- function setProgress(done, total, fillCls) {
267
- const pct = total > 0 ? Math.round(done / total * 100) : 0;
268
- const f = $('progFill');
269
- f.style.width = pct + '%';
270
- if (fillCls) { f.classList.remove('done','fail','cancelled'); f.classList.add(fillCls); }
271
- $('progRight').textContent = pct + '%';
272
- $('progLeft').textContent = total > 0 ? `Step ${done} of ${total}` : 'Waiting…';
273
- }
274
-
275
- function updateStats() {
276
- $('sTotal').textContent = totalSteps > 0 ? totalSteps : '—';
277
- $('sPass').textContent = passed;
278
- $('sFail').textContent = failed;
279
- $('sSkip').textContent = skipped;
280
- if (!isDone) $('sDur').textContent = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
281
- }
282
-
283
- function fmtDur(ms) {
284
- if (!ms && ms !== 0) return '—';
285
- return ms < 1000 ? ms + 'ms' : (ms/1000).toFixed(1) + 's';
286
- }
287
-
288
- function log(msg, cls='') {
289
- const b = $('logBox');
290
- const ts = new Date().toLocaleTimeString('en-GB');
291
- const d = document.createElement('div');
292
- d.className = 'll ' + cls;
293
- d.textContent = `[${ts}] ${msg}`;
294
- b.appendChild(d);
295
- b.scrollTop = b.scrollHeight;
296
- }
297
-
298
- function openModal(src) { $('modalImg').src = src; $('modal').classList.add('open'); }
299
- function closeModal() { $('modal').classList.remove('open'); }
300
- document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
301
-
302
- async function cancelExecution() {
303
- if (!confirm('Cancel? The current step will finish, then remaining steps are skipped.')) return;
304
- $('cancelBtn').disabled = true;
305
- try {
306
- const r = await fetch(`/api/cancel/${execId}`, { method: 'DELETE' });
307
- const d = await r.json();
308
- log('⏹ ' + d.message, 'ly');
309
- } catch (e) {
310
- log('Cancel failed: ' + e.message, 'lf');
311
- $('cancelBtn').disabled = false;
312
- }
313
- }
314
-
315
- // ─── Step card builder ────────────────────────────────────────────────────────
316
-
317
- /**
318
- * Create a RUNNING step card.
319
- * Called on step_start.
320
- */
321
- function addStepCard(data) {
322
- const empty = $('emptyState');
323
- if (empty) empty.remove();
324
- if ($('sc-' + data.step_number)) return; // replay guard
325
-
326
- const card = document.createElement('div');
327
- card.className = 'step-card RUNNING';
328
- card.id = 'sc-' + data.step_number;
329
- card.innerHTML = `
330
- <div class="step-row" onclick="toggleDet(${data.step_number})">
331
- <div class="snum" id="sn-${data.step_number}">${data.step_number}</div>
332
- <div>
333
- <div class="sname">${esc(data.step_name)}</div>
334
-
335
- </div>
336
- <span class="spill spill-RUNNING" id="sp-${data.step_number}">⏳ RUNNING</span>
337
- <span class="sdur" id="sd-${data.step_number}">—</span>
338
- </div>
339
- <div class="sdet" id="sdet-${data.step_number}"></div>`;
340
- $('stepsContainer').appendChild(card);
341
- card.scrollIntoView({ behavior:'smooth', block:'nearest' });
342
- }
343
-
344
- /**
345
- * ✅ FIX: Complete a step card — also CREATES it if it doesn't exist yet.
346
- * Skipped steps arrive as step_complete WITHOUT a prior step_start,
347
- * so we must handle card creation here too.
348
- */
349
- function completeStepCard(data) {
350
- const empty = $('emptyState');
351
- if (empty) empty.remove();
352
-
353
- let card = $('sc-' + data.step_number);
354
-
355
- // ── Create card if it doesn't exist (SKIP steps bypass step_start) ──────
356
- if (!card) {
357
- card = document.createElement('div');
358
- card.className = 'step-card ' + data.status;
359
- card.id = 'sc-' + data.step_number;
360
- card.innerHTML = `
361
- <div class="step-row" onclick="toggleDet(${data.step_number})">
362
- <div class="snum" id="sn-${data.step_number}">${data.step_number}</div>
363
- <div>
364
- <div class="sname">${esc(data.step_name)}</div>
365
-
366
- </div>
367
- <span class="spill spill-${data.status}" id="sp-${data.step_number}"></span>
368
- <span class="sdur" id="sd-${data.step_number}">—</span>
369
- </div>
370
- <div class="sdet" id="sdet-${data.step_number}"></div>`;
371
- $('stepsContainer').appendChild(card);
372
- // Auto-scroll for skipped steps too
373
- card.scrollIntoView({ behavior:'smooth', block:'nearest' });
374
- } else {
375
- card.classList.remove('RUNNING');
376
- card.classList.add(data.status);
377
- }
378
-
379
- const icons = { PASS:'✅', FAIL:'❌', SKIP:'⏭' };
380
- const labels = { PASS:'PASS', FAIL:'FAIL', SKIP:'SKIP' };
381
- $('sp-' + data.step_number).textContent = `${icons[data.status]||''} ${labels[data.status]||data.status}`;
382
- $('sp-' + data.step_number).className = `spill spill-${data.status}`;
383
- $('sd-' + data.step_number).textContent = data.duration >= 0 ? fmtDur(data.duration) : '—';
384
-
385
- // Detail block
386
- let cls = '';
387
- if (data.status === 'FAIL') cls = 'is-fail';
388
- if (data.status === 'SKIP') cls = data.skip_reason === 'CANCELLED' ? 'is-cancel' : 'is-skip';
389
-
390
- const commentText = data.skip_reason
391
- ? `⏭ ${data.skip_reason}`
392
- : (data.comments || '—');
393
-
394
- let html = `<div class="sdet-comment ${cls}">${esc(commentText)}</div>`;
395
- if (data.screenshot) {
396
- html += `<img class="step-sc" src="data:image/png;base64,${data.screenshot}" onclick="openModal(this.src)" title="Click to enlarge" />`;
397
- }
398
- $('sdet-' + data.step_number).innerHTML = html;
399
-
400
- // Auto-expand failures and skips
401
- if (data.status === 'FAIL' || data.status === 'SKIP') {
402
- $('sdet-' + data.step_number).classList.add('open');
403
- }
404
- }
405
-
406
- // ─── Step overview (sidebar mini-list) ───────────────────────────────────────
407
-
408
- /**
409
- * ✅ FIX: addMiniStep handles ALL statuses — not just RUNNING.
410
- * Skipped steps are added directly with SKIP styling.
411
- */
412
- function addMiniStep(data, initialStatus) {
413
- if ($('ms-' + data.step_number)) {
414
- // Already exists — update status
415
- updateMiniStep(data.step_number, initialStatus || 'RUNNING');
416
- return;
417
- }
418
- const status = initialStatus || 'RUNNING';
419
- const icons = { PASS:'✅', FAIL:'❌', SKIP:'⏭', RUNNING:'▶' };
420
- const d = document.createElement('div');
421
- d.className = 'mini-step ' + status;
422
- d.id = 'ms-' + data.step_number;
423
- d.innerHTML = `
424
- <span class="mini-dot"></span>
425
- <span class="mini-step-name">${data.step_number}. ${esc(data.step_name)}</span>
426
- <span class="mini-badge ${status}" id="mb-${data.step_number}">${icons[status]||status}</span>`;
427
- $('miniList').appendChild(d);
428
- $('miniList').scrollTop = $('miniList').scrollHeight;
429
- }
430
-
431
- function updateMiniStep(num, status) {
432
- const el = $('ms-' + num);
433
- const mb = $('mb-' + num);
434
- if (!el) return;
435
- el.className = 'mini-step ' + status;
436
- const icons = { PASS:'✅', FAIL:'❌', SKIP:'⏭', RUNNING:'▶' };
437
- if (mb) { mb.className = `mini-badge ${status}`; mb.textContent = icons[status]||status; }
438
- }
439
-
440
- function toggleDet(num) { $('sdet-' + num)?.classList.toggle('open'); }
441
-
442
- function showSummary(status, dur) {
443
- $('sumSection').style.display = 'block';
444
- $('sumStatus').textContent = status;
445
- $('sumStatus').style.color = status === 'PASS' ? 'var(--pass)' : status === 'CANCELLED' ? 'var(--cancel)' : 'var(--fail)';
446
- $('sumTotal').textContent = passed + failed + skipped;
447
- $('sumPass').textContent = passed;
448
- $('sumFail').textContent = failed;
449
- $('sumSkip').textContent = skipped;
450
- $('sumDur').textContent = dur || $('sDur').textContent;
451
- }
452
-
453
- // ─── SSE Connection ───────────────────────────────────────────────────────────
454
- const es = new EventSource('/api/stream/' + execId);
455
-
456
- // Returns true if this SSE event should be processed by this dashboard.
457
- // Execution-level events (start/complete/cancel/error) always pass through.
458
- // Script/step events are filtered to the target script when in filtered mode.
459
- function isRelevant(data) {
460
- if (!filterScriptUid) return true;
461
- if (data.test_script_uid == null) return true; // execution-level event
462
- return data.test_script_uid === filterScriptUid;
463
- }
464
-
465
- es.onopen = () => {
466
- log('Connected to execution stream.', 'ly');
467
- if (!timerInt) timerInt = setInterval(updateStats, 300);
468
- };
469
-
470
- es.onmessage = e => {
471
- let ev;
472
- try { ev = JSON.parse(e.data); } catch { return; }
473
- const { type, data } = ev;
474
-
475
- if (type === 'log') { log(data.message||'', ''); return; }
476
- if (type === 'queue_update') return;
477
-
478
- if (type === 'execution_start') {
479
- startTime = Date.now();
480
- if (!timerInt) timerInt = setInterval(updateStats, 300);
481
- setStatus('RUNNING', 'badge-running');
482
- $('cancelBtn').disabled = false;
483
- // Show test set name in header if provided
484
- if (data.test_set_name) {
485
- document.title = `${data.test_set_name} — Live Dashboard`;
486
- document.querySelector('.h-title').textContent = data.test_set_name;
487
- }
488
- log(`Execution started — ${data.total_scripts} script(s)`, 'lr');
489
- }
490
-
491
- if (type === 'script_start') {
492
- if (!isRelevant(data)) return;
493
- totalSteps = data.total_steps || 0;
494
- $('scriptHdr').style.display = 'flex';
495
- $('scriptName').textContent = data.test_case_name || data.test_script_uid;
496
- $('scriptMeta').textContent = `${totalSteps} steps · ${data.browser||'chromium'} · UID: ${data.test_script_uid}`;
497
- updateStats();
498
- setProgress(0, totalSteps);
499
- log(`Script: "${data.test_case_name}" — ${totalSteps} steps`, 'ly');
500
- }
501
-
502
- if (type === 'step_start') {
503
- if (!isRelevant(data)) return;
504
- addStepCard(data);
505
- addMiniStep(data, 'RUNNING');
506
- log(`▶ Step ${data.step_number}: ${data.step_name}`, 'lr');
507
- }
508
-
509
- if (type === 'step_complete') {
510
- if (!isRelevant(data)) return;
511
- // ✅ Count first, before card update
512
- if (data.status === 'PASS') passed++;
513
- else if (data.status === 'FAIL') failed++;
514
- else if (data.status === 'SKIP') skipped++;
515
-
516
- // ✅ completeStepCard also creates the card if missing (handles SKIP bypass)
517
- completeStepCard(data);
518
-
519
- // ✅ addMiniStep for skipped steps that never got a step_start
520
- addMiniStep(data, data.status);
521
- updateMiniStep(data.step_number, data.status);
522
-
523
- setProgress(passed + failed + skipped, totalSteps);
524
- updateStats();
525
-
526
- const icon = { PASS:'✅', FAIL:'❌', SKIP:'⏭' }[data.status]||'';
527
- const reason = data.skip_reason ? ` (${data.skip_reason})` : '';
528
- log(`${icon} Step ${data.step_number}: ${data.step_name} — ${data.comments||''}${reason}`,
529
- data.status==='PASS' ? 'lp' : data.status==='SKIP' ? 'ls' : 'lf');
530
- }
531
-
532
- if (type === 'script_complete') {
533
- if (!isRelevant(data)) return;
534
- const dur = fmtDur(data.duration);
535
- // In filtered mode, script_complete IS the final result for this view
536
- if (filterScriptUid) {
537
- isDone = true; clearInterval(timerInt);
538
- $('cancelBtn').disabled = true;
539
- const ok = data.overall_status === 'PASS';
540
- const durStr = fmtDur(data.duration || 0);
541
- setStatus(ok ? '✅ PASSED' : '❌ FAILED', ok ? 'badge-pass' : 'badge-fail');
542
- $('progFill').style.width = '100%';
543
- $('progFill').classList.add(ok ? 'done' : 'fail');
544
- $('progRight').textContent = '100%';
545
- if (totalSteps > 0) $('progLeft').textContent = `Step ${totalSteps} of ${totalSteps}`;
546
- $('sDur').textContent = durStr;
547
- updateStats(); showSummary(data.overall_status, durStr);
548
- }
549
- log(`Script done: ${data.overall_status} | ✅${data.passed_steps} ❌${data.failed_steps} ⏭${data.skipped_steps} (${dur})`, 'ly');
550
- }
551
-
552
- if (type === 'execution_cancelled') {
553
- isDone = true; clearInterval(timerInt);
554
- $('cancelBtn').disabled = true;
555
- setStatus('CANCELLED', 'badge-cancel');
556
- $('progFill').style.width = '100%';
557
- $('progFill').classList.add('cancelled');
558
- $('progRight').textContent = '100%';
559
- updateStats(); showSummary('CANCELLED');
560
- log('⏹ Execution cancelled', 'ly');
561
- es.close();
562
- }
563
-
564
- if (type === 'execution_complete') {
565
- isDone = true; clearInterval(timerInt);
566
- $('cancelBtn').disabled = true;
567
- const ok = data.status === 'PASS';
568
- const dur = fmtDur(data.duration || 0);
569
- setStatus(ok ? '✅ PASSED' : '❌ FAILED', ok ? 'badge-pass' : 'badge-fail');
570
- $('progFill').style.width = '100%';
571
- $('progFill').classList.add(ok ? 'done' : 'fail');
572
- $('progRight').textContent = '100%';
573
- if (totalSteps > 0) $('progLeft').textContent = `Step ${totalSteps} of ${totalSteps}`;
574
- $('sDur').textContent = dur;
575
- updateStats(); showSummary(data.status, dur);
576
- log(`🏁 ${data.status} | ✅${passed} ❌${failed} ⏭${skipped} | ${dur}`, 'ly');
577
- es.close();
578
- }
579
-
580
- if (type === 'error') {
581
- setStatus('ERROR', 'badge-fail');
582
- log('ERROR: ' + data.message, 'lf');
583
- $('cancelBtn').disabled = true; isDone = true; clearInterval(timerInt);
584
- es.close();
585
- }
586
- };
587
-
588
- es.onerror = () => { if (!isDone) log('Stream disconnected.', 'lf'); es.close(); };
589
- </script>
590
- </body>
591
- </html>