@matware/e2e-runner 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -3
- package/bin/cli.js +161 -1
- package/package.json +5 -2
- package/src/actions.js +17 -1
- package/src/ai-generate.js +185 -0
- package/src/config.js +14 -0
- package/src/dashboard.js +546 -0
- package/src/db.js +366 -0
- package/src/index.js +5 -1
- package/src/issues.js +152 -0
- package/src/mcp-server.js +8 -337
- package/src/mcp-tools.js +527 -0
- package/src/reporter.js +73 -2
- package/src/runner.js +52 -8
- package/src/verify.js +53 -0
- package/src/websocket.js +177 -0
- package/templates/dashboard.html +1044 -0
- package/templates/e2e.config.js +3 -0
|
@@ -0,0 +1,1044 @@
|
|
|
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>E2E Runner</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
9
|
+
<style>
|
|
10
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
11
|
+
:root{
|
|
12
|
+
--bg:#090a10;--surface:#11131b;--surface2:#181b26;--surface3:#1f2333;
|
|
13
|
+
--border:#232738;--border-hi:#2d3248;
|
|
14
|
+
--text:#dfe1e8;--text2:#7c8198;--text3:#464b62;
|
|
15
|
+
--accent:#3b82f6;--accent-dim:rgba(59,130,246,.12);
|
|
16
|
+
--green:#10b981;--green-dim:rgba(16,185,129,.10);
|
|
17
|
+
--red:#ef4444;--red-dim:rgba(239,68,68,.10);
|
|
18
|
+
--amber:#f59e0b;--amber-dim:rgba(245,158,11,.10);
|
|
19
|
+
--purple:#8b5cf6;--purple-dim:rgba(139,92,246,.12);
|
|
20
|
+
--mono:'JetBrains Mono','SF Mono','Fira Code',monospace;
|
|
21
|
+
--sans:'Outfit','Inter',-apple-system,sans-serif;
|
|
22
|
+
--r:6px;
|
|
23
|
+
}
|
|
24
|
+
html{font-size:13px}
|
|
25
|
+
body{font-family:var(--mono);background:var(--bg);color:var(--text);min-height:100vh;display:flex}
|
|
26
|
+
a{color:var(--accent);text-decoration:none}
|
|
27
|
+
::selection{background:var(--accent);color:#fff}
|
|
28
|
+
|
|
29
|
+
/* ── Sidebar ── */
|
|
30
|
+
.sidebar{width:232px;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:50;overflow-y:auto}
|
|
31
|
+
.sidebar-logo{padding:20px 16px 16px;border-bottom:1px solid var(--border)}
|
|
32
|
+
.sidebar-logo h1{font-family:var(--sans);font-size:15px;font-weight:700;letter-spacing:-.02em}
|
|
33
|
+
.sidebar-logo h1 span{color:var(--accent)}
|
|
34
|
+
.sidebar-logo .ver{font-size:10px;color:var(--text3);margin-top:2px}
|
|
35
|
+
|
|
36
|
+
.sidebar-section{padding:12px 16px 8px}
|
|
37
|
+
.sidebar-section-label{font-size:9px;font-weight:600;color:var(--text3);letter-spacing:.12em;text-transform:uppercase;margin-bottom:8px}
|
|
38
|
+
.sidebar select{width:100%;padding:8px 10px;border-radius:var(--r);border:1px solid var(--border);background:var(--surface2);color:var(--text);font-family:var(--mono);font-size:12px;appearance:auto;cursor:pointer}
|
|
39
|
+
.sidebar select:focus{outline:none;border-color:var(--accent)}
|
|
40
|
+
|
|
41
|
+
.nav-item{display:flex;align-items:center;gap:10px;padding:8px 16px;font-size:12px;color:var(--text2);cursor:pointer;border-left:2px solid transparent;transition:all .15s}
|
|
42
|
+
.nav-item:hover{color:var(--text);background:var(--surface2)}
|
|
43
|
+
.nav-item.active{color:var(--accent);border-left-color:var(--accent);background:var(--accent-dim)}
|
|
44
|
+
.nav-item .icon{width:16px;text-align:center;font-style:normal}
|
|
45
|
+
.nav-item .badge{margin-left:auto;background:var(--surface3);color:var(--text2);font-size:10px;padding:1px 6px;border-radius:10px}
|
|
46
|
+
|
|
47
|
+
.pool-status{margin-top:auto;padding:16px;border-top:1px solid var(--border)}
|
|
48
|
+
.pool-dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:6px}
|
|
49
|
+
.pool-dot.on{background:var(--green);box-shadow:0 0 8px var(--green)}
|
|
50
|
+
.pool-dot.off{background:var(--red);box-shadow:0 0 8px var(--red)}
|
|
51
|
+
.pool-info{font-size:11px;color:var(--text2);line-height:1.7}
|
|
52
|
+
.pool-info strong{color:var(--text)}
|
|
53
|
+
.ws-dot{width:6px;height:6px;border-radius:50%;display:inline-block;margin-right:4px}
|
|
54
|
+
|
|
55
|
+
/* ── Main ── */
|
|
56
|
+
.main{margin-left:232px;flex:1;min-height:100vh}
|
|
57
|
+
.main-header{padding:16px 24px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;background:var(--surface);position:sticky;top:0;z-index:40}
|
|
58
|
+
.main-header .title{font-family:var(--sans);font-size:16px;font-weight:600}
|
|
59
|
+
.main-header .actions{margin-left:auto;display:flex;gap:8px}
|
|
60
|
+
.view{display:none;padding:24px}
|
|
61
|
+
.view.active{display:block}
|
|
62
|
+
|
|
63
|
+
/* ── Buttons ── */
|
|
64
|
+
.btn{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:var(--r);font-family:var(--mono);font-size:11px;font-weight:500;cursor:pointer;border:1px solid var(--border);background:var(--surface2);color:var(--text);transition:all .15s;white-space:nowrap}
|
|
65
|
+
.btn:hover{background:var(--surface3);border-color:var(--border-hi)}
|
|
66
|
+
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
|
|
67
|
+
.btn.primary:hover{background:#2563eb}
|
|
68
|
+
.btn.danger{background:var(--red-dim);border-color:rgba(239,68,68,.3);color:var(--red)}
|
|
69
|
+
.btn:disabled{opacity:.4;cursor:not-allowed}
|
|
70
|
+
.btn.sm{padding:4px 10px;font-size:10px}
|
|
71
|
+
|
|
72
|
+
/* ── Cards ── */
|
|
73
|
+
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:16px}
|
|
74
|
+
.card-label{font-size:9px;font-weight:600;color:var(--text3);letter-spacing:.1em;text-transform:uppercase;margin-bottom:10px}
|
|
75
|
+
|
|
76
|
+
/* ── Stats Row ── */
|
|
77
|
+
.stats{display:flex;gap:24px;margin-bottom:20px}
|
|
78
|
+
.stat-block{text-align:center;min-width:80px}
|
|
79
|
+
.stat-val{font-size:26px;font-weight:700}
|
|
80
|
+
.stat-lbl{font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.1em;margin-top:2px}
|
|
81
|
+
.stat-val.green{color:var(--green)}
|
|
82
|
+
.stat-val.red{color:var(--red)}
|
|
83
|
+
.stat-val.accent{color:var(--accent)}
|
|
84
|
+
.stat-val.purple{color:var(--purple)}
|
|
85
|
+
|
|
86
|
+
/* ── Tables ── */
|
|
87
|
+
.tbl-wrap{overflow-x:auto}
|
|
88
|
+
table{width:100%;border-collapse:collapse;font-size:12px}
|
|
89
|
+
th{text-align:left;font-size:9px;font-weight:600;color:var(--text3);letter-spacing:.1em;text-transform:uppercase;padding:8px 12px;border-bottom:1px solid var(--border)}
|
|
90
|
+
td{padding:8px 12px;border-bottom:1px solid var(--border)}
|
|
91
|
+
tbody tr{cursor:pointer;transition:background .1s}
|
|
92
|
+
tbody tr:hover td{background:var(--surface2)}
|
|
93
|
+
tbody tr.selected td{background:var(--accent-dim)}
|
|
94
|
+
.badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:600}
|
|
95
|
+
.badge.pass{background:var(--green-dim);color:var(--green)}
|
|
96
|
+
.badge.fail{background:var(--red-dim);color:var(--red)}
|
|
97
|
+
.badge.flaky{background:var(--amber-dim);color:var(--amber)}
|
|
98
|
+
.badge.run{background:var(--purple-dim);color:var(--purple)}
|
|
99
|
+
|
|
100
|
+
/* ── Trend Chart ── */
|
|
101
|
+
.chart{display:flex;align-items:flex-end;gap:3px;height:60px}
|
|
102
|
+
.chart-bar{flex:1;min-width:3px;max-width:16px;border-radius:2px 2px 0 0;cursor:pointer;position:relative;transition:opacity .15s}
|
|
103
|
+
.chart-bar:hover{opacity:.75}
|
|
104
|
+
.chart-bar .tip{display:none;position:absolute;bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);background:var(--surface3);border:1px solid var(--border);padding:4px 8px;border-radius:4px;font-size:10px;white-space:nowrap;z-index:10;pointer-events:none}
|
|
105
|
+
.chart-bar:hover .tip{display:block}
|
|
106
|
+
|
|
107
|
+
/* ── Suite Cards ── */
|
|
108
|
+
.suite-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}
|
|
109
|
+
.suite-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:16px;transition:border-color .15s}
|
|
110
|
+
.suite-card:hover{border-color:var(--border-hi)}
|
|
111
|
+
.suite-card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
|
|
112
|
+
.suite-card-name{font-weight:600;font-size:13px}
|
|
113
|
+
.suite-card-count{font-size:10px;color:var(--text3)}
|
|
114
|
+
.suite-card-tests{list-style:none;margin-bottom:12px}
|
|
115
|
+
.suite-card-tests li{font-size:11px;color:var(--text2);padding:2px 0;padding-left:14px;position:relative}
|
|
116
|
+
.suite-card-tests li::before{content:">";position:absolute;left:0;color:var(--text3)}
|
|
117
|
+
|
|
118
|
+
/* ── Live Execution ── */
|
|
119
|
+
.live-panel{display:none;background:var(--surface);border:1px solid var(--purple);border-radius:var(--r);overflow:hidden;animation:fadeSlide .3s ease}
|
|
120
|
+
.live-panel.active{display:block}
|
|
121
|
+
@keyframes fadeSlide{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
|
|
122
|
+
.live-header{padding:14px 16px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--border);background:var(--purple-dim)}
|
|
123
|
+
.live-header .label{font-weight:600;color:var(--purple);font-size:12px;display:flex;align-items:center;gap:8px}
|
|
124
|
+
.live-header .label .dot{width:8px;height:8px;border-radius:50%;background:var(--purple);animation:pulse 1.5s infinite}
|
|
125
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
|
126
|
+
.live-project{display:flex;align-items:center;padding:2px 10px;background:rgba(255,255,255,.05);border-radius:4px;border:1px solid var(--border)}
|
|
127
|
+
.live-stats{display:flex;gap:16px;font-size:11px;color:var(--text2);margin-left:auto}
|
|
128
|
+
.live-stats span strong{color:var(--text)}
|
|
129
|
+
.live-progress{height:3px;background:var(--surface3)}
|
|
130
|
+
.live-progress-fill{height:100%;background:var(--purple);transition:width .4s;border-radius:0 2px 2px 0}
|
|
131
|
+
.live-tests{padding:12px 16px;display:flex;flex-direction:column;gap:2px;max-height:calc(100vh - 200px);overflow-y:auto}
|
|
132
|
+
.live-test{padding:10px 12px;border-radius:var(--r);border-left:3px solid var(--text3);background:var(--surface2);font-size:11px;transition:border-color .2s,padding .25s,max-height .35s cubic-bezier(.4,0,.2,1)}
|
|
133
|
+
.live-test.running{border-left-color:var(--purple)}
|
|
134
|
+
.live-test.passed{border-left-color:var(--green)}
|
|
135
|
+
.live-test.failed{border-left-color:var(--red)}
|
|
136
|
+
.live-test.collapsed{cursor:pointer;padding:6px 12px}
|
|
137
|
+
.live-test.collapsed:hover{background:var(--surface3)}
|
|
138
|
+
.live-test.collapsed .lt-meta,.live-test.collapsed .lt-actions,.live-test.collapsed .lt-screenshots{display:none}
|
|
139
|
+
.live-test.collapsed .lt-name{margin-bottom:0}
|
|
140
|
+
.live-test.collapsed .lt-summary{display:flex}
|
|
141
|
+
.live-test .lt-name{font-weight:600;margin-bottom:4px;display:flex;align-items:center;gap:6px}
|
|
142
|
+
.live-test .lt-summary{display:none;align-items:center;gap:8px;margin-left:auto;font-size:10px;color:var(--text3);font-family:var(--mono)}
|
|
143
|
+
.live-test .lt-summary .lt-dur{color:var(--text2)}
|
|
144
|
+
.live-test .lt-summary .lt-expand{color:var(--purple);font-size:9px;opacity:.6}
|
|
145
|
+
.live-test .lt-meta{color:var(--text2);font-size:10px;margin-bottom:6px}
|
|
146
|
+
.live-test .lt-icon{font-size:12px}
|
|
147
|
+
.lt-actions{max-height:180px;overflow-y:auto;border-top:1px solid var(--border);padding-top:6px;margin-top:4px}
|
|
148
|
+
.lt-step{display:flex;align-items:flex-start;gap:6px;padding:2px 0;font-size:10px;font-family:var(--mono);line-height:1.4}
|
|
149
|
+
.lt-step .step-icon{flex-shrink:0;width:14px;text-align:center}
|
|
150
|
+
.lt-step .step-icon.ok{color:var(--green)}
|
|
151
|
+
.lt-step .step-icon.fail{color:var(--red)}
|
|
152
|
+
.lt-step .step-icon.run{color:var(--purple)}
|
|
153
|
+
.lt-step .step-type{color:var(--purple);font-weight:600;flex-shrink:0}
|
|
154
|
+
.lt-step .step-detail{color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
|
|
155
|
+
.lt-step .step-dur{color:var(--text3);flex-shrink:0;margin-left:auto}
|
|
156
|
+
.lt-screenshots{border-top:1px solid var(--border);margin-top:6px;padding-top:6px}
|
|
157
|
+
.lt-screenshots-toggle{display:flex;align-items:center;gap:6px;cursor:pointer;font-size:10px;color:var(--text3);font-family:var(--mono);padding:2px 0;user-select:none}
|
|
158
|
+
.lt-screenshots-toggle:hover{color:var(--text)}
|
|
159
|
+
.lt-screenshots-toggle .ss-arrow{transition:transform .2s;font-size:8px}
|
|
160
|
+
.lt-screenshots-toggle.open .ss-arrow{transform:rotate(90deg)}
|
|
161
|
+
.lt-screenshots-grid{display:none;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));gap:6px;padding-top:6px}
|
|
162
|
+
.lt-screenshots-toggle.open+.lt-screenshots-grid{display:grid}
|
|
163
|
+
.lt-ss-thumb{position:relative;border-radius:4px;overflow:hidden;border:1px solid var(--border);cursor:pointer;aspect-ratio:16/10;background:var(--surface2)}
|
|
164
|
+
.lt-ss-thumb img{width:100%;height:100%;object-fit:cover;display:block}
|
|
165
|
+
.lt-ss-thumb:hover{border-color:var(--purple);box-shadow:0 0 0 1px var(--purple)}
|
|
166
|
+
.lt-ss-label{font-size:8px;color:var(--text3);font-family:var(--mono);text-align:center;padding:2px 2px 0;display:flex;align-items:center;justify-content:center;gap:3px;flex-wrap:wrap}
|
|
167
|
+
.lr-section-header{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;margin:6px 0 2px;border-radius:6px;font-family:var(--mono);font-size:12px;font-weight:700;border-left:3px solid var(--purple)}
|
|
168
|
+
.lr-section-header.running{border-color:var(--purple);background:rgba(139,92,246,.08);color:var(--purple)}
|
|
169
|
+
.lr-section-header.pass{border-color:var(--green);background:rgba(52,211,153,.08);color:var(--green)}
|
|
170
|
+
.lr-section-header.fail{border-color:var(--red);background:rgba(248,113,113,.08);color:var(--red)}
|
|
171
|
+
.lr-section-stats{display:flex;align-items:center;gap:4px;font-size:10px;color:var(--text2);font-weight:400}
|
|
172
|
+
.lr-project-name{letter-spacing:.5px}
|
|
173
|
+
.lr-test-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:8px;padding:4px 0 8px}
|
|
174
|
+
.live-done{background:var(--green-dim);color:var(--green);text-align:center;padding:10px;font-weight:600;font-size:12px}
|
|
175
|
+
.live-done.has-failures{background:var(--red-dim);color:var(--red)}
|
|
176
|
+
.live-close{padding:4px 10px;font-size:10px;background:transparent;border:1px solid var(--border);border-radius:var(--r);color:var(--text2);cursor:pointer}
|
|
177
|
+
.live-close:hover{color:var(--text);border-color:var(--border-hi)}
|
|
178
|
+
|
|
179
|
+
.live-nav-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--purple);animation:pulse 1.5s infinite}
|
|
180
|
+
.spinner{display:inline-block;width:12px;height:12px;border:2px solid var(--border);border-top-color:var(--purple);border-radius:50%;animation:spin .6s linear infinite;vertical-align:middle}
|
|
181
|
+
.spinner-small{display:inline-block;width:8px;height:8px;border:1.5px solid var(--border);border-top-color:var(--purple);border-radius:50%;animation:spin .6s linear infinite;vertical-align:middle}
|
|
182
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
183
|
+
|
|
184
|
+
/* ── Screenshots ── */
|
|
185
|
+
.gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}
|
|
186
|
+
.gallery-item{background:var(--surface2);border:1px solid var(--border);border-radius:var(--r);overflow:hidden;cursor:pointer;transition:border-color .15s}
|
|
187
|
+
.gallery-item:hover{border-color:var(--accent)}
|
|
188
|
+
.gallery-item img{width:100%;height:150px;object-fit:cover;display:block}
|
|
189
|
+
.gallery-item .cap{padding:6px 10px;font-size:10px;color:var(--text2);display:flex;align-items:center;gap:4px}
|
|
190
|
+
.gallery-item .cap .cap-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
|
|
191
|
+
.gallery-item .cap .ss-hash{flex-shrink:0}
|
|
192
|
+
|
|
193
|
+
/* ── Inline Run Detail ── */
|
|
194
|
+
.run-detail-row td{padding:0!important;border-bottom:2px solid var(--border-hi)}
|
|
195
|
+
.run-detail-row:hover td{background:transparent!important}
|
|
196
|
+
.rd-wrap{overflow:hidden;transition:max-height .35s cubic-bezier(.4,0,.2,1);max-height:0}
|
|
197
|
+
.rd-wrap.open{max-height:2000px}
|
|
198
|
+
.rd-inner{padding:16px 20px;background:var(--bg);border-left:3px solid var(--purple);margin:0 8px 8px}
|
|
199
|
+
.rd-test{margin-bottom:14px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r);overflow:hidden}
|
|
200
|
+
.rd-test:last-child{margin-bottom:0}
|
|
201
|
+
.rd-test-head{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--border);background:var(--surface2)}
|
|
202
|
+
.rd-test-name{font-weight:600;font-size:12px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
203
|
+
.rd-test-dur{font-size:11px;color:var(--text2);flex-shrink:0}
|
|
204
|
+
.rd-test-body{padding:10px 14px}
|
|
205
|
+
.rd-error-msg{font-size:11px;color:var(--red);padding:8px 12px;background:var(--red-dim);border-radius:var(--r);margin-bottom:10px;word-break:break-all;border-left:3px solid var(--red)}
|
|
206
|
+
.rd-shots{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
|
|
207
|
+
.rd-shot{width:120px;border-radius:4px;overflow:hidden;border:1px solid var(--border);cursor:pointer;transition:border-color .15s,transform .15s}
|
|
208
|
+
.rd-shot:hover{border-color:var(--accent);transform:scale(1.03)}
|
|
209
|
+
.rd-shot img{width:100%;height:72px;object-fit:cover;display:block}
|
|
210
|
+
.rd-shot .rd-shot-cap{font-size:9px;color:var(--text3);padding:4px 6px;background:var(--surface2)}
|
|
211
|
+
.rd-shot .rd-shot-cap .cap-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
212
|
+
.rd-shot .rd-shot-cap .ss-hash{margin-top:3px}
|
|
213
|
+
.rd-shot.err-shot{border-color:rgba(239,68,68,.4)}
|
|
214
|
+
.rd-shot.err-shot .rd-shot-cap{color:var(--red)}
|
|
215
|
+
.rd-logs{margin-top:10px}
|
|
216
|
+
.rd-log-label{font-size:9px;font-weight:600;color:var(--text3);letter-spacing:.08em;text-transform:uppercase;margin-bottom:4px}
|
|
217
|
+
.rd-log-item{font-size:10px;padding:3px 8px;border-left:2px solid var(--border);margin-bottom:2px;color:var(--text2);word-break:break-all}
|
|
218
|
+
.rd-log-item.error{border-left-color:var(--red);color:var(--red)}
|
|
219
|
+
.rd-log-item.warning,.rd-log-item.warn{border-left-color:var(--amber);color:var(--amber)}
|
|
220
|
+
.rd-retries{font-size:10px;color:var(--amber);margin-bottom:6px}
|
|
221
|
+
.rd-summary{display:flex;gap:16px;margin-bottom:14px;padding:10px 14px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r);font-size:11px;align-items:center}
|
|
222
|
+
.rd-summary .rd-s-label{color:var(--text3);font-size:9px;text-transform:uppercase;letter-spacing:.08em}
|
|
223
|
+
.rd-summary .rd-s-val{font-weight:700;font-size:16px;margin-top:2px}
|
|
224
|
+
tr.expanded td{background:var(--surface2)!important}
|
|
225
|
+
tr.expanded td:first-child{position:relative}
|
|
226
|
+
tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--purple)}
|
|
227
|
+
|
|
228
|
+
/* ── Empty ── */
|
|
229
|
+
.empty{text-align:center;padding:48px 24px;color:var(--text3)}
|
|
230
|
+
.empty-icon{font-size:36px;margin-bottom:8px;opacity:.5}
|
|
231
|
+
|
|
232
|
+
/* ── Modal ── */
|
|
233
|
+
.modal{position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:200;display:none;align-items:center;justify-content:center;padding:24px;cursor:pointer}
|
|
234
|
+
.modal.open{display:flex}
|
|
235
|
+
.modal img{max-width:100%;max-height:90vh;border-radius:var(--r);cursor:default}
|
|
236
|
+
|
|
237
|
+
/* ── Screenshot Hash Badge ── */
|
|
238
|
+
.ss-hash{display:inline-flex;align-items:center;gap:4px;padding:2px 7px;border-radius:10px;font-family:var(--mono);font-size:9px;font-weight:500;background:var(--surface3);border:1px solid var(--border);color:var(--text2);cursor:pointer;transition:all .15s;user-select:none;white-space:nowrap;vertical-align:middle}
|
|
239
|
+
.ss-hash:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-dim)}
|
|
240
|
+
.ss-hash.copied{border-color:var(--green);color:var(--green);background:var(--green-dim)}
|
|
241
|
+
.ss-hash .ss-icon{font-size:10px;line-height:1}
|
|
242
|
+
|
|
243
|
+
/* ── Responsive ── */
|
|
244
|
+
@media(max-width:768px){
|
|
245
|
+
.sidebar{width:60px}.sidebar-logo h1,.sidebar-section-label,.nav-item span:not(.icon),.pool-info,.sidebar select,.sidebar-logo .ver{display:none}
|
|
246
|
+
.nav-item{justify-content:center;padding:12px}.nav-item .icon{width:auto}
|
|
247
|
+
.main{margin-left:60px}
|
|
248
|
+
.suite-grid,.gallery{grid-template-columns:1fr}
|
|
249
|
+
.lr-test-grid{grid-template-columns:1fr}
|
|
250
|
+
}
|
|
251
|
+
</style>
|
|
252
|
+
</head>
|
|
253
|
+
<body>
|
|
254
|
+
|
|
255
|
+
<aside class="sidebar">
|
|
256
|
+
<div class="sidebar-logo">
|
|
257
|
+
<h1><span>E2E</span> Runner</h1>
|
|
258
|
+
<div class="ver">Dashboard</div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div class="sidebar-section">
|
|
262
|
+
<div class="sidebar-section-label">Project</div>
|
|
263
|
+
<select id="projectSelect">
|
|
264
|
+
<option value="">All Projects</option>
|
|
265
|
+
</select>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<div class="sidebar-section">
|
|
269
|
+
<div class="sidebar-section-label">Navigation</div>
|
|
270
|
+
</div>
|
|
271
|
+
<div class="nav-item" data-view="live" id="navLive" style="display:none">
|
|
272
|
+
<i class="icon"><span class="live-nav-dot"></span></i><span>Live</span><span class="badge" id="liveBadge" style="background:var(--purple-dim);color:var(--purple)">0</span>
|
|
273
|
+
</div>
|
|
274
|
+
<div class="nav-item active" data-view="suites">
|
|
275
|
+
<i class="icon">▷</i><span>Suites</span>
|
|
276
|
+
</div>
|
|
277
|
+
<div class="nav-item" data-view="runs">
|
|
278
|
+
<i class="icon">☰</i><span>Runs</span>
|
|
279
|
+
</div>
|
|
280
|
+
<div class="nav-item" data-view="screenshots">
|
|
281
|
+
<i class="icon">▣</i><span>Screenshots</span>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div class="pool-status" id="poolStatus">
|
|
285
|
+
<div class="pool-info">
|
|
286
|
+
<span class="pool-dot off" id="poolDot"></span>
|
|
287
|
+
<strong>Pool</strong> <span id="poolLabel">--</span>
|
|
288
|
+
</div>
|
|
289
|
+
<div class="pool-info">Sessions: <strong id="poolSessions">-/-</strong></div>
|
|
290
|
+
<div class="pool-info" style="margin-top:6px">
|
|
291
|
+
<span class="ws-dot" id="wsDot" style="background:var(--red)"></span>
|
|
292
|
+
<span id="wsLabel" style="font-size:10px;color:var(--text3)">ws: connecting</span>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
</aside>
|
|
296
|
+
|
|
297
|
+
<div class="main">
|
|
298
|
+
|
|
299
|
+
<!-- Live View -->
|
|
300
|
+
<div class="view" id="view-live">
|
|
301
|
+
<div class="live-panel active" id="livePanel">
|
|
302
|
+
<div class="live-header">
|
|
303
|
+
<div class="label"><span class="dot"></span> RUNNING</div>
|
|
304
|
+
<div class="live-project" id="liveProject" style="display:none">
|
|
305
|
+
<span style="color:var(--text3);font-size:11px">Project:</span>
|
|
306
|
+
<strong id="liveProjectName" style="font-size:12px;color:var(--text);margin-left:4px"></strong>
|
|
307
|
+
<span id="liveProjectCwd" style="font-size:10px;color:var(--text3);margin-left:8px;font-family:var(--mono);opacity:.7"></span>
|
|
308
|
+
</div>
|
|
309
|
+
<div class="live-stats">
|
|
310
|
+
<span>Total: <strong id="liveTotal">0</strong></span>
|
|
311
|
+
<span style="color:var(--green)">Pass: <strong id="livePass">0</strong></span>
|
|
312
|
+
<span style="color:var(--red)">Fail: <strong id="liveFail">0</strong></span>
|
|
313
|
+
<span style="color:var(--purple)">Active: <strong id="liveActive">0</strong></span>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="live-progress"><div class="live-progress-fill" id="liveProgressFill" style="width:0"></div></div>
|
|
317
|
+
<div class="live-tests" id="liveTests"></div>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="empty" id="liveEmpty">
|
|
320
|
+
<div class="empty-icon" style="font-size:48px;opacity:.3">●</div>
|
|
321
|
+
<p>No tests running. Start a test from the Suites view or another console.</p>
|
|
322
|
+
<p style="margin-top:8px;font-size:11px;color:var(--text3)">This view activates automatically when tests are detected.</p>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<!-- Suites View -->
|
|
327
|
+
<div class="view active" id="view-suites">
|
|
328
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
|
329
|
+
<div style="font-family:var(--sans);font-size:16px;font-weight:600">Test Suites</div>
|
|
330
|
+
<button class="btn primary" id="btnRunAll">Run All Tests</button>
|
|
331
|
+
</div>
|
|
332
|
+
<div class="suite-grid" id="suiteGrid"></div>
|
|
333
|
+
<div class="empty" id="suitesEmpty" style="display:none">
|
|
334
|
+
<div class="empty-icon">▷</div>
|
|
335
|
+
<p>No test suites found.</p>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<!-- Runs View -->
|
|
340
|
+
<div class="view" id="view-runs">
|
|
341
|
+
<div style="font-family:var(--sans);font-size:16px;font-weight:600;margin-bottom:20px">Run History</div>
|
|
342
|
+
<div class="card">
|
|
343
|
+
<div class="card-label">Pass Rate Trend</div>
|
|
344
|
+
<div class="chart" id="trendChart"></div>
|
|
345
|
+
</div>
|
|
346
|
+
<div class="card" style="padding:0">
|
|
347
|
+
<div class="tbl-wrap">
|
|
348
|
+
<table>
|
|
349
|
+
<thead id="runsHead"><tr></tr></thead>
|
|
350
|
+
<tbody id="runsBody"></tbody>
|
|
351
|
+
</table>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
<div class="empty" id="runsEmpty" style="display:none">
|
|
355
|
+
<div class="empty-icon">☰</div>
|
|
356
|
+
<p>No runs recorded yet.</p>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
<!-- Screenshots View -->
|
|
361
|
+
<div class="view" id="view-screenshots">
|
|
362
|
+
<div style="font-family:var(--sans);font-size:16px;font-weight:600;margin-bottom:20px">Screenshots</div>
|
|
363
|
+
<div class="gallery" id="screenshotGallery"></div>
|
|
364
|
+
<div class="empty" id="screenshotsEmpty" style="display:none">
|
|
365
|
+
<div class="empty-icon">▣</div>
|
|
366
|
+
<p>Select a project to view screenshots.</p>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
<div class="modal" id="modal"><img id="modalImg" src="" alt=""></div>
|
|
373
|
+
|
|
374
|
+
<script>
|
|
375
|
+
(function(){
|
|
376
|
+
'use strict';
|
|
377
|
+
var $=function(s){return document.querySelector(s)};
|
|
378
|
+
var $$=function(s){return document.querySelectorAll(s)};
|
|
379
|
+
|
|
380
|
+
function el(tag,a,ch){
|
|
381
|
+
var e=document.createElement(tag);
|
|
382
|
+
if(a)Object.keys(a).forEach(function(k){
|
|
383
|
+
if(k==='className')e.className=a[k];
|
|
384
|
+
else if(k==='style')e.style.cssText=a[k];
|
|
385
|
+
else if(k.indexOf('on')===0)e.addEventListener(k.slice(2),a[k]);
|
|
386
|
+
else e.setAttribute(k,a[k]);
|
|
387
|
+
});
|
|
388
|
+
if(typeof ch==='string')e.textContent=ch;
|
|
389
|
+
else if(Array.isArray(ch))ch.forEach(function(c){if(c)e.appendChild(c)});
|
|
390
|
+
return e;
|
|
391
|
+
}
|
|
392
|
+
function css(n){return n.replace(/[^a-zA-Z0-9\-_]/g,'_')}
|
|
393
|
+
function dur(ms){return ms>=1000?(ms/1000).toFixed(1)+'s':ms+'ms'}
|
|
394
|
+
function fdate(iso){return iso?new Date(iso).toLocaleString():'--'}
|
|
395
|
+
|
|
396
|
+
/* ── Screenshot hash helpers ── */
|
|
397
|
+
var ssHashCache={};
|
|
398
|
+
async function ssHash(filePath){
|
|
399
|
+
if(ssHashCache[filePath])return ssHashCache[filePath];
|
|
400
|
+
var data=new TextEncoder().encode(filePath);
|
|
401
|
+
var buf=await crypto.subtle.digest('SHA-256',data);
|
|
402
|
+
var hex=Array.from(new Uint8Array(buf)).map(function(b){return b.toString(16).padStart(2,'0')}).join('');
|
|
403
|
+
var h=hex.slice(0,8);
|
|
404
|
+
ssHashCache[filePath]=h;
|
|
405
|
+
return h;
|
|
406
|
+
}
|
|
407
|
+
function ssHashSync(filePath){return ssHashCache[filePath]||null}
|
|
408
|
+
function copyHash(hash,badge){
|
|
409
|
+
navigator.clipboard.writeText('ss:'+hash).then(function(){
|
|
410
|
+
badge.classList.add('copied');
|
|
411
|
+
setTimeout(function(){badge.classList.remove('copied')},1200);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
function createHashBadge(hash){
|
|
415
|
+
var badge=el('span',{className:'ss-hash',onclick:function(e){e.stopPropagation();copyHash(hash,badge)}},[
|
|
416
|
+
el('span',{className:'ss-icon'},'\u2318'),
|
|
417
|
+
document.createTextNode('ss:'+hash)
|
|
418
|
+
]);
|
|
419
|
+
return badge;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/* ── State ── */
|
|
423
|
+
var S={
|
|
424
|
+
ws:null,project:null,view:'suites',selectedRun:null,
|
|
425
|
+
liveRuns:{},liveExpanded:new Set(),liveSSOpen:new Set()
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
/* ── Navigation ── */
|
|
429
|
+
$$('.nav-item').forEach(function(n){
|
|
430
|
+
n.addEventListener('click',function(){
|
|
431
|
+
$$('.nav-item').forEach(function(x){x.classList.remove('active')});
|
|
432
|
+
n.classList.add('active');
|
|
433
|
+
S.view=n.dataset.view;
|
|
434
|
+
$$('.view').forEach(function(v){v.classList.remove('active')});
|
|
435
|
+
$('#view-'+S.view).classList.add('active');
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
function showView(v){
|
|
439
|
+
S.view=v;
|
|
440
|
+
$$('.nav-item').forEach(function(n){n.classList.toggle('active',n.dataset.view===v)});
|
|
441
|
+
$$('.view').forEach(function(x){x.classList.remove('active')});
|
|
442
|
+
$('#view-'+v).classList.add('active');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* ── WebSocket ── */
|
|
446
|
+
function connectWS(){
|
|
447
|
+
var proto=location.protocol==='https:'?'wss:':'ws:';
|
|
448
|
+
S.ws=new WebSocket(proto+'//'+location.host);
|
|
449
|
+
S.ws.onopen=function(){$('#wsDot').style.background='var(--green)';$('#wsLabel').textContent='ws: connected';$('#wsLabel').style.color='var(--green)'};
|
|
450
|
+
S.ws.onclose=function(){$('#wsDot').style.background='var(--red)';$('#wsLabel').textContent='ws: disconnected';$('#wsLabel').style.color='var(--text3)';setTimeout(connectWS,3000)};
|
|
451
|
+
S.ws.onerror=function(){};
|
|
452
|
+
S.ws.onmessage=function(e){try{handleWS(JSON.parse(e.data))}catch(x){}};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function getLiveRun(m){
|
|
456
|
+
var rid=m.runId;if(!rid)return null;
|
|
457
|
+
if(!S.liveRuns[rid])S.liveRuns[rid]={on:true,done:false,total:0,completed:0,passed:0,failed:0,active:0,tests:{},project:m.project||null,cwd:m.cwd||null,runId:rid,_lastEvent:Date.now()};
|
|
458
|
+
S.liveRuns[rid]._lastEvent=Date.now();
|
|
459
|
+
return S.liveRuns[rid];
|
|
460
|
+
}
|
|
461
|
+
function anyLiveRunning(){for(var k in S.liveRuns)if(S.liveRuns[k].on)return true;return false}
|
|
462
|
+
|
|
463
|
+
/* Staleness guard: if a live run gets no events for 15s and all tests are complete, auto-finish it */
|
|
464
|
+
setInterval(function(){
|
|
465
|
+
var changed=false;
|
|
466
|
+
for(var k in S.liveRuns){
|
|
467
|
+
var r=S.liveRuns[k];
|
|
468
|
+
if(r.on&&!r.done&&Date.now()-r._lastEvent>15000){
|
|
469
|
+
if(r.completed>=r.total&&r.total>0){r.on=false;r.done=true;r.active=0;changed=true}
|
|
470
|
+
else if(Date.now()-r._lastEvent>30000){r.on=false;r.done=true;r.stale=true;r.active=0;changed=true}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if(changed)renderLive();
|
|
474
|
+
},5000);
|
|
475
|
+
|
|
476
|
+
function handleWS(m){
|
|
477
|
+
switch(m.event){
|
|
478
|
+
case 'pool:status':renderPool(m.data);break;
|
|
479
|
+
case 'run:start':
|
|
480
|
+
var r=getLiveRun(m);
|
|
481
|
+
r.total=m.total;r.on=true;r.done=false;
|
|
482
|
+
S.liveExpanded=new Set();S.liveSSOpen=new Set();
|
|
483
|
+
showView('live');renderLive();break;
|
|
484
|
+
case 'test:start':
|
|
485
|
+
var r=getLiveRun(m);if(!r)break;
|
|
486
|
+
r.active=m.activeCount;
|
|
487
|
+
r.tests[m.name]={status:'running',actions:0,totalActions:0,error:null,actionLog:[],screenshots:[]};
|
|
488
|
+
renderLive();break;
|
|
489
|
+
case 'test:action':
|
|
490
|
+
var r=getLiveRun(m);if(!r||!r.tests[m.name])break;
|
|
491
|
+
var t=r.tests[m.name];
|
|
492
|
+
t.actions=m.actionIndex+1;t.totalActions=m.totalActions;t.actionType=m.action.type;
|
|
493
|
+
t.actionLog.push({type:m.action.type,selector:m.action.selector||null,value:m.action.value||null,text:m.action.text||null,success:m.success,duration:m.duration,error:m.error||null});
|
|
494
|
+
if(m.screenshotPath)t.screenshots.push(m.screenshotPath);
|
|
495
|
+
renderLive();break;
|
|
496
|
+
case 'test:retry':
|
|
497
|
+
var r=getLiveRun(m);if(!r||!r.tests[m.name])break;
|
|
498
|
+
r.tests[m.name].retry=m.attempt+'/'+m.maxAttempts;
|
|
499
|
+
renderLive();break;
|
|
500
|
+
case 'test:complete':
|
|
501
|
+
var r=getLiveRun(m);if(!r)break;
|
|
502
|
+
r.completed++;
|
|
503
|
+
if(m.success){r.passed++;if(r.tests[m.name])r.tests[m.name].status='passed'}
|
|
504
|
+
else{r.failed++;if(r.tests[m.name]){r.tests[m.name].status='failed';r.tests[m.name].error=m.error}}
|
|
505
|
+
if(r.tests[m.name]){
|
|
506
|
+
r.tests[m.name].duration=m.duration;
|
|
507
|
+
if(m.screenshots&&m.screenshots.length)r.tests[m.name].screenshots=m.screenshots;
|
|
508
|
+
if(m.errorScreenshot)r.tests[m.name].errorScreenshot=m.errorScreenshot;
|
|
509
|
+
}
|
|
510
|
+
r.active=Math.max(0,r.active-1);
|
|
511
|
+
renderLive();break;
|
|
512
|
+
case 'run:complete':
|
|
513
|
+
var r=getLiveRun(m);if(r){r.on=false;r.done=true;r.active=0}
|
|
514
|
+
renderLive();refreshRuns();refreshProjects();break;
|
|
515
|
+
case 'run:error':
|
|
516
|
+
var r=getLiveRun(m);if(r){r.on=false;r.done=true;r.tests.__error={status:'failed',error:m.error}}
|
|
517
|
+
renderLive();break;
|
|
518
|
+
case 'db:updated':
|
|
519
|
+
refreshRuns();refreshProjects();refreshScreenshots();break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/* ── API ── */
|
|
524
|
+
function api(p){return fetch(p).then(function(r){return r.json()})}
|
|
525
|
+
function triggerRun(suite,projectId){
|
|
526
|
+
if(anyLiveRunning())return;
|
|
527
|
+
var body={};
|
|
528
|
+
if(suite)body.suite=suite;
|
|
529
|
+
if(projectId)body.projectId=projectId;
|
|
530
|
+
else if(S.project)body.projectId=S.project;
|
|
531
|
+
fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/* ── Pool ── */
|
|
535
|
+
function renderPool(d){
|
|
536
|
+
if(!d)return;
|
|
537
|
+
$('#poolDot').className='pool-dot '+(d.error||!d.available?'off':'on');
|
|
538
|
+
$('#poolLabel').textContent=d.error?'offline':d.available?'ready':'busy';
|
|
539
|
+
$('#poolSessions').textContent=(d.running||0)+'/'+(d.maxConcurrent||0);
|
|
540
|
+
}
|
|
541
|
+
function refreshStatus(){api('/api/status').then(function(d){renderPool(d.pool)}).catch(function(){})}
|
|
542
|
+
|
|
543
|
+
/* ── Projects ── */
|
|
544
|
+
function refreshProjects(){
|
|
545
|
+
api('/api/db/projects').then(function(projects){
|
|
546
|
+
var sel=$('#projectSelect'),prev=sel.value;
|
|
547
|
+
while(sel.options.length>1)sel.remove(1);
|
|
548
|
+
if(Array.isArray(projects))projects.forEach(function(p){
|
|
549
|
+
var o=document.createElement('option');o.value=p.id;o.textContent=p.name;sel.appendChild(o);
|
|
550
|
+
});
|
|
551
|
+
sel.value=prev||'';
|
|
552
|
+
}).catch(function(){});
|
|
553
|
+
}
|
|
554
|
+
$('#projectSelect').addEventListener('change',function(){
|
|
555
|
+
S.project=this.value?parseInt(this.value,10):null;
|
|
556
|
+
S.selectedRun=null;
|
|
557
|
+
refreshRuns();refreshSuites();refreshScreenshots();
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
/* ── Suites ── */
|
|
561
|
+
function refreshSuites(){
|
|
562
|
+
var grid=$('#suiteGrid'),empty=$('#suitesEmpty');
|
|
563
|
+
grid.textContent='';
|
|
564
|
+
|
|
565
|
+
if(S.project){
|
|
566
|
+
// Single project — fetch its suites
|
|
567
|
+
api('/api/db/projects/'+S.project+'/suites').then(function(suites){
|
|
568
|
+
if(!Array.isArray(suites)||suites.length===0){empty.style.display='block';empty.querySelector('p').textContent='No test suites found for this project.';return}
|
|
569
|
+
empty.style.display='none';
|
|
570
|
+
renderSuiteCards(grid,suites,S.project);
|
|
571
|
+
}).catch(function(){});
|
|
572
|
+
} else {
|
|
573
|
+
// All projects — fetch each project's suites
|
|
574
|
+
api('/api/db/projects').then(function(projects){
|
|
575
|
+
if(!Array.isArray(projects)||projects.length===0){empty.style.display='block';empty.querySelector('p').textContent='No projects registered yet.';return}
|
|
576
|
+
var loaded=0,hasAny=false;
|
|
577
|
+
projects.forEach(function(p){
|
|
578
|
+
api('/api/db/projects/'+p.id+'/suites').then(function(suites){
|
|
579
|
+
loaded++;
|
|
580
|
+
if(Array.isArray(suites)&&suites.length>0){
|
|
581
|
+
hasAny=true;
|
|
582
|
+
var label=el('div',{style:'grid-column:1/-1;font-family:var(--sans);font-size:13px;font-weight:600;margin-top:'+(grid.children.length?'16':'0')+'px;padding-bottom:6px;border-bottom:1px solid var(--border);color:var(--text2)'},p.name);
|
|
583
|
+
grid.appendChild(label);
|
|
584
|
+
renderSuiteCards(grid,suites,p.id);
|
|
585
|
+
}
|
|
586
|
+
if(loaded===projects.length&&!hasAny){empty.style.display='block';empty.querySelector('p').textContent='No test suites found.'}
|
|
587
|
+
}).catch(function(){loaded++;});
|
|
588
|
+
});
|
|
589
|
+
}).catch(function(){});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function renderSuiteCards(container,suites,projectId){
|
|
593
|
+
suites.forEach(function(s){
|
|
594
|
+
var tests=el('ul',{className:'suite-card-tests'});
|
|
595
|
+
(s.tests||[]).forEach(function(t){tests.appendChild(el('li',null,t))});
|
|
596
|
+
var pid=projectId;
|
|
597
|
+
var card=el('div',{className:'suite-card'},[
|
|
598
|
+
el('div',{className:'suite-card-head'},[
|
|
599
|
+
el('div',{className:'suite-card-name'},s.name),
|
|
600
|
+
el('span',{className:'suite-card-count'},s.testCount+' tests')
|
|
601
|
+
]),
|
|
602
|
+
tests,
|
|
603
|
+
el('button',{className:'btn sm primary',onclick:function(){triggerRun(s.name,pid)}},'Run Suite')
|
|
604
|
+
]);
|
|
605
|
+
container.appendChild(card);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/* ── Runs ── */
|
|
610
|
+
function refreshRuns(){
|
|
611
|
+
var url=S.project?'/api/db/projects/'+S.project+'/runs':'/api/db/runs';
|
|
612
|
+
api(url).then(function(rows){
|
|
613
|
+
var chart=$('#trendChart'),body=$('#runsBody'),empty=$('#runsEmpty'),head=$('#runsHead');
|
|
614
|
+
chart.textContent='';body.textContent='';
|
|
615
|
+
if(!Array.isArray(rows)||rows.length===0){empty.style.display='block';head.parentNode.parentNode.style.display='none';return}
|
|
616
|
+
empty.style.display='none';head.parentNode.parentNode.style.display='';
|
|
617
|
+
|
|
618
|
+
// Thead
|
|
619
|
+
var htr=document.createElement('tr');
|
|
620
|
+
var cols=[];
|
|
621
|
+
if(!S.project)cols.push('Project');
|
|
622
|
+
cols=cols.concat(['Suite','Date','Total','Pass','Fail','Rate','Time']);
|
|
623
|
+
cols.forEach(function(c){htr.appendChild(el('th',null,c))});
|
|
624
|
+
head.textContent='';head.appendChild(htr);
|
|
625
|
+
var colSpan=cols.length;
|
|
626
|
+
|
|
627
|
+
// Chart
|
|
628
|
+
rows.slice(0,40).slice().reverse().forEach(function(r){
|
|
629
|
+
var rate=parseFloat(r.pass_rate)||0;
|
|
630
|
+
var color=rate>=90?'var(--green)':rate>=70?'var(--amber)':'var(--red)';
|
|
631
|
+
var bar=el('div',{className:'chart-bar',style:'height:'+Math.max(rate,4)+'%;background:'+color});
|
|
632
|
+
bar.appendChild(el('div',{className:'tip'},(r.project_name||'')+(r.suite_name?' / '+r.suite_name:'')+': '+r.pass_rate));
|
|
633
|
+
chart.appendChild(bar);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// Rows
|
|
637
|
+
rows.forEach(function(r){
|
|
638
|
+
var tr=document.createElement('tr');
|
|
639
|
+
tr.dataset.runId=r.id;
|
|
640
|
+
if(r.id===S.selectedRun)tr.classList.add('expanded');
|
|
641
|
+
if(!S.project)tr.appendChild(el('td',{style:'font-weight:600'},r.project_name||'-'));
|
|
642
|
+
tr.appendChild(el('td',{style:'color:var(--accent)'},r.suite_name||'all'));
|
|
643
|
+
tr.appendChild(el('td',null,fdate(r.generated_at)));
|
|
644
|
+
tr.appendChild(el('td',null,String(r.total||0)));
|
|
645
|
+
tr.appendChild(el('td',{style:'color:var(--green)'},String(r.passed||0)));
|
|
646
|
+
tr.appendChild(el('td',{style:'color:var(--red)'},String(r.failed||0)));
|
|
647
|
+
var rv=parseFloat(r.pass_rate)||0;
|
|
648
|
+
tr.appendChild(el('td',{style:'font-weight:600;color:'+(rv>=90?'var(--green)':rv>=70?'var(--amber)':'var(--red)')},r.pass_rate||'-'));
|
|
649
|
+
tr.appendChild(el('td',{style:'color:var(--text2)'},r.duration||'-'));
|
|
650
|
+
tr.addEventListener('click',function(){toggleDetail(r.id,tr,colSpan)});
|
|
651
|
+
body.appendChild(tr);
|
|
652
|
+
|
|
653
|
+
// If this run was already expanded, re-expand it
|
|
654
|
+
if(r.id===S.selectedRun){
|
|
655
|
+
var detailTr=createDetailRow(colSpan);
|
|
656
|
+
body.appendChild(detailTr);
|
|
657
|
+
loadDetailInline(r.id,detailTr);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
}).catch(function(){});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function createDetailRow(colSpan){
|
|
664
|
+
var detailTr=document.createElement('tr');
|
|
665
|
+
detailTr.className='run-detail-row';
|
|
666
|
+
var td=document.createElement('td');
|
|
667
|
+
td.setAttribute('colspan',colSpan);
|
|
668
|
+
var wrap=el('div',{className:'rd-wrap'});
|
|
669
|
+
var inner=el('div',{className:'rd-inner'});
|
|
670
|
+
inner.innerHTML='<div style="color:var(--text3);font-size:11px"><span class="spinner-small"></span> Loading...</div>';
|
|
671
|
+
wrap.appendChild(inner);
|
|
672
|
+
td.appendChild(wrap);
|
|
673
|
+
detailTr.appendChild(td);
|
|
674
|
+
return detailTr;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function toggleDetail(id,clickedTr,colSpan){
|
|
678
|
+
// If already expanded, collapse
|
|
679
|
+
if(S.selectedRun===id){
|
|
680
|
+
var existing=clickedTr.nextElementSibling;
|
|
681
|
+
if(existing&&existing.classList.contains('run-detail-row')){
|
|
682
|
+
var w=existing.querySelector('.rd-wrap');
|
|
683
|
+
if(w)w.classList.remove('open');
|
|
684
|
+
clickedTr.classList.remove('expanded');
|
|
685
|
+
setTimeout(function(){if(existing.parentNode)existing.parentNode.removeChild(existing)},350);
|
|
686
|
+
}
|
|
687
|
+
S.selectedRun=null;
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Collapse any other open detail
|
|
692
|
+
var prevTr=document.querySelector('#runsBody tr.expanded');
|
|
693
|
+
if(prevTr){
|
|
694
|
+
prevTr.classList.remove('expanded');
|
|
695
|
+
var prevDetail=prevTr.nextElementSibling;
|
|
696
|
+
if(prevDetail&&prevDetail.classList.contains('run-detail-row')){
|
|
697
|
+
var pw=prevDetail.querySelector('.rd-wrap');
|
|
698
|
+
if(pw)pw.classList.remove('open');
|
|
699
|
+
setTimeout(function(){if(prevDetail.parentNode)prevDetail.parentNode.removeChild(prevDetail)},350);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Expand new
|
|
704
|
+
S.selectedRun=id;
|
|
705
|
+
clickedTr.classList.add('expanded');
|
|
706
|
+
var detailTr=createDetailRow(colSpan);
|
|
707
|
+
clickedTr.parentNode.insertBefore(detailTr,clickedTr.nextSibling);
|
|
708
|
+
|
|
709
|
+
// Animate open
|
|
710
|
+
requestAnimationFrame(function(){
|
|
711
|
+
requestAnimationFrame(function(){
|
|
712
|
+
var w2=detailTr.querySelector('.rd-wrap');
|
|
713
|
+
if(w2)w2.classList.add('open');
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
loadDetailInline(id,detailTr);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function loadDetailInline(id,detailTr){
|
|
721
|
+
api('/api/db/runs/'+id).then(function(d){
|
|
722
|
+
if(d.error)return;
|
|
723
|
+
var inner=detailTr.querySelector('.rd-inner');
|
|
724
|
+
inner.textContent='';
|
|
725
|
+
|
|
726
|
+
var results=d.results||[];
|
|
727
|
+
|
|
728
|
+
// Summary bar
|
|
729
|
+
var summ=el('div',{className:'rd-summary'},[
|
|
730
|
+
el('div',null,[el('div',{className:'rd-s-label'},'Suite'),el('div',{className:'rd-s-val',style:'font-size:13px;color:var(--accent)'},d.suiteName||'all')]),
|
|
731
|
+
el('div',null,[el('div',{className:'rd-s-label'},'Total'),el('div',{className:'rd-s-val'},String(d.summary.total))]),
|
|
732
|
+
el('div',null,[el('div',{className:'rd-s-label'},'Passed'),el('div',{className:'rd-s-val',style:'color:var(--green)'},String(d.summary.passed))]),
|
|
733
|
+
el('div',null,[el('div',{className:'rd-s-label'},'Failed'),el('div',{className:'rd-s-val',style:'color:var(--red)'},String(d.summary.failed))]),
|
|
734
|
+
el('div',null,[el('div',{className:'rd-s-label'},'Duration'),el('div',{className:'rd-s-val',style:'font-size:13px;color:var(--text2)'},d.summary.duration||'-')])
|
|
735
|
+
]);
|
|
736
|
+
inner.appendChild(summ);
|
|
737
|
+
|
|
738
|
+
// Test result cards
|
|
739
|
+
results.forEach(function(r){
|
|
740
|
+
var d2=r.durationMs?dur(r.durationMs):r.endTime&&r.startTime?dur(new Date(r.endTime)-new Date(r.startTime)):'-';
|
|
741
|
+
var flaky=r.success&&r.attempt>1;
|
|
742
|
+
|
|
743
|
+
// Header: badge + name + duration
|
|
744
|
+
var badges=el('div',{style:'display:flex;gap:6px;align-items:center;flex-shrink:0'});
|
|
745
|
+
badges.appendChild(el('span',{className:'badge '+(r.success?'pass':'fail')},r.success?'PASS':'FAIL'));
|
|
746
|
+
if(flaky)badges.appendChild(el('span',{className:'badge flaky'},'FLAKY'));
|
|
747
|
+
|
|
748
|
+
var head=el('div',{className:'rd-test-head'},[
|
|
749
|
+
badges,
|
|
750
|
+
el('div',{className:'rd-test-name'},r.name),
|
|
751
|
+
el('div',{className:'rd-test-dur'},d2)
|
|
752
|
+
]);
|
|
753
|
+
|
|
754
|
+
// Body content
|
|
755
|
+
var body=el('div',{className:'rd-test-body'});
|
|
756
|
+
|
|
757
|
+
// Retries info
|
|
758
|
+
if(r.maxAttempts>1){
|
|
759
|
+
body.appendChild(el('div',{className:'rd-retries'},'Attempt '+r.attempt+' of '+r.maxAttempts));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Error message
|
|
763
|
+
if(r.error){
|
|
764
|
+
body.appendChild(el('div',{className:'rd-error-msg'},r.error));
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Screenshots per test
|
|
768
|
+
var shots=[];
|
|
769
|
+
var hashes=r.screenshotHashes||{};
|
|
770
|
+
(r.screenshots||[]).forEach(function(p){
|
|
771
|
+
var fname=p.split('/').pop();
|
|
772
|
+
shots.push({path:p,label:fname,type:'screenshot',hash:hashes[p]||null});
|
|
773
|
+
});
|
|
774
|
+
if(r.errorScreenshot){
|
|
775
|
+
var ename=r.errorScreenshot.split('/').pop();
|
|
776
|
+
shots.push({path:r.errorScreenshot,label:ename,type:'error',hash:hashes[r.errorScreenshot]||null});
|
|
777
|
+
}
|
|
778
|
+
if(shots.length){
|
|
779
|
+
var shotsWrap=el('div',{className:'rd-shots'});
|
|
780
|
+
shots.forEach(function(s){
|
|
781
|
+
var src='/api/image?path='+encodeURIComponent(s.path);
|
|
782
|
+
var img=document.createElement('img');img.src=src;img.alt=s.label;img.loading='lazy';
|
|
783
|
+
var capEl=el('div',{className:'rd-shot-cap'},[el('span',{className:'cap-name'},s.label)]);
|
|
784
|
+
if(s.hash){capEl.appendChild(createHashBadge(s.hash))}
|
|
785
|
+
else{(function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,s.path)}
|
|
786
|
+
var shotEl=el('div',{className:'rd-shot'+(s.type==='error'?' err-shot':''),onclick:function(e){e.stopPropagation();openModal(src)}},[
|
|
787
|
+
img,capEl
|
|
788
|
+
]);
|
|
789
|
+
shotsWrap.appendChild(shotEl);
|
|
790
|
+
});
|
|
791
|
+
body.appendChild(shotsWrap);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Console logs (errors and warnings only)
|
|
795
|
+
var cIssues=(r.consoleLogs||[]).filter(function(l){return l.type==='error'||l.type==='warn'||l.type==='warning'});
|
|
796
|
+
if(cIssues.length){
|
|
797
|
+
var logSec=el('div',{className:'rd-logs'});
|
|
798
|
+
logSec.appendChild(el('div',{className:'rd-log-label'},'Console'));
|
|
799
|
+
cIssues.forEach(function(l){
|
|
800
|
+
logSec.appendChild(el('div',{className:'rd-log-item '+l.type},'['+l.type+'] '+l.text));
|
|
801
|
+
});
|
|
802
|
+
body.appendChild(logSec);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Network errors
|
|
806
|
+
if(r.networkErrors&&r.networkErrors.length){
|
|
807
|
+
var netSec=el('div',{className:'rd-logs'});
|
|
808
|
+
netSec.appendChild(el('div',{className:'rd-log-label'},'Network Errors'));
|
|
809
|
+
r.networkErrors.forEach(function(ne){
|
|
810
|
+
netSec.appendChild(el('div',{className:'rd-log-item error'},'['+ne.error+'] '+ne.url));
|
|
811
|
+
});
|
|
812
|
+
body.appendChild(netSec);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
var testCard=el('div',{className:'rd-test'},[head,body]);
|
|
816
|
+
inner.appendChild(testCard);
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// Re-trigger open animation if not yet open
|
|
820
|
+
var w=detailTr.querySelector('.rd-wrap');
|
|
821
|
+
if(w&&!w.classList.contains('open')){
|
|
822
|
+
requestAnimationFrame(function(){w.classList.add('open')});
|
|
823
|
+
}
|
|
824
|
+
}).catch(function(err){
|
|
825
|
+
var inner=detailTr.querySelector('.rd-inner');
|
|
826
|
+
if(inner)inner.textContent='Failed to load run detail';
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/* ── Screenshots ── */
|
|
831
|
+
function refreshScreenshots(){
|
|
832
|
+
var gal=$('#screenshotGallery'),empty=$('#screenshotsEmpty');
|
|
833
|
+
gal.textContent='';
|
|
834
|
+
if(!S.project){empty.style.display='block';empty.querySelector('p').textContent='Select a project to view screenshots.';return}
|
|
835
|
+
api('/api/db/projects/'+S.project+'/screenshots').then(function(files){
|
|
836
|
+
if(!Array.isArray(files)||!files.length){empty.style.display='block';empty.querySelector('p').textContent='No screenshots for this project.';return}
|
|
837
|
+
empty.style.display='none';
|
|
838
|
+
files.forEach(function(f){
|
|
839
|
+
var src='/api/image?path='+encodeURIComponent(f.path);
|
|
840
|
+
var img=document.createElement('img');img.src=src;img.alt=f.name;img.loading='lazy';
|
|
841
|
+
var capEl=el('div',{className:'cap'},[el('span',{className:'cap-name'},f.name)]);
|
|
842
|
+
(function(c,fp){
|
|
843
|
+
ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))});
|
|
844
|
+
})(capEl,f.path);
|
|
845
|
+
var item=el('div',{className:'gallery-item',onclick:function(){openModal(src)}},[
|
|
846
|
+
img,capEl
|
|
847
|
+
]);gal.appendChild(item);
|
|
848
|
+
});
|
|
849
|
+
}).catch(function(){});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/* ── Live Execution ── */
|
|
853
|
+
function renderLive(){
|
|
854
|
+
var panel=$('#livePanel'),grid=$('#liveTests'),navLive=$('#navLive'),liveEmpty=$('#liveEmpty');
|
|
855
|
+
var runs=S.liveRuns;
|
|
856
|
+
var runIds=Object.keys(runs);
|
|
857
|
+
|
|
858
|
+
if(runIds.length===0){
|
|
859
|
+
panel.classList.remove('active');
|
|
860
|
+
navLive.style.display='none';
|
|
861
|
+
liveEmpty.style.display='block';
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
navLive.style.display='';
|
|
866
|
+
liveEmpty.style.display='none';
|
|
867
|
+
panel.classList.add('active');
|
|
868
|
+
|
|
869
|
+
// Aggregate stats across all runs
|
|
870
|
+
var gTotal=0,gCompleted=0,gPassed=0,gFailed=0,gActive=0,gRunning=false,gDone=true;
|
|
871
|
+
runIds.forEach(function(rid){
|
|
872
|
+
var r=runs[rid];gTotal+=r.total;gCompleted+=r.completed;gPassed+=r.passed;gFailed+=r.failed;gActive+=r.active;
|
|
873
|
+
if(r.on)gRunning=true;if(!r.done)gDone=false;
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
var badgeActive=0;
|
|
877
|
+
runIds.forEach(function(rid){var r=runs[rid];Object.keys(r.tests).forEach(function(n){if(n!=='__error'&&r.tests[n].status==='running')badgeActive++})});
|
|
878
|
+
$('#liveBadge').textContent=gRunning?badgeActive:gCompleted;
|
|
879
|
+
$('#liveBadge').style.background=gRunning?'var(--purple-dim)':gFailed>0?'var(--red-dim)':'var(--green-dim)';
|
|
880
|
+
$('#liveBadge').style.color=gRunning?'var(--purple)':gFailed>0?'var(--red)':'var(--green)';
|
|
881
|
+
|
|
882
|
+
$('#liveTotal').textContent=gTotal;
|
|
883
|
+
$('#livePass').textContent=gPassed;
|
|
884
|
+
$('#liveFail').textContent=gFailed;
|
|
885
|
+
$('#liveActive').textContent=gActive;
|
|
886
|
+
var pct=gTotal>0?gCompleted/gTotal*100:0;
|
|
887
|
+
$('#liveProgressFill').style.width=pct+'%';
|
|
888
|
+
|
|
889
|
+
// Hide single-project info (now shown per-section)
|
|
890
|
+
$('#liveProject').style.display='none';
|
|
891
|
+
|
|
892
|
+
// Header state
|
|
893
|
+
var lbl=panel.querySelector('.live-header .label');
|
|
894
|
+
var anyStale=runIds.some(function(rid){return runs[rid].stale});
|
|
895
|
+
if(!gRunning&&gDone){
|
|
896
|
+
lbl.textContent=anyStale?'COMPLETED (connection lost)':gFailed>0?'COMPLETED WITH FAILURES':'ALL TESTS PASSED';
|
|
897
|
+
lbl.style.color=anyStale?'var(--yellow)':gFailed>0?'var(--red)':'var(--green)';
|
|
898
|
+
var dot=lbl.querySelector('.dot');if(dot)dot.remove();
|
|
899
|
+
$('#liveProgressFill').style.background=anyStale?'var(--yellow)':gFailed>0?'var(--red)':'var(--green)';
|
|
900
|
+
} else {
|
|
901
|
+
if(!lbl.querySelector('.dot')){
|
|
902
|
+
lbl.textContent='';
|
|
903
|
+
var d=el('span',{className:'dot'});lbl.appendChild(d);lbl.appendChild(document.createTextNode(' RUNNING'));
|
|
904
|
+
}
|
|
905
|
+
lbl.style.color='var(--purple)';
|
|
906
|
+
$('#liveProgressFill').style.background='var(--purple)';
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Render per-run sections
|
|
910
|
+
grid.textContent='';
|
|
911
|
+
runIds.forEach(function(rid){
|
|
912
|
+
var L=runs[rid];
|
|
913
|
+
// Project section header
|
|
914
|
+
var projLabel=L.project||(L.cwd?L.cwd.split('/').pop():'Run');
|
|
915
|
+
var runStatus=L.done?(L.failed>0?'fail':'pass'):'running';
|
|
916
|
+
var sectionHeader=el('div',{className:'lr-section-header '+runStatus},[
|
|
917
|
+
el('span',{className:'lr-project-name'},projLabel),
|
|
918
|
+
el('span',{className:'lr-section-stats'},[
|
|
919
|
+
el('span',{},L.completed+'/'+L.total),
|
|
920
|
+
L.failed>0?el('span',{style:'color:var(--red);margin-left:6px'},L.failed+' failed'):null,
|
|
921
|
+
L.on?el('span',{className:'spinner-small',style:'margin-left:6px'}):null
|
|
922
|
+
])
|
|
923
|
+
]);
|
|
924
|
+
grid.appendChild(sectionHeader);
|
|
925
|
+
|
|
926
|
+
var testGrid=el('div',{className:'lr-test-grid'});
|
|
927
|
+
var names=Object.keys(L.tests);
|
|
928
|
+
names.forEach(function(name){
|
|
929
|
+
if(name==='__error')return;
|
|
930
|
+
var t=L.tests[name];
|
|
931
|
+
var testKey=rid+'::'+name;
|
|
932
|
+
var iconText=t.status==='passed'?'\u2714':t.status==='failed'?'\u2718':'\u25CF';
|
|
933
|
+
var iconColor=t.status==='passed'?'color:var(--green)':t.status==='failed'?'color:var(--red)':'color:var(--purple)';
|
|
934
|
+
var meta='';
|
|
935
|
+
if(t.status==='running'){
|
|
936
|
+
meta=t.actionType?('Step '+(t.actions||0)+'/'+(t.totalActions||'?')):'starting...';
|
|
937
|
+
if(t.retry)meta='Retry '+t.retry;
|
|
938
|
+
} else if(t.status==='passed'){meta=t.duration||'done'}
|
|
939
|
+
else if(t.status==='failed'){meta=t.error||'failed'}
|
|
940
|
+
// Action log
|
|
941
|
+
var stepsEl=el('div',{className:'lt-actions'});
|
|
942
|
+
if(t.actionLog&&t.actionLog.length>0){
|
|
943
|
+
t.actionLog.forEach(function(a){
|
|
944
|
+
var detail=a.selector||a.value||a.text||'';
|
|
945
|
+
var durText=a.duration!=null?(a.duration<1000?a.duration+'ms':(a.duration/1000).toFixed(1)+'s'):'';
|
|
946
|
+
stepsEl.appendChild(el('div',{className:'lt-step'},[
|
|
947
|
+
el('span',{className:'step-icon '+(a.success?'ok':'fail')},a.success?'\u2714':'\u2718'),
|
|
948
|
+
el('span',{className:'step-type'},a.type),
|
|
949
|
+
el('span',{className:'step-detail'},detail),
|
|
950
|
+
el('span',{className:'step-dur'},durText)
|
|
951
|
+
]));
|
|
952
|
+
});
|
|
953
|
+
if(t.status==='running'&&t.actions<t.totalActions){
|
|
954
|
+
stepsEl.appendChild(el('div',{className:'lt-step'},[el('span',{className:'step-icon run spinner-small'}),el('span',{className:'step-type',style:'opacity:.6'},'waiting...')]));
|
|
955
|
+
}
|
|
956
|
+
} else if(t.status==='running'){
|
|
957
|
+
stepsEl.appendChild(el('div',{className:'lt-step'},[el('span',{className:'step-icon run spinner-small'}),el('span',{className:'step-type',style:'opacity:.6'},'connecting...')]));
|
|
958
|
+
}
|
|
959
|
+
var isFinished=t.status==='passed'||t.status==='failed';
|
|
960
|
+
var isCollapsed=isFinished&&!S.liveExpanded.has(testKey);
|
|
961
|
+
var summaryEl=el('div',{className:'lt-summary'},[
|
|
962
|
+
el('span',{className:'lt-dur'},t.duration||''),
|
|
963
|
+
el('span',{className:'lt-expand'},isCollapsed?'\u25BC':'\u25B2')
|
|
964
|
+
]);
|
|
965
|
+
// Screenshots section
|
|
966
|
+
var ssEl=null;
|
|
967
|
+
var allSS=(t.screenshots||[]).slice();
|
|
968
|
+
if(t.errorScreenshot)allSS.push(t.errorScreenshot);
|
|
969
|
+
if(allSS.length>0){
|
|
970
|
+
var ssOpen=S.liveSSOpen&&S.liveSSOpen.has(testKey);
|
|
971
|
+
var toggle=el('div',{className:'lt-screenshots-toggle'+(ssOpen?' open':'')},[
|
|
972
|
+
el('span',{className:'ss-arrow'},'\u25B6'),
|
|
973
|
+
el('span',{},'Screenshots ('+allSS.length+')')
|
|
974
|
+
]);
|
|
975
|
+
var ssGridEl=el('div',{className:'lt-screenshots-grid'});
|
|
976
|
+
allSS.forEach(function(ssPath){
|
|
977
|
+
var fname=ssPath.split('/').pop();
|
|
978
|
+
var isErr=t.errorScreenshot&&ssPath===t.errorScreenshot;
|
|
979
|
+
var thumb=el('div',{className:'lt-ss-thumb'});
|
|
980
|
+
var img=document.createElement('img');
|
|
981
|
+
img.src='/api/image?path='+encodeURIComponent(ssPath);
|
|
982
|
+
img.alt=fname;img.loading='lazy';
|
|
983
|
+
if(isErr)thumb.style.borderColor='var(--red)';
|
|
984
|
+
thumb.appendChild(img);
|
|
985
|
+
thumb.addEventListener('click',function(e){e.stopPropagation();openModal('/api/image?path='+encodeURIComponent(ssPath),fname)});
|
|
986
|
+
var labelEl=el('div',{className:'lt-ss-label'},[el('span',{style:'overflow:hidden;text-overflow:ellipsis;white-space:nowrap'},fname)]);
|
|
987
|
+
// Compute hash async and append badge
|
|
988
|
+
(function(lbl,sp){
|
|
989
|
+
ssHash(sp).then(function(h){lbl.appendChild(createHashBadge(h))});
|
|
990
|
+
})(labelEl,ssPath);
|
|
991
|
+
ssGridEl.appendChild(el('div',{},[thumb,labelEl]));
|
|
992
|
+
});
|
|
993
|
+
toggle.addEventListener('click',function(e){
|
|
994
|
+
e.stopPropagation();
|
|
995
|
+
if(S.liveSSOpen.has(testKey))S.liveSSOpen.delete(testKey);else S.liveSSOpen.add(testKey);
|
|
996
|
+
toggle.classList.toggle('open');
|
|
997
|
+
ssGridEl.style.display=ssGridEl.style.display==='grid'?'none':'grid';
|
|
998
|
+
});
|
|
999
|
+
if(ssOpen)ssGridEl.style.display='grid';
|
|
1000
|
+
ssEl=el('div',{className:'lt-screenshots'},[toggle,ssGridEl]);
|
|
1001
|
+
}
|
|
1002
|
+
var card=el('div',{className:'live-test '+t.status+(isCollapsed?' collapsed':'')},[
|
|
1003
|
+
el('div',{className:'lt-name'},[
|
|
1004
|
+
t.status==='running'?el('span',{className:'spinner'}):el('span',{className:'lt-icon',style:iconColor},iconText),
|
|
1005
|
+
document.createTextNode(' '+name),
|
|
1006
|
+
summaryEl
|
|
1007
|
+
]),
|
|
1008
|
+
el('div',{className:'lt-meta'},meta),
|
|
1009
|
+
stepsEl
|
|
1010
|
+
]);
|
|
1011
|
+
if(ssEl)card.appendChild(ssEl);
|
|
1012
|
+
if(isFinished){
|
|
1013
|
+
card.addEventListener('click',function(){
|
|
1014
|
+
if(S.liveExpanded.has(testKey))S.liveExpanded.delete(testKey);
|
|
1015
|
+
else S.liveExpanded.add(testKey);
|
|
1016
|
+
renderLive();
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
testGrid.appendChild(card);
|
|
1020
|
+
if(!isCollapsed)stepsEl.scrollTop=stepsEl.scrollHeight;
|
|
1021
|
+
});
|
|
1022
|
+
grid.appendChild(testGrid);
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/* ── Actions ── */
|
|
1027
|
+
$('#btnRunAll').addEventListener('click',function(){triggerRun()});
|
|
1028
|
+
|
|
1029
|
+
/* ── Modal ── */
|
|
1030
|
+
function openModal(src){$('#modalImg').src=src;$('#modal').classList.add('open')}
|
|
1031
|
+
$('#modal').addEventListener('click',function(){$('#modal').classList.remove('open')});
|
|
1032
|
+
document.addEventListener('keydown',function(e){if(e.key==='Escape')$('#modal').classList.remove('open')});
|
|
1033
|
+
|
|
1034
|
+
/* ── Init ── */
|
|
1035
|
+
connectWS();
|
|
1036
|
+
refreshStatus();
|
|
1037
|
+
refreshProjects();
|
|
1038
|
+
refreshSuites();
|
|
1039
|
+
refreshRuns();
|
|
1040
|
+
refreshScreenshots();
|
|
1041
|
+
})();
|
|
1042
|
+
</script>
|
|
1043
|
+
</body>
|
|
1044
|
+
</html>
|