@matware/e2e-runner 1.2.1 → 1.3.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/.claude-plugin/marketplace.json +52 -0
- package/.claude-plugin/plugin.json +17 -3
- package/.mcp.json +2 -2
- package/.opencode/commands/create-test.md +63 -0
- package/.opencode/commands/run.md +50 -0
- package/.opencode/commands/verify-issue.md +62 -0
- package/.opencode/skills/e2e-testing/SKILL.md +181 -0
- package/.opencode/skills/e2e-testing/references/action-types.md +143 -0
- package/.opencode/skills/e2e-testing/references/auth-strategies.md +91 -0
- package/.opencode/skills/e2e-testing/references/graphql.md +59 -0
- package/.opencode/skills/e2e-testing/references/issue-verification.md +59 -0
- package/.opencode/skills/e2e-testing/references/multi-pool.md +60 -0
- package/.opencode/skills/e2e-testing/references/network-debugging.md +62 -0
- package/.opencode/skills/e2e-testing/references/test-json-format.md +163 -0
- package/.opencode/skills/e2e-testing/references/troubleshooting.md +224 -0
- package/.opencode/skills/e2e-testing/references/variables.md +41 -0
- package/.opencode/skills/e2e-testing/references/visual-verification.md +89 -0
- package/LICENSE +190 -0
- package/OPENCODE.md +166 -0
- package/README.md +165 -104
- package/agents/test-creator.md +54 -1
- package/agents/test-improver.md +37 -0
- package/bin/cli.js +409 -16
- package/commands/capture.md +45 -0
- package/commands/create-test.md +16 -1
- package/opencode.json +11 -0
- package/package.json +7 -2
- package/scripts/setup-opencode.sh +113 -0
- package/skills/e2e-testing/SKILL.md +10 -3
- package/skills/e2e-testing/references/action-types.md +48 -5
- package/skills/e2e-testing/references/auth-strategies.md +91 -0
- package/skills/e2e-testing/references/graphql.md +59 -0
- package/skills/e2e-testing/references/issue-verification.md +59 -0
- package/skills/e2e-testing/references/multi-pool.md +60 -0
- package/skills/e2e-testing/references/network-debugging.md +62 -0
- package/skills/e2e-testing/references/test-json-format.md +4 -0
- package/skills/e2e-testing/references/troubleshooting.md +44 -2
- package/skills/e2e-testing/references/variables.md +41 -0
- package/skills/e2e-testing/references/visual-verification.md +89 -0
- package/src/actions.js +475 -2
- package/src/ai-generate.js +139 -8
- package/src/app-pool.js +339 -0
- package/src/config.js +266 -5
- package/src/dashboard.js +216 -17
- package/src/db.js +191 -7
- package/src/index.js +12 -9
- package/src/learner-sqlite.js +458 -0
- package/src/learner.js +78 -6
- package/src/mcp-tools.js +1348 -51
- package/src/module-resolver.js +37 -0
- package/src/narrate.js +65 -0
- package/src/pool-manager.js +229 -0
- package/src/pool.js +301 -31
- package/src/reporter.js +86 -2
- package/src/runner.js +480 -71
- package/src/sync/auth.js +354 -0
- package/src/sync/client.js +572 -0
- package/src/sync/hub-routes.js +816 -0
- package/src/sync/index.js +68 -0
- package/src/sync/middleware.js +347 -0
- package/src/sync/queue.js +209 -0
- package/src/sync/schema.js +540 -0
- package/src/verify.js +10 -7
- package/src/visual-diff.js +446 -0
- package/src/watch.js +384 -0
- package/templates/build-dashboard.js +47 -6
- package/templates/dashboard/js/api.js +62 -0
- package/templates/dashboard/js/init.js +13 -0
- package/templates/dashboard/js/keyboard.js +46 -0
- package/templates/dashboard/js/state.js +40 -0
- package/templates/dashboard/js/toast.js +41 -0
- package/templates/dashboard/js/utils.js +216 -0
- package/templates/dashboard/js/view-live.js +181 -0
- package/templates/dashboard/js/view-runs.js +676 -0
- package/templates/dashboard/js/view-tests.js +294 -0
- package/templates/dashboard/js/view-watch.js +242 -0
- package/templates/dashboard/js/websocket.js +116 -0
- package/templates/dashboard/styles/base.css +69 -0
- package/templates/dashboard/styles/components.css +117 -0
- package/templates/dashboard/styles/view-live.css +97 -0
- package/templates/dashboard/styles/view-runs.css +243 -0
- package/templates/dashboard/styles/view-tests.css +96 -0
- package/templates/dashboard/styles/view-watch.css +53 -0
- package/templates/dashboard/template.html +181 -100
- package/templates/dashboard.html +1614 -547
- package/templates/sample-test.json +0 -8
- package/templates/dashboard/app.js +0 -1152
- package/templates/dashboard/styles.css +0 -413
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/* ── DOM Helpers ── */
|
|
2
|
+
var $=function(s){return document.querySelector(s)};
|
|
3
|
+
var $$=function(s){return document.querySelectorAll(s)};
|
|
4
|
+
|
|
5
|
+
function el(tag,a,ch){
|
|
6
|
+
var e=document.createElement(tag);
|
|
7
|
+
if(a)Object.keys(a).forEach(function(k){
|
|
8
|
+
if(k==='className')e.className=a[k];
|
|
9
|
+
else if(k==='style')e.style.cssText=a[k];
|
|
10
|
+
else if(k.indexOf('on')===0)e.addEventListener(k.slice(2),a[k]);
|
|
11
|
+
else e.setAttribute(k,a[k]);
|
|
12
|
+
});
|
|
13
|
+
if(typeof ch==='string')e.textContent=ch;
|
|
14
|
+
else if(Array.isArray(ch))ch.forEach(function(c){if(c)e.appendChild(c)});
|
|
15
|
+
return e;
|
|
16
|
+
}
|
|
17
|
+
function css(n){return n.replace(/[^a-zA-Z0-9\-_]/g,'_')}
|
|
18
|
+
function dur(ms){return ms>=1000?(ms/1000).toFixed(1)+'s':ms+'ms'}
|
|
19
|
+
function fdate(iso){return iso?new Date(iso).toLocaleString():'--'}
|
|
20
|
+
|
|
21
|
+
function prettyJson(str){
|
|
22
|
+
if(!str)return '';
|
|
23
|
+
try{return JSON.stringify(JSON.parse(str),null,2)}catch(e){return str}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fmtHeaders(h){
|
|
27
|
+
if(!h||typeof h!=='object')return '';
|
|
28
|
+
return Object.keys(h).map(function(k){return k+': '+h[k]}).join('\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildHeaderKV(h){
|
|
32
|
+
if(!h||typeof h!=='object') return el('div',{className:'rd-nd-empty'},'No data');
|
|
33
|
+
var table=el('div',{className:'rd-hdr-table'});
|
|
34
|
+
Object.keys(h).forEach(function(k){
|
|
35
|
+
var row=el('div',{className:'rd-hdr-row'});
|
|
36
|
+
row.appendChild(el('span',{className:'rd-hdr-key'},k));
|
|
37
|
+
row.appendChild(el('span',{className:'rd-hdr-val'},String(h[k])));
|
|
38
|
+
table.appendChild(row);
|
|
39
|
+
});
|
|
40
|
+
return table;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function makeCopyBtn(getTextFn){
|
|
44
|
+
var btn=el('span',{className:'copy-btn',onclick:function(e){
|
|
45
|
+
e.stopPropagation();
|
|
46
|
+
var text=typeof getTextFn==='function'?getTextFn():String(getTextFn);
|
|
47
|
+
navigator.clipboard.writeText(text).then(function(){
|
|
48
|
+
btn.textContent='\u2713 Copied';
|
|
49
|
+
btn.classList.add('copied');
|
|
50
|
+
setTimeout(function(){btn.textContent='\u2398 Copy';btn.classList.remove('copied')},1200);
|
|
51
|
+
});
|
|
52
|
+
}},'\u2398 Copy');
|
|
53
|
+
return btn;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildNdSection(title,contentEl,count,copyText){
|
|
57
|
+
var toggle=el('div',{className:'rd-nd-toggle'},[
|
|
58
|
+
el('span',{className:'nd-arrow'},'\u25B6'),
|
|
59
|
+
el('span',null,title),
|
|
60
|
+
count?el('span',{className:'nd-count'},count+' entries'):null,
|
|
61
|
+
makeCopyBtn(copyText||function(){return contentWrap.textContent})
|
|
62
|
+
]);
|
|
63
|
+
var contentWrap=el('div',{className:'rd-nd-content'});
|
|
64
|
+
contentWrap.appendChild(contentEl);
|
|
65
|
+
toggle.addEventListener('click',function(e){
|
|
66
|
+
e.stopPropagation();
|
|
67
|
+
toggle.classList.toggle('open');
|
|
68
|
+
});
|
|
69
|
+
return el('div',{className:'rd-nd-section'},[toggle,contentWrap]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function gqlOp(n){
|
|
73
|
+
if(n.requestBody){
|
|
74
|
+
try{
|
|
75
|
+
var b=JSON.parse(n.requestBody);
|
|
76
|
+
if(b.operationName)return b.operationName;
|
|
77
|
+
if(b.query){var m=b.query.match(/^(?:query|mutation|subscription)\s+([A-Za-z_]\w*)/);if(m)return m[1]}
|
|
78
|
+
}catch(e){}
|
|
79
|
+
}
|
|
80
|
+
if(n.url){
|
|
81
|
+
try{var u=new URL(n.url,location.href);var op=u.searchParams.get('operationName');if(op)return op}catch(e){}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildNetRow(n){
|
|
87
|
+
var mCls='rd-net-method '+(n.method||'GET').toLowerCase();
|
|
88
|
+
var sCode=n.status||0;
|
|
89
|
+
var sCls='rd-net-status '+(sCode<300?'s2xx':sCode<400?'s3xx':sCode<500?'s4xx':'s5xx');
|
|
90
|
+
var hasDetail=n.requestBody||n.responseBody||n.requestHeaders||n.responseHeaders;
|
|
91
|
+
var rowCls='rd-net-row'+(sCode>=400?' has-error':'');
|
|
92
|
+
var opName=gqlOp(n);
|
|
93
|
+
var children=[
|
|
94
|
+
el('span',{className:'rd-net-expand'},hasDetail?'\u25B6':''),
|
|
95
|
+
el('span',{className:mCls},n.method||'GET'),
|
|
96
|
+
el('span',{className:sCls},String(sCode))
|
|
97
|
+
];
|
|
98
|
+
if(opName)children.push(el('span',{className:'rd-net-op'},opName));
|
|
99
|
+
children.push(el('span',{className:'rd-net-url'},n.url||''));
|
|
100
|
+
children.push(makeCopyBtn(n.url||''));
|
|
101
|
+
children.push(el('span',{className:'rd-net-dur'},dur(n.duration)));
|
|
102
|
+
var row=el('div',{className:rowCls},children);
|
|
103
|
+
var detail=null;
|
|
104
|
+
if(hasDetail){
|
|
105
|
+
var sections=[];
|
|
106
|
+
if(n.requestHeaders){
|
|
107
|
+
var hCount=Object.keys(n.requestHeaders).length;
|
|
108
|
+
sections.push(buildNdSection('Request Headers',buildHeaderKV(n.requestHeaders),hCount,fmtHeaders(n.requestHeaders)));
|
|
109
|
+
}
|
|
110
|
+
if(n.requestBody){
|
|
111
|
+
var rbText=prettyJson(n.requestBody);
|
|
112
|
+
sections.push(buildNdSection('Request Body',el('pre',null,rbText),null,rbText));
|
|
113
|
+
}
|
|
114
|
+
if(n.responseHeaders){
|
|
115
|
+
var rhCount=Object.keys(n.responseHeaders).length;
|
|
116
|
+
sections.push(buildNdSection('Response Headers',buildHeaderKV(n.responseHeaders),rhCount,fmtHeaders(n.responseHeaders)));
|
|
117
|
+
}
|
|
118
|
+
if(n.responseBody){
|
|
119
|
+
var respText=prettyJson(n.responseBody);
|
|
120
|
+
sections.push(buildNdSection('Response Body',el('pre',null,respText),null,respText));
|
|
121
|
+
}
|
|
122
|
+
detail=el('div',{className:'rd-net-detail'},sections);
|
|
123
|
+
row.addEventListener('click',function(e){e.stopPropagation();row.classList.toggle('open')});
|
|
124
|
+
}
|
|
125
|
+
return {row:row,detail:detail};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* ── Screenshot hash helpers ── */
|
|
129
|
+
var ssHashCache={};
|
|
130
|
+
async function ssHash(filePath){
|
|
131
|
+
if(ssHashCache[filePath])return ssHashCache[filePath];
|
|
132
|
+
var data=new TextEncoder().encode(filePath);
|
|
133
|
+
var buf=await crypto.subtle.digest('SHA-256',data);
|
|
134
|
+
var hex=Array.from(new Uint8Array(buf)).map(function(b){return b.toString(16).padStart(2,'0')}).join('');
|
|
135
|
+
var h=hex.slice(0,8);
|
|
136
|
+
ssHashCache[filePath]=h;
|
|
137
|
+
return h;
|
|
138
|
+
}
|
|
139
|
+
function ssHashSync(filePath){return ssHashCache[filePath]||null}
|
|
140
|
+
function copyHash(hash,badge){
|
|
141
|
+
navigator.clipboard.writeText('ss:'+hash).then(function(){
|
|
142
|
+
badge.classList.add('copied');
|
|
143
|
+
setTimeout(function(){badge.classList.remove('copied')},1200);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function createHashBadge(hash){
|
|
147
|
+
var badge=el('span',{className:'ss-hash',onclick:function(e){e.stopPropagation();copyHash(hash,badge)}},[
|
|
148
|
+
el('span',{className:'ss-icon'},'\u2318'),
|
|
149
|
+
document.createTextNode('ss:'+hash)
|
|
150
|
+
]);
|
|
151
|
+
return badge;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function createTriggerBadge(source){
|
|
155
|
+
var s=source||'unknown';
|
|
156
|
+
var labels={dashboard:'Dashboard',mcp:'MCP',cli:'CLI',unknown:'--'};
|
|
157
|
+
var icons={dashboard:'\u{1F464}',mcp:'\u{1F916}',cli:'>_',unknown:'\u2022'};
|
|
158
|
+
var badge=el('span',{className:'trigger-badge src-'+s},[
|
|
159
|
+
el('span',{className:'trig-icon'},icons[s]||icons.unknown),
|
|
160
|
+
document.createTextNode(labels[s]||s)
|
|
161
|
+
]);
|
|
162
|
+
return badge;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function createDriverBadge(driver){
|
|
166
|
+
if(!driver)return document.createTextNode('--');
|
|
167
|
+
var labels={browserless:'Browserless',cdp:'CDP',steel:'Steel',auto:'Auto'};
|
|
168
|
+
var colors={browserless:'var(--accent)',cdp:'var(--purple)',steel:'var(--amber)'};
|
|
169
|
+
var icons={browserless:'\u{1F310}',cdp:'\u{1F50C}',steel:'\u{1F6E1}'};
|
|
170
|
+
// Handle multi-driver (e.g. "browserless,steel")
|
|
171
|
+
var parts=driver.split(',');
|
|
172
|
+
if(parts.length>1){
|
|
173
|
+
var wrap=el('span',{style:'display:inline-flex;gap:4px'});
|
|
174
|
+
parts.forEach(function(d){wrap.appendChild(createDriverBadge(d.trim()))});
|
|
175
|
+
return wrap;
|
|
176
|
+
}
|
|
177
|
+
var d=driver.trim();
|
|
178
|
+
var badge=el('span',{className:'driver-badge drv-'+d,style:'color:'+(colors[d]||'var(--text3)')},[
|
|
179
|
+
el('span',{className:'drv-icon'},icons[d]||'\u2699'),
|
|
180
|
+
document.createTextNode(labels[d]||d)
|
|
181
|
+
]);
|
|
182
|
+
return badge;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* ── Pool Distribution Summary ── */
|
|
186
|
+
var POOL_COLORS=['#6366f1','#22d3ee','#f59e0b','#10b981','#ef4444','#8b5cf6','#ec4899','#14b8a6'];
|
|
187
|
+
function buildPoolDistribution(tests){
|
|
188
|
+
var pools={};var total=0;
|
|
189
|
+
Object.keys(tests).forEach(function(n){
|
|
190
|
+
if(n==='__error')return;var t=tests[n];
|
|
191
|
+
if(!t.poolUrl)return;
|
|
192
|
+
var label=t.poolUrl.replace('ws://','').replace('wss://','');
|
|
193
|
+
if(!pools[label])pools[label]={count:0,passed:0,failed:0};
|
|
194
|
+
pools[label].count++;total++;
|
|
195
|
+
if(t.status==='passed'||t.success)pools[label].passed++;
|
|
196
|
+
if(t.status==='failed'||t.success===false)pools[label].failed++;
|
|
197
|
+
});
|
|
198
|
+
var keys=Object.keys(pools);
|
|
199
|
+
if(keys.length<2)return null;
|
|
200
|
+
var bar=el('div',{className:'pool-dist'});
|
|
201
|
+
var legend=el('div',{className:'pool-dist-legend'});
|
|
202
|
+
keys.forEach(function(k,i){
|
|
203
|
+
var pct=Math.round(pools[k].count/total*100);
|
|
204
|
+
var color=POOL_COLORS[i%POOL_COLORS.length];
|
|
205
|
+
var seg=el('div',{className:'pool-dist-seg'});
|
|
206
|
+
seg.style.flex=pools[k].count;seg.style.background=color;
|
|
207
|
+
seg.textContent=k+' ('+pools[k].count+')';
|
|
208
|
+
bar.appendChild(seg);
|
|
209
|
+
var lg=el('span',{},k+': '+pools[k].count+' tests ('+pct+'%)');
|
|
210
|
+
lg.style.cssText='display:inline-flex;align-items:center;gap:4px';
|
|
211
|
+
var dot=el('span',{});dot.style.cssText='width:8px;height:8px;border-radius:2px;background:'+color+';flex-shrink:0';
|
|
212
|
+
lg.insertBefore(dot,lg.firstChild);
|
|
213
|
+
legend.appendChild(lg);
|
|
214
|
+
});
|
|
215
|
+
return el('div',{style:'padding:4px 12px'},[bar,legend]);
|
|
216
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/* ══════════════════════════════════════════════════════════════════
|
|
2
|
+
Live Execution View
|
|
3
|
+
══════════════════════════════════════════════════════════════════ */
|
|
4
|
+
function clearFinishedLiveRuns(){for(var k in S.liveRuns){if(S.liveRuns[k].done||!S.liveRuns[k].on)delete S.liveRuns[k]}S.screencastTest=null;renderLive()}
|
|
5
|
+
function dismissLiveRun(rid){delete S.liveRuns[rid];renderLive()}
|
|
6
|
+
$('#liveClearBtn').addEventListener('click',clearFinishedLiveRuns);
|
|
7
|
+
|
|
8
|
+
// Screencast state
|
|
9
|
+
S.screencastTest=null;
|
|
10
|
+
|
|
11
|
+
$('#screencastSelect').addEventListener('change',function(){
|
|
12
|
+
S.screencastTest=this.value||null;
|
|
13
|
+
var img=$('#screencastImg'),ph=$('#screencastPlaceholder');
|
|
14
|
+
if(S.screencastTest){img.style.display='block';ph.style.display='none';img.src=''}
|
|
15
|
+
else{img.style.display='none';ph.style.display='flex'}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function updateScreencastSelect(){
|
|
19
|
+
var sel=$('#screencastSelect'),panel=$('#screencastPanel');
|
|
20
|
+
var runningTests=[];
|
|
21
|
+
for(var k in S.liveRuns){var r=S.liveRuns[k];for(var n in r.tests){if(n!=='__error'&&r.tests[n].status==='running')runningTests.push(n)}}
|
|
22
|
+
// Show panel if any run is active
|
|
23
|
+
var anyActive=false;for(var k2 in S.liveRuns)if(S.liveRuns[k2].on)anyActive=true;
|
|
24
|
+
panel.style.display=anyActive?'':'none';
|
|
25
|
+
// Rebuild options
|
|
26
|
+
var prev=sel.value;
|
|
27
|
+
while(sel.options.length>1)sel.remove(1);
|
|
28
|
+
runningTests.forEach(function(n){var o=document.createElement('option');o.value=n;o.textContent=n;sel.appendChild(o)});
|
|
29
|
+
// Auto-select first running test if nothing selected
|
|
30
|
+
if(!S.screencastTest&&runningTests.length>0){S.screencastTest=runningTests[0];sel.value=S.screencastTest;$('#screencastImg').style.display='block';$('#screencastPlaceholder').style.display='none'}
|
|
31
|
+
else if(S.screencastTest&&runningTests.indexOf(S.screencastTest)===-1){
|
|
32
|
+
// Current test finished — pick next running or clear
|
|
33
|
+
if(runningTests.length>0){S.screencastTest=runningTests[0];sel.value=S.screencastTest}
|
|
34
|
+
else{S.screencastTest=null;sel.value='';$('#screencastImg').style.display='none';$('#screencastPlaceholder').style.display='flex';$('#screencastPlaceholder').textContent='No running tests'}
|
|
35
|
+
}
|
|
36
|
+
else{sel.value=S.screencastTest||''}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function renderLive(){
|
|
40
|
+
var panel=$('#livePanel'),grid=$('#liveTests'),navLive=$('#navLive'),liveEmpty=$('#liveEmpty');
|
|
41
|
+
var runs=S.liveRuns;var runIds=Object.keys(runs);
|
|
42
|
+
|
|
43
|
+
if(runIds.length===0){panel.classList.remove('active');navLive.style.display='none';liveEmpty.style.display='block';$('#liveClearBtn').style.display='none';return}
|
|
44
|
+
|
|
45
|
+
navLive.style.display='';liveEmpty.style.display='none';panel.classList.add('active');
|
|
46
|
+
|
|
47
|
+
var gTotal=0,gCompleted=0,gPassed=0,gFailed=0,gActive=0,gRunning=false,gDone=true;
|
|
48
|
+
runIds.forEach(function(rid){var r=runs[rid];gTotal+=r.total;gCompleted+=r.completed;gPassed+=r.passed;gFailed+=r.failed;gActive+=r.active;if(r.on)gRunning=true;if(!r.done)gDone=false});
|
|
49
|
+
|
|
50
|
+
var badgeActive=0;
|
|
51
|
+
runIds.forEach(function(rid){var r=runs[rid];Object.keys(r.tests).forEach(function(n){if(n!=='__error'&&r.tests[n].status==='running')badgeActive++})});
|
|
52
|
+
$('#liveBadge').textContent=gRunning?badgeActive:gCompleted;
|
|
53
|
+
$('#liveBadge').style.background=gRunning?'var(--purple-dim)':gFailed>0?'var(--red-dim)':'var(--green-dim)';
|
|
54
|
+
$('#liveBadge').style.color=gRunning?'var(--purple)':gFailed>0?'var(--red)':'var(--green)';
|
|
55
|
+
|
|
56
|
+
$('#liveTotal').textContent=gTotal;$('#livePass').textContent=gPassed;$('#liveFail').textContent=gFailed;$('#liveActive').textContent=gActive;
|
|
57
|
+
$('#liveProgressFill').style.width=(gTotal>0?gCompleted/gTotal*100:0)+'%';
|
|
58
|
+
$('#liveProject').style.display='none';
|
|
59
|
+
|
|
60
|
+
var hasFinished=runIds.some(function(rid){return runs[rid].done||!runs[rid].on});
|
|
61
|
+
$('#liveClearBtn').style.display=hasFinished?'inline-block':'none';
|
|
62
|
+
|
|
63
|
+
var lbl=panel.querySelector('.live-header .label');
|
|
64
|
+
var anyStale=runIds.some(function(rid){return runs[rid].stale});
|
|
65
|
+
if(!gRunning&&gDone){
|
|
66
|
+
lbl.textContent=anyStale?'COMPLETED (connection lost)':gFailed>0?'COMPLETED WITH FAILURES':'ALL TESTS PASSED';
|
|
67
|
+
lbl.style.color=anyStale?'var(--yellow)':gFailed>0?'var(--red)':'var(--green)';
|
|
68
|
+
var dot=lbl.querySelector('.dot');if(dot)dot.remove();
|
|
69
|
+
$('#liveProgressFill').style.background=anyStale?'var(--yellow)':gFailed>0?'var(--red)':'var(--green)';
|
|
70
|
+
} else {
|
|
71
|
+
if(!lbl.querySelector('.dot')){lbl.textContent='';var d=el('span',{className:'dot'});lbl.appendChild(d);lbl.appendChild(document.createTextNode(' RUNNING'))}
|
|
72
|
+
lbl.style.color='var(--purple)';$('#liveProgressFill').style.background='var(--purple)';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
grid.textContent='';
|
|
76
|
+
runIds.forEach(function(rid){
|
|
77
|
+
var L=runs[rid];
|
|
78
|
+
var projLabel=L.project||(L.cwd?L.cwd.split('/').pop():'Run');
|
|
79
|
+
var runStatus=L.done?(L.failed>0?'fail':'pass'):'running';
|
|
80
|
+
var dismissBtn=null;
|
|
81
|
+
if(L.done||!L.on){dismissBtn=el('button',{className:'lr-dismiss',onclick:function(e){e.stopPropagation();dismissLiveRun(rid)}},'\u2715')}
|
|
82
|
+
grid.appendChild(el('div',{className:'lr-section-header '+runStatus},[
|
|
83
|
+
el('span',{className:'lr-project-name'},projLabel),createTriggerBadge(L.triggeredBy),
|
|
84
|
+
el('span',{className:'lr-section-stats'},[el('span',{},L.completed+'/'+L.total),L.failed>0?el('span',{style:'color:var(--red);margin-left:6px'},L.failed+' failed'):null,L.on?el('span',{className:'spinner-small',style:'margin-left:6px'}):null]),
|
|
85
|
+
dismissBtn
|
|
86
|
+
]));
|
|
87
|
+
|
|
88
|
+
var poolDist=buildPoolDistribution(L.tests);
|
|
89
|
+
if(poolDist)grid.appendChild(poolDist);
|
|
90
|
+
|
|
91
|
+
var testGrid=el('div',{className:'lr-test-grid'});
|
|
92
|
+
Object.keys(L.tests).forEach(function(name){
|
|
93
|
+
if(name==='__error')return;
|
|
94
|
+
var t=L.tests[name];var testKey=rid+'::'+name;
|
|
95
|
+
var iconText=t.status==='passed'?'\u2714':t.status==='failed'?'\u2718':'\u25CF';
|
|
96
|
+
var iconColor=t.status==='passed'?'color:var(--green)':t.status==='failed'?'color:var(--red)':'color:var(--purple)';
|
|
97
|
+
var meta='';
|
|
98
|
+
if(t.status==='running'){meta=t.actionType?('Step '+(t.actions||0)+'/'+(t.totalActions||'?')):'starting...';if(t.retry)meta='Retry '+t.retry}
|
|
99
|
+
else if(t.status==='passed'){meta=t.duration||'done'}
|
|
100
|
+
else if(t.status==='failed'){meta=t.error||'failed'}
|
|
101
|
+
|
|
102
|
+
var stepsEl=el('div',{className:'lt-actions'});
|
|
103
|
+
if(t.actionLog&&t.actionLog.length>0){
|
|
104
|
+
t.actionLog.forEach(function(a){
|
|
105
|
+
var detail=a.narrative||a.selector||a.value||a.text||'';
|
|
106
|
+
var durText=a.duration!=null?(a.duration<1000?a.duration+'ms':(a.duration/1000).toFixed(1)+'s'):'';
|
|
107
|
+
var retryBadge=null;
|
|
108
|
+
if(a.actionRetries&&a.actionRetries>0){retryBadge=el('span',{className:'badge flaky',style:'font-size:9px;padding:1px 5px;margin-left:4px'},'\u21BB x'+a.actionRetries)}
|
|
109
|
+
var stepCls='lt-step'+(a.isPoolLog?' pool-log':'');
|
|
110
|
+
stepsEl.appendChild(el('div',{className:stepCls},[
|
|
111
|
+
el('span',{className:'step-icon '+(a.isPoolLog?'':a.success?'ok':'fail')},a.isPoolLog?'\uD83D\uDD17':a.success?'\u2714':'\u2718'),
|
|
112
|
+
el('span',{className:'step-type'},a.isPoolLog?'pool':a.type),
|
|
113
|
+
el('span',{className:'step-detail'},a.isPoolLog?a.narrative:detail),
|
|
114
|
+
retryBadge,
|
|
115
|
+
a.isPoolLog?null:el('span',{className:'step-dur'},durText)
|
|
116
|
+
]));
|
|
117
|
+
});
|
|
118
|
+
if(t.status==='running'&&t.actions<t.totalActions){stepsEl.appendChild(el('div',{className:'lt-step'},[el('span',{className:'step-icon run spinner-small'}),el('span',{className:'step-type',style:'opacity:.6'},'waiting...')]))}
|
|
119
|
+
} else if(t.status==='running'){
|
|
120
|
+
stepsEl.appendChild(el('div',{className:'lt-step'},[el('span',{className:'step-icon run spinner-small'}),el('span',{className:'step-type',style:'opacity:.6'},'connecting...')]));
|
|
121
|
+
}
|
|
122
|
+
var isFinished=t.status==='passed'||t.status==='failed';
|
|
123
|
+
var isCollapsed=isFinished&&S.liveCollapsed.has(testKey);
|
|
124
|
+
var summaryEl=el('div',{className:'lt-summary'},[el('span',{className:'lt-dur'},t.duration||''),el('span',{className:'lt-expand'},isCollapsed?'\u25BC':'\u25B2')]);
|
|
125
|
+
|
|
126
|
+
var ssEl=null;
|
|
127
|
+
var allSS=(t.screenshots||[]).slice();
|
|
128
|
+
if(t.errorScreenshot)allSS.push(t.errorScreenshot);
|
|
129
|
+
if(allSS.length>0){
|
|
130
|
+
var ssOpen=S.liveSSOpen&&S.liveSSOpen.has(testKey);
|
|
131
|
+
var toggle=el('div',{className:'lt-screenshots-toggle'+(ssOpen?' open':'')},[el('span',{className:'ss-arrow'},'\u25B6'),el('span',{},'Screenshots ('+allSS.length+')')]);
|
|
132
|
+
var ssGridEl=el('div',{className:'lt-screenshots-grid'});
|
|
133
|
+
allSS.forEach(function(ssPath){
|
|
134
|
+
var fname=ssPath.split('/').pop();var isErr=t.errorScreenshot&&ssPath===t.errorScreenshot;
|
|
135
|
+
var thumb=el('div',{className:'lt-ss-thumb'});
|
|
136
|
+
var img=document.createElement('img');img.src='/api/image?path='+encodeURIComponent(ssPath);img.alt=fname;img.loading='lazy';
|
|
137
|
+
if(isErr)thumb.style.borderColor='var(--red)';
|
|
138
|
+
thumb.appendChild(img);
|
|
139
|
+
thumb.addEventListener('click',function(e){e.stopPropagation();openModal('/api/image?path='+encodeURIComponent(ssPath),fname)});
|
|
140
|
+
var labelEl=el('div',{className:'lt-ss-label'},[el('span',{style:'overflow:hidden;text-overflow:ellipsis;white-space:nowrap'},fname)]);
|
|
141
|
+
(function(lbl,sp){ssHash(sp).then(function(h){lbl.appendChild(createHashBadge(h))})})(labelEl,ssPath);
|
|
142
|
+
ssGridEl.appendChild(el('div',{},[thumb,labelEl]));
|
|
143
|
+
});
|
|
144
|
+
toggle.addEventListener('click',function(e){e.stopPropagation();if(S.liveSSOpen.has(testKey))S.liveSSOpen.delete(testKey);else S.liveSSOpen.add(testKey);toggle.classList.toggle('open');ssGridEl.style.display=ssGridEl.style.display==='grid'?'none':'grid'});
|
|
145
|
+
if(ssOpen)ssGridEl.style.display='grid';
|
|
146
|
+
ssEl=el('div',{className:'lt-screenshots'},[toggle,ssGridEl]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Screencast focus indicator
|
|
150
|
+
var scFocusBadge=null;
|
|
151
|
+
if(t.status==='running'){
|
|
152
|
+
var isFocused=S.screencastTest===name;
|
|
153
|
+
scFocusBadge=el('span',{className:'sc-focus-badge'+(isFocused?' active':''),title:'Watch this test',onclick:function(e){e.stopPropagation();S.screencastTest=name;$('#screencastSelect').value=name;$('#screencastImg').style.display='block';$('#screencastPlaceholder').style.display='none';renderLive()}},'\uD83C\uDFA5');
|
|
154
|
+
}
|
|
155
|
+
var serialBadge=t.serial?el('span',{className:'serial-badge'},'Serial'):null;
|
|
156
|
+
var poolBadge=t.poolUrl?el('span',{className:'pool-badge'},t.poolUrl.replace('ws://','').replace('wss://','')):null;
|
|
157
|
+
var card=el('div',{className:'live-test '+t.status+(isCollapsed?' collapsed':'')},[
|
|
158
|
+
el('div',{className:'lt-name'},[
|
|
159
|
+
t.status==='running'?el('span',{className:'spinner'}):el('span',{className:'lt-icon',style:iconColor},iconText),
|
|
160
|
+
document.createTextNode(' '+name),scFocusBadge,serialBadge,poolBadge,summaryEl
|
|
161
|
+
]),
|
|
162
|
+
el('div',{className:'lt-meta'},meta),stepsEl
|
|
163
|
+
]);
|
|
164
|
+
if(ssEl)card.appendChild(ssEl);
|
|
165
|
+
if(t.networkLogs&&t.networkLogs.length&&!isCollapsed){
|
|
166
|
+
var liveErrCount=t.networkLogs.filter(function(n){return n.status>=400}).length;
|
|
167
|
+
var liveNetHead=el('div',{className:'rd-net-head'},[el('span',{className:'net-arrow'},'\u25B6'),el('span',{className:'net-title'},'Network Requests'),el('div',{className:'net-stats'},[el('span',{className:'net-stat'},[document.createTextNode('Total: '),el('strong',null,String(t.networkLogs.length))]),liveErrCount?el('span',{className:'net-stat has-err'},[document.createTextNode('Errors: '),el('strong',null,String(liveErrCount))]):null])]);
|
|
168
|
+
var liveNetCols=el('div',{className:'rd-net-cols'},[el('span',{className:'col-e'},''),el('span',{className:'col-m'},'Method'),el('span',{className:'col-s'},'Status'),el('span',{className:'col-u'},'URL'),el('span',{className:'col-d'},'Time')]);
|
|
169
|
+
var liveNetBody=el('div',{className:'rd-net-body'},[liveNetCols]);
|
|
170
|
+
t.networkLogs.forEach(function(n){var built=buildNetRow(n);liveNetBody.appendChild(built.row);if(built.detail)liveNetBody.appendChild(built.detail)});
|
|
171
|
+
liveNetHead.addEventListener('click',function(e){e.stopPropagation();liveNetHead.classList.toggle('open')});
|
|
172
|
+
card.appendChild(el('div',{className:'rd-net-panel',style:'margin-top:6px'},[liveNetHead,liveNetBody]));
|
|
173
|
+
}
|
|
174
|
+
if(isFinished){card.addEventListener('click',function(e){if(window.getSelection().toString())return;if(S.liveCollapsed.has(testKey))S.liveCollapsed.delete(testKey);else S.liveCollapsed.add(testKey);renderLive()})}
|
|
175
|
+
testGrid.appendChild(card);
|
|
176
|
+
if(!isCollapsed)stepsEl.scrollTop=stepsEl.scrollHeight;
|
|
177
|
+
});
|
|
178
|
+
grid.appendChild(testGrid);
|
|
179
|
+
});
|
|
180
|
+
updateScreencastSelect();
|
|
181
|
+
}
|