pw-automation-framework 2.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.
- package/README.md +93 -0
- package/bin/lexxit-automation-framework.js +427 -0
- package/dist/app.d.ts +2 -0
- package/dist/app.js +26 -0
- package/dist/app.js.map +1 -0
- package/dist/controllers/controller.d.ts +57 -0
- package/dist/controllers/controller.js +263 -0
- package/dist/controllers/controller.js.map +1 -0
- package/dist/core/BrowserManager.d.ts +46 -0
- package/dist/core/BrowserManager.js +377 -0
- package/dist/core/BrowserManager.js.map +1 -0
- package/dist/core/PlaywrightEngine.d.ts +16 -0
- package/dist/core/PlaywrightEngine.js +246 -0
- package/dist/core/PlaywrightEngine.js.map +1 -0
- package/dist/core/ScreenshotManager.d.ts +10 -0
- package/dist/core/ScreenshotManager.js +28 -0
- package/dist/core/ScreenshotManager.js.map +1 -0
- package/dist/core/TestData.d.ts +12 -0
- package/dist/core/TestData.js +29 -0
- package/dist/core/TestData.js.map +1 -0
- package/dist/core/TestExecutor.d.ts +16 -0
- package/dist/core/TestExecutor.js +355 -0
- package/dist/core/TestExecutor.js.map +1 -0
- package/dist/core/handlers/AllHandlers.d.ts +116 -0
- package/dist/core/handlers/AllHandlers.js +648 -0
- package/dist/core/handlers/AllHandlers.js.map +1 -0
- package/dist/core/handlers/BaseHandler.d.ts +16 -0
- package/dist/core/handlers/BaseHandler.js +27 -0
- package/dist/core/handlers/BaseHandler.js.map +1 -0
- package/dist/core/handlers/ClickHandler.d.ts +34 -0
- package/dist/core/handlers/ClickHandler.js +359 -0
- package/dist/core/handlers/ClickHandler.js.map +1 -0
- package/dist/core/handlers/CustomCodeHandler.d.ts +35 -0
- package/dist/core/handlers/CustomCodeHandler.js +102 -0
- package/dist/core/handlers/CustomCodeHandler.js.map +1 -0
- package/dist/core/handlers/DropdownHandler.d.ts +43 -0
- package/dist/core/handlers/DropdownHandler.js +304 -0
- package/dist/core/handlers/DropdownHandler.js.map +1 -0
- package/dist/core/handlers/InputHandler.d.ts +24 -0
- package/dist/core/handlers/InputHandler.js +197 -0
- package/dist/core/handlers/InputHandler.js.map +1 -0
- package/dist/core/registry/ActionRegistry.d.ts +8 -0
- package/dist/core/registry/ActionRegistry.js +35 -0
- package/dist/core/registry/ActionRegistry.js.map +1 -0
- package/dist/installer/frameworkLauncher.d.ts +31 -0
- package/dist/installer/frameworkLauncher.js +198 -0
- package/dist/installer/frameworkLauncher.js.map +1 -0
- package/dist/queue/ExecutionQueue.d.ts +52 -0
- package/dist/queue/ExecutionQueue.js +175 -0
- package/dist/queue/ExecutionQueue.js.map +1 -0
- package/dist/routes/api.routes.d.ts +2 -0
- package/dist/routes/api.routes.js +16 -0
- package/dist/routes/api.routes.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +30 -0
- package/dist/server.js.map +1 -0
- package/dist/types/types.d.ts +135 -0
- package/dist/types/types.js +4 -0
- package/dist/types/types.js.map +1 -0
- package/dist/utils/elementHighlight.d.ts +35 -0
- package/dist/utils/elementHighlight.js +136 -0
- package/dist/utils/elementHighlight.js.map +1 -0
- package/dist/utils/locatorHelper.d.ts +7 -0
- package/dist/utils/locatorHelper.js +53 -0
- package/dist/utils/locatorHelper.js.map +1 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +35 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/response.d.ts +4 -0
- package/dist/utils/response.js +25 -0
- package/dist/utils/response.js.map +1 -0
- package/dist/utils/responseFormatter.d.ts +78 -0
- package/dist/utils/responseFormatter.js +123 -0
- package/dist/utils/responseFormatter.js.map +1 -0
- package/dist/utils/sseManager.d.ts +32 -0
- package/dist/utils/sseManager.js +122 -0
- package/dist/utils/sseManager.js.map +1 -0
- package/lexxit-automation-framework-2.0.0.tgz +0 -0
- package/npmignore +5 -0
- package/package.json +36 -0
- package/scripts/postinstall.js +52 -0
- package/src/app.ts +27 -0
- package/src/controllers/controller.ts +282 -0
- package/src/core/BrowserManager.ts +398 -0
- package/src/core/PlaywrightEngine.ts +371 -0
- package/src/core/ScreenshotManager.ts +25 -0
- package/src/core/TestData.ts +25 -0
- package/src/core/TestExecutor.ts +436 -0
- package/src/core/handlers/AllHandlers.ts +626 -0
- package/src/core/handlers/BaseHandler.ts +41 -0
- package/src/core/handlers/ClickHandler.ts +482 -0
- package/src/core/handlers/CustomCodeHandler.ts +123 -0
- package/src/core/handlers/DropdownHandler.ts +438 -0
- package/src/core/handlers/InputHandler.ts +192 -0
- package/src/core/registry/ActionRegistry.ts +31 -0
- package/src/installer/frameworkLauncher.ts +242 -0
- package/src/installer/install.sh +107 -0
- package/src/public/dashboard.html +540 -0
- package/src/public/queue-monitor.html +190 -0
- package/src/queue/ExecutionQueue.ts +200 -0
- package/src/routes/api.routes.ts +16 -0
- package/src/server.ts +29 -0
- package/src/types/types.ts +169 -0
- package/src/utils/elementHighlight.ts +174 -0
- package/src/utils/locatorHelper.ts +49 -0
- package/src/utils/logger.ts +40 -0
- package/src/utils/response.ts +27 -0
- package/src/utils/responseFormatter.ts +167 -0
- package/src/utils/sseManager.ts +127 -0
- package/tsconfig.json +18 -0
- package/videos/fb1b94b6-6639-4c9a-82bb-63572606f403/page@5bd5c6c8b62baa700e9810cdd64f5c49.webm +0 -0
|
@@ -0,0 +1,540 @@
|
|
|
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">Playwright — Live Dashboard</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
|
+
const execId = location.pathname.split('/').pop();
|
|
230
|
+
const $ = id => document.getElementById(id);
|
|
231
|
+
|
|
232
|
+
let totalSteps = 0, passed = 0, failed = 0, skipped = 0;
|
|
233
|
+
let startTime = Date.now(), timerInt, isDone = false;
|
|
234
|
+
|
|
235
|
+
// Set "open in new tab" link
|
|
236
|
+
$('execLabel').textContent = 'ID: ' + execId;
|
|
237
|
+
$('newWinBtn').href = location.href;
|
|
238
|
+
|
|
239
|
+
const esc = s => String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
240
|
+
|
|
241
|
+
function setStatus(text, cls) {
|
|
242
|
+
const b = $('statusBadge');
|
|
243
|
+
b.className = 'badge ' + cls;
|
|
244
|
+
b.querySelector('span:last-child').textContent = text;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function setProgress(done, total, fillCls) {
|
|
248
|
+
const pct = total > 0 ? Math.round(done / total * 100) : 0;
|
|
249
|
+
const f = $('progFill');
|
|
250
|
+
f.style.width = pct + '%';
|
|
251
|
+
if (fillCls) { f.classList.remove('done','fail','cancelled'); f.classList.add(fillCls); }
|
|
252
|
+
$('progRight').textContent = pct + '%';
|
|
253
|
+
$('progLeft').textContent = total > 0 ? `Step ${done} of ${total}` : 'Waiting…';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function updateStats() {
|
|
257
|
+
$('sTotal').textContent = totalSteps > 0 ? totalSteps : '—';
|
|
258
|
+
$('sPass').textContent = passed;
|
|
259
|
+
$('sFail').textContent = failed;
|
|
260
|
+
$('sSkip').textContent = skipped;
|
|
261
|
+
if (!isDone) $('sDur').textContent = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function fmtDur(ms) {
|
|
265
|
+
if (!ms && ms !== 0) return '—';
|
|
266
|
+
return ms < 1000 ? ms + 'ms' : (ms/1000).toFixed(1) + 's';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function log(msg, cls='') {
|
|
270
|
+
const b = $('logBox');
|
|
271
|
+
const ts = new Date().toLocaleTimeString('en-GB');
|
|
272
|
+
const d = document.createElement('div');
|
|
273
|
+
d.className = 'll ' + cls;
|
|
274
|
+
d.textContent = `[${ts}] ${msg}`;
|
|
275
|
+
b.appendChild(d);
|
|
276
|
+
b.scrollTop = b.scrollHeight;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function openModal(src) { $('modalImg').src = src; $('modal').classList.add('open'); }
|
|
280
|
+
function closeModal() { $('modal').classList.remove('open'); }
|
|
281
|
+
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
|
|
282
|
+
|
|
283
|
+
async function cancelExecution() {
|
|
284
|
+
if (!confirm('Cancel? The current step will finish, then remaining steps are skipped.')) return;
|
|
285
|
+
$('cancelBtn').disabled = true;
|
|
286
|
+
try {
|
|
287
|
+
const r = await fetch(`/api/cancel/${execId}`, { method: 'DELETE' });
|
|
288
|
+
const d = await r.json();
|
|
289
|
+
log('⏹ ' + d.message, 'ly');
|
|
290
|
+
} catch (e) {
|
|
291
|
+
log('Cancel failed: ' + e.message, 'lf');
|
|
292
|
+
$('cancelBtn').disabled = false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ─── Step card builder ────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Create a RUNNING step card.
|
|
300
|
+
* Called on step_start.
|
|
301
|
+
*/
|
|
302
|
+
function addStepCard(data) {
|
|
303
|
+
const empty = $('emptyState');
|
|
304
|
+
if (empty) empty.remove();
|
|
305
|
+
if ($('sc-' + data.step_number)) return; // replay guard
|
|
306
|
+
|
|
307
|
+
const card = document.createElement('div');
|
|
308
|
+
card.className = 'step-card RUNNING';
|
|
309
|
+
card.id = 'sc-' + data.step_number;
|
|
310
|
+
card.innerHTML = `
|
|
311
|
+
<div class="step-row" onclick="toggleDet(${data.step_number})">
|
|
312
|
+
<div class="snum" id="sn-${data.step_number}">${data.step_number}</div>
|
|
313
|
+
<div>
|
|
314
|
+
<div class="sname">${esc(data.step_name)}</div>
|
|
315
|
+
<div class="sscript" title="${esc(data.step_script)}">${esc(data.step_script||'')}</div>
|
|
316
|
+
</div>
|
|
317
|
+
<span class="spill spill-RUNNING" id="sp-${data.step_number}">⏳ RUNNING</span>
|
|
318
|
+
<span class="sdur" id="sd-${data.step_number}">—</span>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="sdet" id="sdet-${data.step_number}"></div>`;
|
|
321
|
+
$('stepsContainer').appendChild(card);
|
|
322
|
+
card.scrollIntoView({ behavior:'smooth', block:'nearest' });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* ✅ FIX: Complete a step card — also CREATES it if it doesn't exist yet.
|
|
327
|
+
* Skipped steps arrive as step_complete WITHOUT a prior step_start,
|
|
328
|
+
* so we must handle card creation here too.
|
|
329
|
+
*/
|
|
330
|
+
function completeStepCard(data) {
|
|
331
|
+
const empty = $('emptyState');
|
|
332
|
+
if (empty) empty.remove();
|
|
333
|
+
|
|
334
|
+
let card = $('sc-' + data.step_number);
|
|
335
|
+
|
|
336
|
+
// ── Create card if it doesn't exist (SKIP steps bypass step_start) ──────
|
|
337
|
+
if (!card) {
|
|
338
|
+
card = document.createElement('div');
|
|
339
|
+
card.className = 'step-card ' + data.status;
|
|
340
|
+
card.id = 'sc-' + data.step_number;
|
|
341
|
+
card.innerHTML = `
|
|
342
|
+
<div class="step-row" onclick="toggleDet(${data.step_number})">
|
|
343
|
+
<div class="snum" id="sn-${data.step_number}">${data.step_number}</div>
|
|
344
|
+
<div>
|
|
345
|
+
<div class="sname">${esc(data.step_name)}</div>
|
|
346
|
+
<div class="sscript" title="${esc(data.step_script)}">${esc(data.step_script||'')}</div>
|
|
347
|
+
</div>
|
|
348
|
+
<span class="spill spill-${data.status}" id="sp-${data.step_number}"></span>
|
|
349
|
+
<span class="sdur" id="sd-${data.step_number}">—</span>
|
|
350
|
+
</div>
|
|
351
|
+
<div class="sdet" id="sdet-${data.step_number}"></div>`;
|
|
352
|
+
$('stepsContainer').appendChild(card);
|
|
353
|
+
// Auto-scroll for skipped steps too
|
|
354
|
+
card.scrollIntoView({ behavior:'smooth', block:'nearest' });
|
|
355
|
+
} else {
|
|
356
|
+
card.classList.remove('RUNNING');
|
|
357
|
+
card.classList.add(data.status);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const icons = { PASS:'✅', FAIL:'❌', SKIP:'⏭' };
|
|
361
|
+
const labels = { PASS:'PASS', FAIL:'FAIL', SKIP:'SKIP' };
|
|
362
|
+
$('sp-' + data.step_number).textContent = `${icons[data.status]||''} ${labels[data.status]||data.status}`;
|
|
363
|
+
$('sp-' + data.step_number).className = `spill spill-${data.status}`;
|
|
364
|
+
$('sd-' + data.step_number).textContent = data.duration_ms >= 0 ? fmtDur(data.duration_ms) : '—';
|
|
365
|
+
|
|
366
|
+
// Detail block
|
|
367
|
+
let cls = '';
|
|
368
|
+
if (data.status === 'FAIL') cls = 'is-fail';
|
|
369
|
+
if (data.status === 'SKIP') cls = data.skip_reason === 'CANCELLED' ? 'is-cancel' : 'is-skip';
|
|
370
|
+
|
|
371
|
+
const commentText = data.skip_reason
|
|
372
|
+
? `⏭ ${data.skip_reason}`
|
|
373
|
+
: (data.comments || '—');
|
|
374
|
+
|
|
375
|
+
let html = `<div class="sdet-comment ${cls}">${esc(commentText)}</div>`;
|
|
376
|
+
if (data.screenshot) {
|
|
377
|
+
html += `<img class="step-sc" src="data:image/png;base64,${data.screenshot}" onclick="openModal(this.src)" title="Click to enlarge" />`;
|
|
378
|
+
}
|
|
379
|
+
$('sdet-' + data.step_number).innerHTML = html;
|
|
380
|
+
|
|
381
|
+
// Auto-expand failures and skips
|
|
382
|
+
if (data.status === 'FAIL' || data.status === 'SKIP') {
|
|
383
|
+
$('sdet-' + data.step_number).classList.add('open');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ─── Step overview (sidebar mini-list) ───────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* ✅ FIX: addMiniStep handles ALL statuses — not just RUNNING.
|
|
391
|
+
* Skipped steps are added directly with SKIP styling.
|
|
392
|
+
*/
|
|
393
|
+
function addMiniStep(data, initialStatus) {
|
|
394
|
+
if ($('ms-' + data.step_number)) {
|
|
395
|
+
// Already exists — update status
|
|
396
|
+
updateMiniStep(data.step_number, initialStatus || 'RUNNING');
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const status = initialStatus || 'RUNNING';
|
|
400
|
+
const icons = { PASS:'✅', FAIL:'❌', SKIP:'⏭', RUNNING:'▶' };
|
|
401
|
+
const d = document.createElement('div');
|
|
402
|
+
d.className = 'mini-step ' + status;
|
|
403
|
+
d.id = 'ms-' + data.step_number;
|
|
404
|
+
d.innerHTML = `
|
|
405
|
+
<span class="mini-dot"></span>
|
|
406
|
+
<span class="mini-step-name">${data.step_number}. ${esc(data.step_name)}</span>
|
|
407
|
+
<span class="mini-badge ${status}" id="mb-${data.step_number}">${icons[status]||status}</span>`;
|
|
408
|
+
$('miniList').appendChild(d);
|
|
409
|
+
$('miniList').scrollTop = $('miniList').scrollHeight;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function updateMiniStep(num, status) {
|
|
413
|
+
const el = $('ms-' + num);
|
|
414
|
+
const mb = $('mb-' + num);
|
|
415
|
+
if (!el) return;
|
|
416
|
+
el.className = 'mini-step ' + status;
|
|
417
|
+
const icons = { PASS:'✅', FAIL:'❌', SKIP:'⏭', RUNNING:'▶' };
|
|
418
|
+
if (mb) { mb.className = `mini-badge ${status}`; mb.textContent = icons[status]||status; }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function toggleDet(num) { $('sdet-' + num)?.classList.toggle('open'); }
|
|
422
|
+
|
|
423
|
+
function showSummary(status, dur) {
|
|
424
|
+
$('sumSection').style.display = 'block';
|
|
425
|
+
$('sumStatus').textContent = status;
|
|
426
|
+
$('sumStatus').style.color = status === 'PASS' ? 'var(--pass)' : status === 'CANCELLED' ? 'var(--cancel)' : 'var(--fail)';
|
|
427
|
+
$('sumTotal').textContent = passed + failed + skipped;
|
|
428
|
+
$('sumPass').textContent = passed;
|
|
429
|
+
$('sumFail').textContent = failed;
|
|
430
|
+
$('sumSkip').textContent = skipped;
|
|
431
|
+
$('sumDur').textContent = dur || $('sDur').textContent;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ─── SSE Connection ───────────────────────────────────────────────────────────
|
|
435
|
+
const es = new EventSource('/api/stream/' + execId);
|
|
436
|
+
|
|
437
|
+
es.onopen = () => {
|
|
438
|
+
log('Connected to execution stream.', 'ly');
|
|
439
|
+
if (!timerInt) timerInt = setInterval(updateStats, 300);
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
es.onmessage = e => {
|
|
443
|
+
let ev;
|
|
444
|
+
try { ev = JSON.parse(e.data); } catch { return; }
|
|
445
|
+
const { type, data } = ev;
|
|
446
|
+
|
|
447
|
+
if (type === 'log') { log(data.message||'', ''); return; }
|
|
448
|
+
if (type === 'queue_update') return;
|
|
449
|
+
|
|
450
|
+
if (type === 'execution_start') {
|
|
451
|
+
startTime = Date.now();
|
|
452
|
+
if (!timerInt) timerInt = setInterval(updateStats, 300);
|
|
453
|
+
setStatus('RUNNING', 'badge-running');
|
|
454
|
+
$('cancelBtn').disabled = false;
|
|
455
|
+
log(`Execution started — ${data.total_scripts} script(s)`, 'lr');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (type === 'script_start') {
|
|
459
|
+
totalSteps = data.total_steps || 0;
|
|
460
|
+
$('scriptHdr').style.display = 'flex';
|
|
461
|
+
$('scriptName').textContent = data.test_case_name || data.test_script_uid;
|
|
462
|
+
$('scriptMeta').textContent = `${totalSteps} steps · ${data.browser||'chromium'} · UID: ${data.test_script_uid}`;
|
|
463
|
+
updateStats();
|
|
464
|
+
setProgress(0, totalSteps);
|
|
465
|
+
log(`Script: "${data.test_case_name}" — ${totalSteps} steps`, 'ly');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (type === 'step_start') {
|
|
469
|
+
addStepCard(data);
|
|
470
|
+
addMiniStep(data, 'RUNNING');
|
|
471
|
+
log(`▶ Step ${data.step_number}: ${data.step_name}`, 'lr');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (type === 'step_complete') {
|
|
475
|
+
// ✅ Count first, before card update
|
|
476
|
+
if (data.status === 'PASS') passed++;
|
|
477
|
+
else if (data.status === 'FAIL') failed++;
|
|
478
|
+
else if (data.status === 'SKIP') skipped++;
|
|
479
|
+
|
|
480
|
+
// ✅ completeStepCard also creates the card if missing (handles SKIP bypass)
|
|
481
|
+
completeStepCard(data);
|
|
482
|
+
|
|
483
|
+
// ✅ addMiniStep for skipped steps that never got a step_start
|
|
484
|
+
addMiniStep(data, data.status);
|
|
485
|
+
updateMiniStep(data.step_number, data.status);
|
|
486
|
+
|
|
487
|
+
setProgress(passed + failed + skipped, totalSteps);
|
|
488
|
+
updateStats();
|
|
489
|
+
|
|
490
|
+
const icon = { PASS:'✅', FAIL:'❌', SKIP:'⏭' }[data.status]||'';
|
|
491
|
+
const reason = data.skip_reason ? ` (${data.skip_reason})` : '';
|
|
492
|
+
log(`${icon} Step ${data.step_number}: ${data.step_name} — ${data.comments||''}${reason}`,
|
|
493
|
+
data.status==='PASS' ? 'lp' : data.status==='SKIP' ? 'ls' : 'lf');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (type === 'script_complete') {
|
|
497
|
+
const dur = fmtDur(data.duration_ms);
|
|
498
|
+
log(`Script done: ${data.overall_status} | ✅${data.passed_steps} ❌${data.failed_steps} ⏭${data.skipped_steps} (${dur})`, 'ly');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (type === 'execution_cancelled') {
|
|
502
|
+
isDone = true; clearInterval(timerInt);
|
|
503
|
+
$('cancelBtn').disabled = true;
|
|
504
|
+
setStatus('CANCELLED', 'badge-cancel');
|
|
505
|
+
$('progFill').style.width = '100%';
|
|
506
|
+
$('progFill').classList.add('cancelled');
|
|
507
|
+
$('progRight').textContent = '100%';
|
|
508
|
+
updateStats(); showSummary('CANCELLED');
|
|
509
|
+
log('⏹ Execution cancelled', 'ly');
|
|
510
|
+
es.close();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (type === 'execution_complete') {
|
|
514
|
+
isDone = true; clearInterval(timerInt);
|
|
515
|
+
$('cancelBtn').disabled = true;
|
|
516
|
+
const ok = data.overall_status === 'PASS';
|
|
517
|
+
const dur = fmtDur(data.duration_ms || 0);
|
|
518
|
+
setStatus(ok ? '✅ PASSED' : '❌ FAILED', ok ? 'badge-pass' : 'badge-fail');
|
|
519
|
+
$('progFill').style.width = '100%';
|
|
520
|
+
$('progFill').classList.add(ok ? 'done' : 'fail');
|
|
521
|
+
$('progRight').textContent = '100%';
|
|
522
|
+
if (totalSteps > 0) $('progLeft').textContent = `Step ${totalSteps} of ${totalSteps}`;
|
|
523
|
+
$('sDur').textContent = dur;
|
|
524
|
+
updateStats(); showSummary(data.overall_status, dur);
|
|
525
|
+
log(`🏁 ${data.overall_status} | ✅${passed} ❌${failed} ⏭${skipped} | ${dur}`, 'ly');
|
|
526
|
+
es.close();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (type === 'error') {
|
|
530
|
+
setStatus('ERROR', 'badge-fail');
|
|
531
|
+
log('ERROR: ' + data.message, 'lf');
|
|
532
|
+
$('cancelBtn').disabled = true; isDone = true; clearInterval(timerInt);
|
|
533
|
+
es.close();
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
es.onerror = () => { if (!isDone) log('Stream disconnected.', 'lf'); es.close(); };
|
|
538
|
+
</script>
|
|
539
|
+
</body>
|
|
540
|
+
</html>
|