@matware/e2e-runner 1.3.1 → 1.5.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 +4 -4
- package/.claude-plugin/plugin.json +2 -2
- package/LICENSE +1 -1
- package/README.md +491 -225
- package/agents/test-creator.md +4 -2
- package/agents/test-improver.md +7 -4
- package/bin/cli.js +93 -19
- package/package.json +4 -3
- package/skills/e2e-testing/SKILL.md +5 -3
- package/skills/e2e-testing/references/action-types.md +35 -18
- package/skills/e2e-testing/references/test-json-format.md +23 -0
- package/skills/e2e-testing/references/troubleshooting.md +2 -26
- package/src/actions.js +181 -15
- package/src/config.js +6 -0
- package/src/dashboard.js +185 -9
- package/src/db.js +26 -0
- package/src/mcp-tools.js +238 -69
- package/src/module-analysis.js +247 -0
- package/src/module-resolver.js +35 -2
- package/src/narrate.js +33 -1
- package/src/pool-manager.js +46 -1
- package/src/pool.js +177 -20
- package/src/runner.js +144 -19
- package/src/visual-diff.js +74 -4
- package/src/websocket.js +14 -3
- package/src/wizard.js +184 -0
- package/templates/build-dashboard.js +3 -0
- package/templates/dashboard/js/api.js +60 -3
- package/templates/dashboard/js/init.js +46 -0
- package/templates/dashboard/js/keyboard.js +8 -7
- package/templates/dashboard/js/quicksearch.js +277 -0
- package/templates/dashboard/js/state.js +61 -7
- package/templates/dashboard/js/toast.js +1 -1
- package/templates/dashboard/js/utils.js +23 -2
- package/templates/dashboard/js/view-live.js +235 -42
- package/templates/dashboard/js/view-runs.js +469 -42
- package/templates/dashboard/js/view-tests.js +157 -16
- package/templates/dashboard/js/view-tools.js +234 -0
- package/templates/dashboard/js/view-watch.js +2 -2
- package/templates/dashboard/js/websocket.js +33 -3
- package/templates/dashboard/styles/base.css +489 -53
- package/templates/dashboard/styles/components.css +736 -84
- package/templates/dashboard/styles/view-live.css +459 -78
- package/templates/dashboard/styles/view-runs.css +826 -177
- package/templates/dashboard/styles/view-tests.css +440 -77
- package/templates/dashboard/styles/view-tools.css +206 -0
- package/templates/dashboard/styles/view-watch.css +198 -41
- package/templates/dashboard/template.html +356 -58
- package/templates/dashboard.html +5354 -722
- package/templates/docker-compose-lightpanda.yml +7 -0
|
@@ -1,57 +1,229 @@
|
|
|
1
1
|
/* ══════════════════════════════════════════════════════════════════
|
|
2
2
|
Live Execution View
|
|
3
3
|
══════════════════════════════════════════════════════════════════ */
|
|
4
|
-
function clearFinishedLiveRuns(){
|
|
5
|
-
|
|
4
|
+
function clearFinishedLiveRuns(){
|
|
5
|
+
for(var k in S.liveRuns){if(S.liveRuns[k].done||!S.liveRuns[k].on)delete S.liveRuns[k]}
|
|
6
|
+
S.screencastSel=null;S.screencastLast=null;
|
|
7
|
+
var im=$('#screencastImg');if(im){im.src='';im.style.display='none';var vp=im.closest('.screencast-viewport');if(vp)vp.classList.remove('has-frame')}
|
|
8
|
+
if(typeof resetFilmstrip==='function')resetFilmstrip();
|
|
9
|
+
renderLive();
|
|
10
|
+
}
|
|
11
|
+
function dismissLiveRun(rid){
|
|
12
|
+
if(S.screencastSel&&S.screencastSel.runId===rid)S.screencastSel=null;
|
|
13
|
+
if(S.screencastLast&&S.screencastLast.runId===rid)S.screencastLast=null;
|
|
14
|
+
delete S.liveRuns[rid];renderLive();
|
|
15
|
+
}
|
|
6
16
|
$('#liveClearBtn').addEventListener('click',clearFinishedLiveRuns);
|
|
7
17
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
18
|
+
/* Pick a test to screencast. Composite key {runId, name} so concurrent
|
|
19
|
+
runs with the same test name never collide into a single <img>. */
|
|
20
|
+
function selectScreencast(runId,name){
|
|
21
|
+
if(S.screencastSel&&S.screencastSel.runId===runId&&S.screencastSel.name===name){
|
|
22
|
+
// Same test clicked again — unselect (stop watching)
|
|
23
|
+
S.screencastSel=null;
|
|
24
|
+
}else{
|
|
25
|
+
S.screencastSel={runId:runId,name:name};
|
|
26
|
+
// Clear the previous frame so we don't briefly show another test's last frame
|
|
27
|
+
var im=$('#screencastImg');if(im){im.src='';im.style.display='none'}
|
|
28
|
+
}
|
|
29
|
+
renderLive();
|
|
30
|
+
}
|
|
31
|
+
function stopScreencast(){S.screencastSel=null;renderLive()}
|
|
32
|
+
|
|
33
|
+
var _scStopBtn=$('#screencastStopBtn');
|
|
34
|
+
if(_scStopBtn)_scStopBtn.addEventListener('click',stopScreencast);
|
|
35
|
+
|
|
36
|
+
/* Reset the live preview + filmstrip (used when the watched test changes so
|
|
37
|
+
two tests' frames never pile up in the same feed). */
|
|
38
|
+
function clearScreencastFrame(){
|
|
39
|
+
var im=$('#screencastImg');
|
|
40
|
+
if(im){im.src='';im.style.display='none';var vp=im.closest('.screencast-viewport');if(vp)vp.classList.remove('has-frame')}
|
|
41
|
+
S.screencastLast=null;
|
|
42
|
+
if(typeof resetFilmstrip==='function')resetFilmstrip();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Test chooser: "auto" follows the latest test; a specific value pins the feed
|
|
46
|
+
to that one test only. */
|
|
47
|
+
(function(){
|
|
48
|
+
var sel=$('#screencastTestSelect');if(!sel)return;
|
|
49
|
+
sel.addEventListener('change',function(){
|
|
50
|
+
if(this.value==='auto'){
|
|
51
|
+
S.screencastSel=null;S.screencastAuto=true;
|
|
52
|
+
}else{
|
|
53
|
+
var i=this.value.indexOf('::');
|
|
54
|
+
S.screencastSel={runId:this.value.slice(0,i),name:this.value.slice(i+2)};
|
|
55
|
+
}
|
|
56
|
+
clearScreencastFrame();
|
|
57
|
+
renderLive();
|
|
58
|
+
});
|
|
59
|
+
})();
|
|
60
|
+
|
|
61
|
+
/* Keep the chooser's options in sync with the live tests — but only rebuild
|
|
62
|
+
when the set actually changes, so it doesn't clobber the open dropdown. */
|
|
63
|
+
function syncScreencastSelect(){
|
|
64
|
+
var sel=$('#screencastTestSelect');if(!sel)return;
|
|
65
|
+
var items=[];
|
|
66
|
+
Object.keys(S.liveRuns).forEach(function(rid){
|
|
67
|
+
var run=S.liveRuns[rid];
|
|
68
|
+
Object.keys(run.tests||{}).forEach(function(n){
|
|
69
|
+
if(n==='__error')return;
|
|
70
|
+
items.push({key:rid+'::'+n,runId:rid,name:n,status:run.tests[n].status});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
items.sort(function(a,b){return (a.status==='running'?0:1)-(b.status==='running'?0:1)});
|
|
74
|
+
var curKey=S.screencastSel?(S.screencastSel.runId+'::'+S.screencastSel.name):'auto';
|
|
75
|
+
var sig=curKey+'|'+items.map(function(o){return o.key+':'+o.status}).join(',');
|
|
76
|
+
if(sel.getAttribute('data-sig')===sig)return;
|
|
77
|
+
sel.setAttribute('data-sig',sig);
|
|
78
|
+
sel.textContent='';
|
|
79
|
+
sel.appendChild(el('option',{value:'auto'},'Auto — latest test'));
|
|
80
|
+
items.forEach(function(o){
|
|
81
|
+
var mark=o.status==='running'?'● ':o.status==='passed'?'✓ ':o.status==='failed'?'✕ ':'· ';
|
|
82
|
+
sel.appendChild(el('option',{value:o.key},mark+o.name));
|
|
83
|
+
});
|
|
84
|
+
sel.value=curKey;
|
|
85
|
+
if(sel.value!==curKey)sel.value='auto';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Click the live preview → open it full-size in the lightbox. */
|
|
89
|
+
(function(){
|
|
90
|
+
var im=$('#screencastImg');
|
|
91
|
+
if(im)im.addEventListener('click',function(){
|
|
92
|
+
if(im.src&&im.src.indexOf('data:')===0&&typeof openModal==='function')openModal(im.src);
|
|
93
|
+
});
|
|
94
|
+
})();
|
|
95
|
+
|
|
96
|
+
/* Filmstrip — keep a small ring buffer of recent frames, throttled so a
|
|
97
|
+
high-rate screencast doesn't thrash the DOM. Each thumb opens full-size. */
|
|
98
|
+
var SC_FILM_MAX=24, SC_FILM_THROTTLE=600;
|
|
99
|
+
function pushFilmFrame(src,name,runId){
|
|
100
|
+
var now=Date.now();
|
|
101
|
+
if(now-(S._filmTs||0)<SC_FILM_THROTTLE)return;
|
|
102
|
+
S._filmTs=now;
|
|
103
|
+
S.screencastFilm.push({src:src,name:name||'',runId:runId||null,ts:now});
|
|
104
|
+
while(S.screencastFilm.length>SC_FILM_MAX)S.screencastFilm.shift();
|
|
105
|
+
renderFilmstrip();
|
|
106
|
+
}
|
|
107
|
+
/* Render the bottom band: recent frames in fixed slots that fit the width —
|
|
108
|
+
no horizontal scroll, so the strip stays put while frames pass through it.
|
|
109
|
+
Newest is the rightmost (marked LIVE). Any thumb opens full-size on click. */
|
|
110
|
+
function renderFilmstrip(){
|
|
111
|
+
var strip=$('#screencastFilm');if(!strip)return;
|
|
112
|
+
strip.textContent='';
|
|
113
|
+
var film=S.screencastFilm||[];
|
|
114
|
+
// Pinned to one test → show only its frames (safety net against mixing).
|
|
115
|
+
if(S.screencastSel){
|
|
116
|
+
film=film.filter(function(f){return f.runId===S.screencastSel.runId&&f.name===S.screencastSel.name});
|
|
117
|
+
}
|
|
118
|
+
if(!film.length){
|
|
119
|
+
strip.appendChild(el('div',{className:'screencast-film-empty'},
|
|
120
|
+
anyLiveRunning()?'Waiting for first frame…':'No frames yet'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Fit as many fixed-width slots as the band can show without scrolling.
|
|
124
|
+
var h=strip.clientHeight||150, gap=10;
|
|
125
|
+
var thumbW=Math.max(120,(h-28)*1.6);
|
|
126
|
+
var avail=(strip.clientWidth||900)-32;
|
|
127
|
+
var fit=Math.max(1,Math.floor((avail+gap)/(thumbW+gap)));
|
|
128
|
+
var start=Math.max(0,film.length-fit);
|
|
129
|
+
var shown=film.slice(start);
|
|
130
|
+
var live=anyLiveRunning();
|
|
131
|
+
shown.forEach(function(f,i){
|
|
132
|
+
var isLast=(i===shown.length-1);
|
|
133
|
+
var img=document.createElement('img');img.src=f.src;img.alt=f.name;img.loading='lazy';
|
|
134
|
+
var thumb=el('div',{className:'film-thumb'+(isLast&&live?' is-live':''),
|
|
135
|
+
title:(f.name||'frame')+' — click to enlarge',
|
|
136
|
+
onclick:(function(s){return function(){if(typeof openModal==='function')openModal(s)}})(f.src)},
|
|
137
|
+
[img,el('span',{className:'film-idx'},String(start+i+1))]);
|
|
138
|
+
strip.appendChild(thumb);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function resetFilmstrip(){S.screencastFilm=[];S._filmTs=0;renderFilmstrip()}
|
|
142
|
+
|
|
143
|
+
function scTestStatus(sel){
|
|
144
|
+
if(!sel)return null;
|
|
145
|
+
var r=S.liveRuns[sel.runId];if(!r)return 'gone';
|
|
146
|
+
var t=r.tests&&r.tests[sel.name];if(!t)return 'gone';
|
|
147
|
+
return t.status;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function updateScreencastUI(){
|
|
151
|
+
var panel=$('#screencastPanel');
|
|
152
|
+
var anyActive=false;for(var k in S.liveRuns)if(S.liveRuns[k].on)anyActive=true;
|
|
153
|
+
var ctxEl=$('#screencastContext'),img=$('#screencastImg'),ph=$('#screencastPlaceholder'),stopBtn=$('#screencastStopBtn');
|
|
154
|
+
var hasFrame=img&&img.src&&img.src.indexOf('data:')===0;
|
|
155
|
+
|
|
156
|
+
var pinned=S.screencastSel;
|
|
157
|
+
var auto=!pinned&&S.screencastAuto!==false;
|
|
158
|
+
|
|
159
|
+
// Show panel while a run is active, a test is pinned, or a frame is on screen.
|
|
160
|
+
panel.style.display=(anyActive||pinned||hasFrame)?'':'none';
|
|
161
|
+
|
|
162
|
+
// Idle: nothing pinned, auto off (or nothing ever shown) and no activity.
|
|
163
|
+
if(!pinned&&!auto&&!hasFrame){
|
|
164
|
+
if(ctxEl){ctxEl.textContent='';ctxEl.className='screencast-context idle'}
|
|
165
|
+
if(stopBtn)stopBtn.style.display='none';
|
|
166
|
+
if(img)img.style.display='none';
|
|
167
|
+
if(ph){ph.style.display='flex';ph.textContent=anyActive?'Waiting for first frame…':'No tests running'}
|
|
168
|
+
return;
|
|
35
169
|
}
|
|
36
|
-
|
|
170
|
+
|
|
171
|
+
// Subject: the pinned test, or (auto) the test of the last frame shown.
|
|
172
|
+
var sel=pinned||S.screencastLast;
|
|
173
|
+
var status=pinned?scTestStatus(pinned):(anyActive?'running':'ended');
|
|
174
|
+
var run=sel&&S.liveRuns[sel.runId];
|
|
175
|
+
var proj=(run&&(run.project||(run.cwd?run.cwd.split('/').pop():'')))||'';
|
|
176
|
+
|
|
177
|
+
if(ctxEl){
|
|
178
|
+
ctxEl.textContent='';
|
|
179
|
+
if(proj)ctxEl.appendChild(el('span',{className:'sc-ctx-proj'},proj));
|
|
180
|
+
if(sel)ctxEl.appendChild(el('span',{className:'sc-ctx-name'},sel.name));
|
|
181
|
+
var pillTxt,pillCls;
|
|
182
|
+
if(pinned){
|
|
183
|
+
pillTxt=status==='running'?'WATCHING':status==='passed'?'ENDED · PASSED':status==='failed'?'ENDED · FAILED':'GONE';
|
|
184
|
+
pillCls=status==='running'?'running':status==='passed'?'passed':status==='failed'?'failed':'gone';
|
|
185
|
+
}else{
|
|
186
|
+
pillTxt=anyActive?'AUTO · LIVE':'AUTO · LAST FRAME';
|
|
187
|
+
pillCls=anyActive?'running':'gone';
|
|
188
|
+
}
|
|
189
|
+
ctxEl.appendChild(el('span',{className:'sc-ctx-pill '+pillCls},pillTxt));
|
|
190
|
+
ctxEl.className='screencast-context '+(((pinned&&status==='running')||(!pinned&&anyActive))?'active':'ended');
|
|
191
|
+
}
|
|
192
|
+
// Stop button only when pinned — it returns to auto-follow.
|
|
193
|
+
if(stopBtn){stopBtn.style.display=pinned?'':'none';stopBtn.title='Stop watching (return to auto-follow)'}
|
|
194
|
+
|
|
195
|
+
if(hasFrame)img.style.display='block';
|
|
196
|
+
if(ph){
|
|
197
|
+
if(hasFrame){ph.style.display='none'}
|
|
198
|
+
else{ph.style.display='flex';ph.textContent=anyActive?'Waiting for first frame…':'No frames yet'}
|
|
199
|
+
}
|
|
200
|
+
// Keep the bottom band in sync (empty hint when no frames yet).
|
|
201
|
+
if(typeof renderFilmstrip==='function')renderFilmstrip();
|
|
202
|
+
if(typeof syncScreencastSelect==='function')syncScreencastSelect();
|
|
37
203
|
}
|
|
38
204
|
|
|
39
205
|
function renderLive(){
|
|
40
206
|
var panel=$('#livePanel'),grid=$('#liveTests'),navLive=$('#navLive'),liveEmpty=$('#liveEmpty');
|
|
41
207
|
var runs=S.liveRuns;var runIds=Object.keys(runs);
|
|
42
208
|
|
|
43
|
-
if(runIds.length===0){
|
|
209
|
+
if(runIds.length===0){
|
|
210
|
+
panel.classList.remove('active');liveEmpty.style.display='block';$('#liveClearBtn').style.display='none';
|
|
211
|
+
var lb=$('#liveBadge');lb.textContent='0';lb.className='badge idle';
|
|
212
|
+
syncTopbarLive(false,0,0);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
44
215
|
|
|
45
|
-
|
|
216
|
+
liveEmpty.style.display='none';panel.classList.add('active');
|
|
46
217
|
|
|
47
218
|
var gTotal=0,gCompleted=0,gPassed=0,gFailed=0,gActive=0,gRunning=false,gDone=true;
|
|
48
219
|
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
220
|
|
|
50
221
|
var badgeActive=0;
|
|
51
222
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
223
|
+
var lb2=$('#liveBadge');
|
|
224
|
+
lb2.textContent=gRunning?badgeActive:gCompleted;
|
|
225
|
+
lb2.className='badge '+(gRunning?'running':gFailed>0?'failed':'passed');
|
|
226
|
+
syncTopbarLive(gRunning,badgeActive,gFailed);
|
|
55
227
|
|
|
56
228
|
$('#liveTotal').textContent=gTotal;$('#livePass').textContent=gPassed;$('#liveFail').textContent=gFailed;$('#liveActive').textContent=gActive;
|
|
57
229
|
$('#liveProgressFill').style.width=(gTotal>0?gCompleted/gTotal*100:0)+'%';
|
|
@@ -146,18 +318,26 @@ function renderLive(){
|
|
|
146
318
|
ssEl=el('div',{className:'lt-screenshots'},[toggle,ssGridEl]);
|
|
147
319
|
}
|
|
148
320
|
|
|
149
|
-
// Screencast
|
|
150
|
-
var
|
|
321
|
+
// Screencast watch button \u2014 only on running tests. Composite-key aware.
|
|
322
|
+
var isWatched=S.screencastSel&&S.screencastSel.runId===rid&&S.screencastSel.name===name;
|
|
323
|
+
var scWatchBtn=null;
|
|
151
324
|
if(t.status==='running'){
|
|
152
|
-
|
|
153
|
-
|
|
325
|
+
scWatchBtn=el('button',{
|
|
326
|
+
className:'sc-watch-btn'+(isWatched?' active':''),
|
|
327
|
+
title:isWatched?'Stop watching':'Watch this test',
|
|
328
|
+
onclick:(function(_rid,_name){return function(e){e.stopPropagation();selectScreencast(_rid,_name)}})(rid,name)
|
|
329
|
+
},[
|
|
330
|
+
el('span',{className:'sc-eye'},isWatched?'\u25C9':'\u25CB'),
|
|
331
|
+
el('span',{className:'sc-watch-label'},isWatched?'WATCHING':'WATCH')
|
|
332
|
+
]);
|
|
154
333
|
}
|
|
155
334
|
var serialBadge=t.serial?el('span',{className:'serial-badge'},'Serial'):null;
|
|
156
335
|
var poolBadge=t.poolUrl?el('span',{className:'pool-badge'},t.poolUrl.replace('ws://','').replace('wss://','')):null;
|
|
157
|
-
var
|
|
336
|
+
var cardCls='live-test '+t.status+(isCollapsed?' collapsed':'')+(isWatched?' sc-watching':'');
|
|
337
|
+
var card=el('div',{className:cardCls},[
|
|
158
338
|
el('div',{className:'lt-name'},[
|
|
159
339
|
t.status==='running'?el('span',{className:'spinner'}):el('span',{className:'lt-icon',style:iconColor},iconText),
|
|
160
|
-
document.createTextNode(' '+name),
|
|
340
|
+
document.createTextNode(' '+name),scWatchBtn,serialBadge,poolBadge,summaryEl
|
|
161
341
|
]),
|
|
162
342
|
el('div',{className:'lt-meta'},meta),stepsEl
|
|
163
343
|
]);
|
|
@@ -177,5 +357,18 @@ function renderLive(){
|
|
|
177
357
|
});
|
|
178
358
|
grid.appendChild(testGrid);
|
|
179
359
|
});
|
|
180
|
-
|
|
360
|
+
updateScreencastUI();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* Sync the top bar Live shortcut: greyed when idle, pulsing purple when running */
|
|
364
|
+
function syncTopbarLive(running,activeCount,failedCount){
|
|
365
|
+
var pill=$('#topbarLive');if(!pill)return;
|
|
366
|
+
var count=$('#topbarLiveCount');
|
|
367
|
+
pill.classList.remove('idle','running','failed','passed');
|
|
368
|
+
if(running){pill.classList.add('running');count.textContent=activeCount}
|
|
369
|
+
else if(failedCount>0){pill.classList.add('failed');count.textContent=failedCount}
|
|
370
|
+
else if(activeCount===0&&!running){pill.classList.add('idle');count.textContent='0'}
|
|
371
|
+
else{pill.classList.add('passed');count.textContent=activeCount||0}
|
|
372
|
+
// Mirror the running count to the telemetry strip
|
|
373
|
+
if(typeof renderRunningTelemetry==='function')renderRunningTelemetry(running?activeCount:0);
|
|
181
374
|
}
|