lexxit-automation-framework 2.0.7 → 2.0.9
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.
- package/dist-obf/app.js +1 -1
- package/dist-obf/controllers/controller.js +1 -1
- package/dist-obf/core/BrowserManager.js +1 -1
- package/dist-obf/core/PlaywrightEngine.js +1 -1
- package/dist-obf/core/ScreenshotManager.js +1 -1
- package/dist-obf/core/TestData.js +1 -1
- package/dist-obf/core/TestExecutor.js +1 -1
- package/dist-obf/core/handlers/AllHandlers.js +1 -1
- package/dist-obf/core/handlers/BaseHandler.js +1 -1
- package/dist-obf/core/handlers/ClickHandler.js +1 -1
- package/dist-obf/core/handlers/CustomCodeHandler.js +1 -1
- package/dist-obf/core/handlers/DropdownHandler.js +1 -1
- package/dist-obf/core/handlers/InputHandler.js +1 -1
- package/dist-obf/core/registry/ActionRegistry.js +1 -1
- package/dist-obf/installer/frameworkLauncher.js +1 -1
- package/dist-obf/public/dashboard.html +61 -10
- package/dist-obf/public/execution.html +510 -0
- package/dist-obf/public/queue-monitor.html +221 -3
- package/dist-obf/queue/ExecutionQueue.js +1 -1
- package/dist-obf/routes/api.routes.js +1 -1
- package/dist-obf/server.js +1 -1
- package/dist-obf/types/types.js +1 -1
- package/dist-obf/utils/chromefinder.js +1 -1
- package/dist-obf/utils/elementHighlight.js +1 -1
- package/dist-obf/utils/locatorHelper.js +1 -1
- package/dist-obf/utils/logger.js +1 -1
- package/dist-obf/utils/response.js +1 -1
- package/dist-obf/utils/responseFormatter.js +1 -1
- package/dist-obf/utils/sseManager.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,510 @@
|
|
|
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 Overview</title>
|
|
7
|
+
<style>
|
|
8
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
9
|
+
:root{
|
|
10
|
+
--bg:#0a0d14;--s:#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
|
+
--pend:#64748b;--pendb:#0f172a;--pendbd:#1e2d45;
|
|
18
|
+
--cancel:#a855f7;--cnb:#1a0a2e;--cnbd:#4a1d7e;
|
|
19
|
+
--r:10px;--rs:6px;
|
|
20
|
+
}
|
|
21
|
+
html,body{height:100%;background:var(--bg);color:var(--t);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
22
|
+
|
|
23
|
+
/* ── Header ── */
|
|
24
|
+
header{background:var(--s);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}
|
|
25
|
+
.h-left{display:flex;align-items:center;gap:12px}
|
|
26
|
+
.h-logo{font-size:1.4rem}
|
|
27
|
+
.h-title{font-size:.95rem;font-weight:700}
|
|
28
|
+
.h-sub{font-size:.68rem;color:var(--m);font-family:monospace;margin-top:2px}
|
|
29
|
+
.h-right{display:flex;align-items:center;gap:8px}
|
|
30
|
+
|
|
31
|
+
.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 11px;border-radius:99px;font-size:.7rem;font-weight:700;letter-spacing:.04em;border:1px solid transparent}
|
|
32
|
+
.badge .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
|
|
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)}
|
|
37
|
+
.badge-fail{background:var(--fb);color:var(--fail);border-color:var(--fbd)}
|
|
38
|
+
.badge-fail .dot{background:var(--fail)}
|
|
39
|
+
.badge-idle{background:var(--s2);color:var(--m);border-color:var(--b)}
|
|
40
|
+
.badge-idle .dot{background:var(--m)}
|
|
41
|
+
.badge-mode{background:var(--s2);color:var(--m2);border-color:var(--b);font-size:.65rem;padding:3px 9px}
|
|
42
|
+
|
|
43
|
+
.btn-icon{display:inline-flex;align-items:center;gap:5px;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:not(:disabled){background:#3d0a0a;border-color:var(--fail)}
|
|
46
|
+
.btn-cancel:disabled{opacity:.4;cursor:not-allowed}
|
|
47
|
+
.btn-queue{color:var(--m2);border-color:var(--b);background:var(--s2)}
|
|
48
|
+
.btn-queue:hover{color:var(--t);border-color:var(--b2)}
|
|
49
|
+
|
|
50
|
+
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.8)}}
|
|
51
|
+
|
|
52
|
+
/* ── Layout ── */
|
|
53
|
+
.layout{display:grid;grid-template-columns:1fr 280px;height:calc(100vh - 53px);overflow:hidden}
|
|
54
|
+
.main{overflow-y:auto;padding:18px 20px;display:flex;flex-direction:column;gap:14px}
|
|
55
|
+
.sidebar{background:var(--s);border-left:1px solid var(--b);display:flex;flex-direction:column;overflow:hidden}
|
|
56
|
+
|
|
57
|
+
/* ── Top stats bar ── */
|
|
58
|
+
.stats-bar{display:grid;grid-template-columns:repeat(6,1fr);gap:8px}
|
|
59
|
+
.stat{background:var(--s2);border:1px solid var(--b);border-radius:var(--r);padding:10px;text-align:center}
|
|
60
|
+
.stat-v{font-size:1.5rem;font-weight:800;line-height:1}
|
|
61
|
+
.stat-l{font-size:.58rem;color:var(--m);margin-top:3px;text-transform:uppercase;letter-spacing:.08em}
|
|
62
|
+
.stat.total .stat-v{color:var(--t)}
|
|
63
|
+
.stat.running .stat-v{color:var(--run)}
|
|
64
|
+
.stat.passed .stat-v{color:var(--pass)}
|
|
65
|
+
.stat.failed .stat-v{color:var(--fail)}
|
|
66
|
+
.stat.pending .stat-v{color:var(--pend)}
|
|
67
|
+
.stat.dur .stat-v{color:var(--run);font-size:1.1rem}
|
|
68
|
+
|
|
69
|
+
/* ── Section label ── */
|
|
70
|
+
.sec-label{font-size:.62rem;font-weight:700;color:var(--m);text-transform:uppercase;letter-spacing:.1em}
|
|
71
|
+
|
|
72
|
+
/* ── Script cards grid ── */
|
|
73
|
+
.scripts-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:10px}
|
|
74
|
+
|
|
75
|
+
.script-card{background:var(--s2);border:1px solid var(--b);border-left:3px solid var(--pendbd);border-radius:var(--r);padding:13px 15px;transition:border-left-color .3s}
|
|
76
|
+
.script-card.pending {border-left-color:var(--pendbd)}
|
|
77
|
+
.script-card.running {border-left-color:var(--run)}
|
|
78
|
+
.script-card.passed {border-left-color:var(--pass)}
|
|
79
|
+
.script-card.failed {border-left-color:var(--fail)}
|
|
80
|
+
.script-card.cancelled{border-left-color:var(--cancel)}
|
|
81
|
+
|
|
82
|
+
.card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:8px}
|
|
83
|
+
.card-info{flex:1;min-width:0}
|
|
84
|
+
.card-name{font-size:.82rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--t)}
|
|
85
|
+
.card-meta{font-size:.65rem;color:var(--m);margin-top:3px;display:flex;gap:8px;flex-wrap:wrap}
|
|
86
|
+
.card-meta span{display:flex;align-items:center;gap:3px}
|
|
87
|
+
|
|
88
|
+
.sbadge{padding:2px 9px;border-radius:99px;font-size:.62rem;font-weight:700;flex-shrink:0;white-space:nowrap}
|
|
89
|
+
.sb-pending {background:var(--pendb);color:var(--pend);border:1px solid var(--pendbd)}
|
|
90
|
+
.sb-running {background:var(--rb);color:var(--run);border:1px solid var(--rbd)}
|
|
91
|
+
.sb-running span{animation:pulse .9s infinite}
|
|
92
|
+
.sb-passed {background:var(--pb);color:var(--pass);border:1px solid var(--pbd)}
|
|
93
|
+
.sb-failed {background:var(--fb);color:var(--fail);border:1px solid var(--fbd)}
|
|
94
|
+
.sb-cancelled{background:var(--cnb);color:var(--cancel);border:1px solid var(--cnbd)}
|
|
95
|
+
|
|
96
|
+
.prog-wrap{height:3px;background:var(--b);border-radius:99px;overflow:hidden;margin-bottom:8px}
|
|
97
|
+
.prog-fill{height:100%;background:var(--run);border-radius:99px;transition:width .4s ease;width:0%}
|
|
98
|
+
.prog-fill.done{background:var(--pass)}
|
|
99
|
+
.prog-fill.fail{background:var(--fail)}
|
|
100
|
+
|
|
101
|
+
.card-bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
102
|
+
.card-counts{display:flex;align-items:center;gap:8px;font-size:.68rem;color:var(--m)}
|
|
103
|
+
.card-counts .cp{color:var(--pass)}.card-counts .cf{color:var(--fail)}.card-counts .cs{color:var(--skip)}
|
|
104
|
+
.card-dur{font-size:.65rem;color:var(--m);font-family:monospace}
|
|
105
|
+
|
|
106
|
+
.btn-view{padding:3px 11px;border-radius:6px;font-size:.66rem;font-weight:600;cursor:pointer;border:1px solid var(--b);background:var(--s2);color:var(--run);text-decoration:none;transition:all .15s;display:inline-block}
|
|
107
|
+
.btn-view:hover{border-color:var(--run)}
|
|
108
|
+
.btn-view:not([href]){opacity:.35;pointer-events:none;cursor:default}
|
|
109
|
+
|
|
110
|
+
/* ── Sidebar ── */
|
|
111
|
+
.sb-sec{padding:12px 14px;border-bottom:1px solid var(--b);flex-shrink:0}
|
|
112
|
+
.sb-title{font-size:.62rem;font-weight:700;color:var(--m);text-transform:uppercase;letter-spacing:.12em;margin-bottom:8px}
|
|
113
|
+
.sum-box{background:var(--s2);border:1px solid var(--b);border-radius:var(--r)}
|
|
114
|
+
.sum-row{display:flex;justify-content:space-between;padding:7px 12px;font-size:.74rem;border-bottom:1px solid var(--b)}
|
|
115
|
+
.sum-row:last-child{border-bottom:none}
|
|
116
|
+
.sum-lbl{color:var(--m)}.sum-val{font-weight:600}
|
|
117
|
+
|
|
118
|
+
.log-box{flex:1;overflow-y:auto;padding:8px 12px;font-family:'Cascadia Code','Fira Code',monospace;font-size:.65rem;line-height:1.7;background:#060b12}
|
|
119
|
+
.ll{color:var(--m);word-break:break-word}
|
|
120
|
+
.lp{color:var(--pass)}.lf{color:var(--fail)}.ls{color:var(--skip)}.lr{color:var(--run)}.ly{color:#d2a8ff}
|
|
121
|
+
|
|
122
|
+
.empty{text-align:center;padding:40px;color:var(--m);font-size:.82rem}
|
|
123
|
+
|
|
124
|
+
::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--b2);border-radius:2px}
|
|
125
|
+
</style>
|
|
126
|
+
</head>
|
|
127
|
+
<body>
|
|
128
|
+
|
|
129
|
+
<header>
|
|
130
|
+
<div class="h-left">
|
|
131
|
+
<span class="h-logo">🎭</span>
|
|
132
|
+
<div>
|
|
133
|
+
<div class="h-title">Playwright — Execution Overview</div>
|
|
134
|
+
<div class="h-sub" id="execLabel">Connecting…</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="h-right">
|
|
138
|
+
<span class="badge badge-mode" id="modeTag">SEQUENTIAL</span>
|
|
139
|
+
<a class="btn-icon btn-queue" href="/queue-monitor" target="_blank">☰ Queue</a>
|
|
140
|
+
<button class="btn-icon btn-cancel" id="cancelBtn" onclick="cancelExecution()" disabled>⏹ Cancel</button>
|
|
141
|
+
<div class="badge badge-idle" id="statusBadge"><span class="dot"></span><span id="statusText">CONNECTING</span></div>
|
|
142
|
+
</div>
|
|
143
|
+
</header>
|
|
144
|
+
|
|
145
|
+
<div class="layout">
|
|
146
|
+
<div class="main">
|
|
147
|
+
<!-- Stats bar -->
|
|
148
|
+
<div class="stats-bar">
|
|
149
|
+
<div class="stat total" ><div class="stat-v" id="stTotal">—</div><div class="stat-l">Scripts</div></div>
|
|
150
|
+
<div class="stat running"><div class="stat-v" id="stRun">0</div><div class="stat-l">Running</div></div>
|
|
151
|
+
<div class="stat passed" ><div class="stat-v" id="stPass">0</div><div class="stat-l">Passed</div></div>
|
|
152
|
+
<div class="stat failed" ><div class="stat-v" id="stFail">0</div><div class="stat-l">Failed</div></div>
|
|
153
|
+
<div class="stat pending"><div class="stat-v" id="stPend">—</div><div class="stat-l">Pending</div></div>
|
|
154
|
+
<div class="stat dur" ><div class="stat-v" id="stDur">0s</div><div class="stat-l">Elapsed</div></div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<!-- Script cards -->
|
|
158
|
+
<div class="sec-label" id="scriptsLabel">Scripts</div>
|
|
159
|
+
<div class="scripts-grid" id="scriptsGrid">
|
|
160
|
+
<div class="empty">Loading scripts…</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<!-- Sidebar -->
|
|
165
|
+
<div class="sidebar">
|
|
166
|
+
<div class="sb-sec">
|
|
167
|
+
<div class="sb-title">🏁 Summary</div>
|
|
168
|
+
<div class="sum-box">
|
|
169
|
+
<div class="sum-row"><span class="sum-lbl">Status</span> <span class="sum-val" id="sumStatus">—</span></div>
|
|
170
|
+
<div class="sum-row"><span class="sum-lbl">Mode</span> <span class="sum-val" id="sumMode">—</span></div>
|
|
171
|
+
<div class="sum-row"><span class="sum-lbl">Total Scripts</span><span class="sum-val" id="sumScripts">—</span></div>
|
|
172
|
+
<div class="sum-row"><span class="sum-lbl">Passed</span> <span class="sum-val" style="color:var(--pass)" id="sumPass">—</span></div>
|
|
173
|
+
<div class="sum-row"><span class="sum-lbl">Failed</span> <span class="sum-val" style="color:var(--fail)" id="sumFail">—</span></div>
|
|
174
|
+
<div class="sum-row"><span class="sum-lbl">Duration</span> <span class="sum-val" id="sumDur">—</span></div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="sb-sec" style="flex-shrink:0"><div class="sb-title">📋 Live Log</div></div>
|
|
178
|
+
<div class="log-box" id="logBox"></div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<script>
|
|
183
|
+
const execId = location.pathname.split('/').filter(Boolean)[1];
|
|
184
|
+
const $ = id => document.getElementById(id);
|
|
185
|
+
const esc = s => String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
186
|
+
|
|
187
|
+
// ── State ────────────────────────────────────────────────────────────────────
|
|
188
|
+
let scripts = {}; // test_script_uid → { name, totalSteps, browser, status, passed, failed, skipped, startMs }
|
|
189
|
+
let execInfo = null; // from /api/execution/:id/info
|
|
190
|
+
let isDone = false;
|
|
191
|
+
let startMs = Date.now();
|
|
192
|
+
let timerInt = null;
|
|
193
|
+
let runCount = 0, passCount = 0, failCount = 0;
|
|
194
|
+
|
|
195
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
196
|
+
function fmtDur(ms) {
|
|
197
|
+
if (!ms && ms !== 0) return '—';
|
|
198
|
+
if (ms < 1000) return ms + 'ms';
|
|
199
|
+
return (ms / 1000).toFixed(1) + 's';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function log(msg, cls='') {
|
|
203
|
+
const b = $('logBox');
|
|
204
|
+
const ts = new Date().toLocaleTimeString('en-GB');
|
|
205
|
+
const d = document.createElement('div');
|
|
206
|
+
d.className = 'll ' + cls;
|
|
207
|
+
d.textContent = `[${ts}] ${msg}`;
|
|
208
|
+
b.appendChild(d);
|
|
209
|
+
b.scrollTop = b.scrollHeight;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function setStatus(text, cls) {
|
|
213
|
+
const b = $('statusBadge');
|
|
214
|
+
b.className = 'badge ' + cls;
|
|
215
|
+
b.querySelector('span:last-child').textContent = text;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function updateTopStats() {
|
|
219
|
+
const list = Object.values(scripts);
|
|
220
|
+
const pending = list.filter(s => s.status === 'pending').length;
|
|
221
|
+
const running = list.filter(s => s.status === 'running').length;
|
|
222
|
+
const passed = list.filter(s => s.status === 'passed').length;
|
|
223
|
+
const failed = list.filter(s => s.status === 'failed').length;
|
|
224
|
+
$('stTotal').textContent = list.length || '—';
|
|
225
|
+
$('stRun').textContent = running;
|
|
226
|
+
$('stPass').textContent = passed;
|
|
227
|
+
$('stFail').textContent = failed;
|
|
228
|
+
$('stPend').textContent = pending;
|
|
229
|
+
if (!isDone) $('stDur').textContent = ((Date.now() - startMs) / 1000).toFixed(1) + 's';
|
|
230
|
+
|
|
231
|
+
// Overall status badge
|
|
232
|
+
if (isDone) return;
|
|
233
|
+
if (running > 0) setStatus('RUNNING', 'badge-running');
|
|
234
|
+
else if (list.length > 0 && pending === list.length) setStatus('QUEUED', 'badge-idle');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Script card builders ──────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
function cardId(uid) { return 'sc-' + uid.replace(/-/g,''); }
|
|
240
|
+
|
|
241
|
+
function renderCard(uid) {
|
|
242
|
+
const s = scripts[uid];
|
|
243
|
+
if (!s) return;
|
|
244
|
+
const pct = s.totalSteps > 0 ? Math.round(((s.passed + s.failed + s.skipped) / s.totalSteps) * 100) : 0;
|
|
245
|
+
const dur = s.startMs ? fmtDur(Date.now() - s.startMs) : '—';
|
|
246
|
+
const fillCls = s.status === 'passed' ? 'done' : s.status === 'failed' ? 'fail' : '';
|
|
247
|
+
const shortUid = uid.slice(0, 8) + '…';
|
|
248
|
+
|
|
249
|
+
const badgeMap = {
|
|
250
|
+
pending: `<span class="sbadge sb-pending">PENDING</span>`,
|
|
251
|
+
running: `<span class="sbadge sb-running"><span>●</span> RUNNING</span>`,
|
|
252
|
+
passed: `<span class="sbadge sb-passed">✅ PASSED</span>`,
|
|
253
|
+
failed: `<span class="sbadge sb-failed">❌ FAILED</span>`,
|
|
254
|
+
cancelled: `<span class="sbadge sb-cancelled">CANCELLED</span>`,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// "View →" only available once script has started
|
|
258
|
+
const viewHref = (s.status !== 'pending') ? `/dashboard/${execId}/${uid}` : '';
|
|
259
|
+
const viewAttrs = viewHref ? `href="${viewHref}" target="_blank"` : '';
|
|
260
|
+
|
|
261
|
+
const el = $(cardId(uid));
|
|
262
|
+
if (!el) return;
|
|
263
|
+
|
|
264
|
+
el.className = `script-card ${s.status}`;
|
|
265
|
+
el.innerHTML = `
|
|
266
|
+
<div class="card-top">
|
|
267
|
+
<div class="card-info">
|
|
268
|
+
<div class="card-name" title="${esc(s.name)}">${esc(s.name)}</div>
|
|
269
|
+
<div class="card-meta">
|
|
270
|
+
<span>🆔 ${shortUid}</span>
|
|
271
|
+
<span>📋 ${s.totalSteps} steps</span>
|
|
272
|
+
<span>🌐 ${esc(s.browser)}</span>
|
|
273
|
+
${s.startMs && !['passed','failed','cancelled'].includes(s.status) ? `<span>⏱ ${fmtDur(Date.now() - s.startMs)}</span>` : ''}
|
|
274
|
+
${s.finalDur ? `<span>⏱ ${fmtDur(s.finalDur)}</span>` : ''}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
${badgeMap[s.status] || ''}
|
|
278
|
+
</div>
|
|
279
|
+
<div class="prog-wrap"><div class="prog-fill ${fillCls}" style="width:${pct}%"></div></div>
|
|
280
|
+
<div class="card-bottom">
|
|
281
|
+
<div class="card-counts">
|
|
282
|
+
<span class="cp">✅ ${s.passed}</span>
|
|
283
|
+
<span class="cf">❌ ${s.failed}</span>
|
|
284
|
+
<span class="cs">⏭ ${s.skipped}</span>
|
|
285
|
+
<span>${pct}%</span>
|
|
286
|
+
</div>
|
|
287
|
+
<a class="btn-view" ${viewAttrs}>View →</a>
|
|
288
|
+
</div>`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function buildGrid() {
|
|
292
|
+
const grid = $('scriptsGrid');
|
|
293
|
+
// First call — create all card shells in order
|
|
294
|
+
grid.innerHTML = Object.keys(scripts).map(uid =>
|
|
295
|
+
`<div class="script-card pending" id="${cardId(uid)}"></div>`
|
|
296
|
+
).join('');
|
|
297
|
+
// Then populate each
|
|
298
|
+
Object.keys(scripts).forEach(renderCard);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Load initial script list from backend ─────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
async function loadInfo() {
|
|
304
|
+
try {
|
|
305
|
+
const r = await fetch(`/api/execution/${execId}/info`);
|
|
306
|
+
const data = await r.json();
|
|
307
|
+
if (!data.scripts) return;
|
|
308
|
+
|
|
309
|
+
execInfo = data;
|
|
310
|
+
|
|
311
|
+
// Update mode badge
|
|
312
|
+
$('modeTag').textContent = data.parallel
|
|
313
|
+
? `PARALLEL (max ${data.max_parallel})`
|
|
314
|
+
: 'SEQUENTIAL';
|
|
315
|
+
|
|
316
|
+
$('execLabel').textContent = data.test_set_name
|
|
317
|
+
? `Set: ${data.test_set_name} · ID: ${execId}`
|
|
318
|
+
: 'ID: ' + execId;
|
|
319
|
+
$('sumMode').textContent = data.parallel ? 'Parallel' : 'Sequential';
|
|
320
|
+
$('sumScripts').textContent = data.scripts.length;
|
|
321
|
+
|
|
322
|
+
const label = data.parallel
|
|
323
|
+
? `${data.scripts.length} Scripts — Parallel (max ${data.max_parallel})`
|
|
324
|
+
: `${data.scripts.length} Scripts — Sequential`;
|
|
325
|
+
$('scriptsLabel').textContent = label;
|
|
326
|
+
|
|
327
|
+
// Update browser tab title
|
|
328
|
+
if (data.test_set_name) {
|
|
329
|
+
document.title = `${data.test_set_name} — Execution Overview`;
|
|
330
|
+
document.querySelector('.h-title').textContent = data.test_set_name;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Pre-populate script state map in order
|
|
334
|
+
data.scripts.forEach(s => {
|
|
335
|
+
scripts[s.test_script_uid] = {
|
|
336
|
+
name: s.test_case_name || s.test_script_uid,
|
|
337
|
+
totalSteps: s.total_steps,
|
|
338
|
+
browser: s.browser || 'chromium',
|
|
339
|
+
status: 'pending',
|
|
340
|
+
passed: 0, failed: 0, skipped: 0,
|
|
341
|
+
startMs: null,
|
|
342
|
+
finalDur: null,
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
buildGrid();
|
|
347
|
+
updateTopStats();
|
|
348
|
+
} catch(e) {
|
|
349
|
+
log('Failed to load execution info: ' + e.message, 'lf');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ── SSE ───────────────────────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
const es = new EventSource('/api/stream/' + execId);
|
|
356
|
+
|
|
357
|
+
es.onopen = () => {
|
|
358
|
+
log('Connected to execution stream.', 'ly');
|
|
359
|
+
if (!timerInt) timerInt = setInterval(() => {
|
|
360
|
+
// Refresh durations on running cards
|
|
361
|
+
Object.keys(scripts).forEach(uid => {
|
|
362
|
+
if (scripts[uid].status === 'running') renderCard(uid);
|
|
363
|
+
});
|
|
364
|
+
updateTopStats();
|
|
365
|
+
}, 1000);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
es.onmessage = e => {
|
|
369
|
+
let ev;
|
|
370
|
+
try { ev = JSON.parse(e.data); } catch { return; }
|
|
371
|
+
const { type, data } = ev;
|
|
372
|
+
|
|
373
|
+
if (type === 'log') { log(data.message || '', ''); return; }
|
|
374
|
+
if (type === 'queue_update') return;
|
|
375
|
+
|
|
376
|
+
// ── execution_start ──────────────────────────────────────────────────────
|
|
377
|
+
if (type === 'execution_start') {
|
|
378
|
+
startMs = Date.now();
|
|
379
|
+
if (!timerInt) timerInt = setInterval(updateTopStats, 1000);
|
|
380
|
+
setStatus('RUNNING', 'badge-running');
|
|
381
|
+
$('cancelBtn').disabled = false;
|
|
382
|
+
log(`Execution started — ${data.total_scripts} script(s) | ${data.parallel ? 'Parallel' : 'Sequential'}`, 'lr');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ── script_start ─────────────────────────────────────────────────────────
|
|
386
|
+
if (type === 'script_start') {
|
|
387
|
+
const uid = data.test_script_uid;
|
|
388
|
+
if (!scripts[uid]) {
|
|
389
|
+
// Fallback: script wasn't in info (shouldn't happen but handle gracefully)
|
|
390
|
+
scripts[uid] = {
|
|
391
|
+
name: data.test_case_name || uid,
|
|
392
|
+
totalSteps: data.total_steps || 0,
|
|
393
|
+
browser: data.browser || 'chromium',
|
|
394
|
+
status: 'running',
|
|
395
|
+
passed: 0, failed: 0, skipped: 0,
|
|
396
|
+
startMs: Date.now(), finalDur: null,
|
|
397
|
+
};
|
|
398
|
+
// Add a new card to the grid
|
|
399
|
+
const shell = document.createElement('div');
|
|
400
|
+
shell.className = 'script-card running';
|
|
401
|
+
shell.id = cardId(uid);
|
|
402
|
+
$('scriptsGrid').appendChild(shell);
|
|
403
|
+
} else {
|
|
404
|
+
scripts[uid].status = 'running';
|
|
405
|
+
scripts[uid].startMs = Date.now();
|
|
406
|
+
}
|
|
407
|
+
renderCard(uid);
|
|
408
|
+
updateTopStats();
|
|
409
|
+
log(`▶ Script started: "${esc(scripts[uid].name)}"`, 'lr');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ── step_complete — update per-script counts ─────────────────────────────
|
|
413
|
+
if (type === 'step_complete') {
|
|
414
|
+
const uid = data.test_script_uid;
|
|
415
|
+
if (!uid || !scripts[uid]) return;
|
|
416
|
+
const s = scripts[uid];
|
|
417
|
+
if (data.status === 'PASS') s.passed++;
|
|
418
|
+
else if (data.status === 'FAIL') s.failed++;
|
|
419
|
+
else if (data.status === 'SKIP') s.skipped++;
|
|
420
|
+
renderCard(uid);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── script_complete ───────────────────────────────────────────────────────
|
|
424
|
+
if (type === 'script_complete') {
|
|
425
|
+
const uid = data.test_script_uid;
|
|
426
|
+
if (!uid || !scripts[uid]) return;
|
|
427
|
+
const s = scripts[uid];
|
|
428
|
+
s.status = data.overall_status === 'PASS' ? 'passed' : 'failed';
|
|
429
|
+
s.passed = data.passed_steps;
|
|
430
|
+
s.failed = data.failed_steps;
|
|
431
|
+
s.skipped = data.skipped_steps;
|
|
432
|
+
s.finalDur = data.duration;
|
|
433
|
+
renderCard(uid);
|
|
434
|
+
updateTopStats();
|
|
435
|
+
const icon = s.status === 'passed' ? '✅' : '❌';
|
|
436
|
+
log(`${icon} Script done: "${esc(s.name)}" — ${data.overall_status} | ✅${data.passed_steps} ❌${data.failed_steps} ⏭${data.skipped_steps} (${fmtDur(data.duration)})`,
|
|
437
|
+
s.status === 'passed' ? 'lp' : 'lf');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── execution_cancelled ───────────────────────────────────────────────────
|
|
441
|
+
if (type === 'execution_cancelled') {
|
|
442
|
+
isDone = true; clearInterval(timerInt);
|
|
443
|
+
$('cancelBtn').disabled = true;
|
|
444
|
+
setStatus('CANCELLED', 'badge-idle');
|
|
445
|
+
// Mark any still-pending/running scripts as cancelled
|
|
446
|
+
Object.values(scripts).forEach(s => {
|
|
447
|
+
if (s.status === 'pending' || s.status === 'running') s.status = 'cancelled';
|
|
448
|
+
});
|
|
449
|
+
buildGrid();
|
|
450
|
+
updateTopStats();
|
|
451
|
+
updateSummary('CANCELLED');
|
|
452
|
+
log('⏹ Execution cancelled', 'ly');
|
|
453
|
+
es.close();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ── execution_complete ────────────────────────────────────────────────────
|
|
457
|
+
if (type === 'execution_complete') {
|
|
458
|
+
isDone = true; clearInterval(timerInt);
|
|
459
|
+
$('cancelBtn').disabled = true;
|
|
460
|
+
const ok = data.status === 'PASS';
|
|
461
|
+
const dur = fmtDur(data.duration || 0);
|
|
462
|
+
setStatus(ok ? '✅ PASSED' : '❌ FAILED', ok ? 'badge-pass' : 'badge-fail');
|
|
463
|
+
$('stDur').textContent = dur;
|
|
464
|
+
updateTopStats();
|
|
465
|
+
updateSummary(data.status, dur);
|
|
466
|
+
log(`🏁 Execution ${data.status} | ✅${data.passed} ❌${data.failed} scripts | ${dur}`, ok ? 'lp' : 'lf');
|
|
467
|
+
es.close();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (type === 'error') {
|
|
471
|
+
setStatus('ERROR', 'badge-fail');
|
|
472
|
+
log('ERROR: ' + data.message, 'lf');
|
|
473
|
+
isDone = true; clearInterval(timerInt);
|
|
474
|
+
$('cancelBtn').disabled = true;
|
|
475
|
+
es.close();
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
es.onerror = () => { if (!isDone) log('Stream disconnected.', 'lf'); es.close(); };
|
|
480
|
+
|
|
481
|
+
function updateSummary(status, dur) {
|
|
482
|
+
const list = Object.values(scripts);
|
|
483
|
+
const passed = list.filter(s => s.status === 'passed').length;
|
|
484
|
+
const failed = list.filter(s => s.status !== 'passed').length;
|
|
485
|
+
$('sumStatus').textContent = status;
|
|
486
|
+
$('sumStatus').style.color = status === 'PASS' ? 'var(--pass)' : status === 'CANCELLED' ? 'var(--cancel)' : 'var(--fail)';
|
|
487
|
+
$('sumPass').textContent = passed;
|
|
488
|
+
$('sumFail').textContent = failed;
|
|
489
|
+
$('sumDur').textContent = dur || $('stDur').textContent;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function cancelExecution() {
|
|
493
|
+
if (!confirm('Cancel? Running steps will finish, then remaining steps are skipped.')) return;
|
|
494
|
+
$('cancelBtn').disabled = true;
|
|
495
|
+
try {
|
|
496
|
+
const r = await fetch(`/api/cancel/${execId}`, { method: 'DELETE' });
|
|
497
|
+
const d = await r.json();
|
|
498
|
+
log('⏹ ' + d.message, 'ly');
|
|
499
|
+
} catch (e) {
|
|
500
|
+
log('Cancel failed: ' + e.message, 'lf');
|
|
501
|
+
$('cancelBtn').disabled = false;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ── Boot ──────────────────────────────────────────────────────────────────────
|
|
506
|
+
$('execLabel').textContent = 'ID: ' + execId;
|
|
507
|
+
loadInfo();
|
|
508
|
+
</script>
|
|
509
|
+
</body>
|
|
510
|
+
</html>
|