@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.
Files changed (50) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/LICENSE +1 -1
  4. package/README.md +491 -225
  5. package/agents/test-creator.md +4 -2
  6. package/agents/test-improver.md +7 -4
  7. package/bin/cli.js +93 -19
  8. package/package.json +4 -3
  9. package/skills/e2e-testing/SKILL.md +5 -3
  10. package/skills/e2e-testing/references/action-types.md +35 -18
  11. package/skills/e2e-testing/references/test-json-format.md +23 -0
  12. package/skills/e2e-testing/references/troubleshooting.md +2 -26
  13. package/src/actions.js +181 -15
  14. package/src/config.js +6 -0
  15. package/src/dashboard.js +185 -9
  16. package/src/db.js +26 -0
  17. package/src/mcp-tools.js +238 -69
  18. package/src/module-analysis.js +247 -0
  19. package/src/module-resolver.js +35 -2
  20. package/src/narrate.js +33 -1
  21. package/src/pool-manager.js +46 -1
  22. package/src/pool.js +177 -20
  23. package/src/runner.js +144 -19
  24. package/src/visual-diff.js +74 -4
  25. package/src/websocket.js +14 -3
  26. package/src/wizard.js +184 -0
  27. package/templates/build-dashboard.js +3 -0
  28. package/templates/dashboard/js/api.js +60 -3
  29. package/templates/dashboard/js/init.js +46 -0
  30. package/templates/dashboard/js/keyboard.js +8 -7
  31. package/templates/dashboard/js/quicksearch.js +277 -0
  32. package/templates/dashboard/js/state.js +61 -7
  33. package/templates/dashboard/js/toast.js +1 -1
  34. package/templates/dashboard/js/utils.js +23 -2
  35. package/templates/dashboard/js/view-live.js +235 -42
  36. package/templates/dashboard/js/view-runs.js +469 -42
  37. package/templates/dashboard/js/view-tests.js +157 -16
  38. package/templates/dashboard/js/view-tools.js +234 -0
  39. package/templates/dashboard/js/view-watch.js +2 -2
  40. package/templates/dashboard/js/websocket.js +33 -3
  41. package/templates/dashboard/styles/base.css +489 -53
  42. package/templates/dashboard/styles/components.css +736 -84
  43. package/templates/dashboard/styles/view-live.css +459 -78
  44. package/templates/dashboard/styles/view-runs.css +826 -177
  45. package/templates/dashboard/styles/view-tests.css +440 -77
  46. package/templates/dashboard/styles/view-tools.css +206 -0
  47. package/templates/dashboard/styles/view-watch.css +198 -41
  48. package/templates/dashboard/template.html +356 -58
  49. package/templates/dashboard.html +5354 -722
  50. package/templates/docker-compose-lightpanda.yml +7 -0
@@ -1,57 +1,229 @@
1
1
  /* ══════════════════════════════════════════════════════════════════
2
2
  Live Execution View
3
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()}
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
- // 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'}
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
- else{sel.value=S.screencastTest||''}
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){panel.classList.remove('active');navLive.style.display='none';liveEmpty.style.display='block';$('#liveClearBtn').style.display='none';return}
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
- navLive.style.display='';liveEmpty.style.display='none';panel.classList.add('active');
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
- $('#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)';
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 focus indicator
150
- var scFocusBadge=null;
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
- 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');
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 card=el('div',{className:'live-test '+t.status+(isCollapsed?' collapsed':'')},[
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),scFocusBadge,serialBadge,poolBadge,summaryEl
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
- updateScreencastSelect();
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
  }