@matware/e2e-runner 1.3.0 → 1.5.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/.claude-plugin/marketplace.json +37 -6
- package/.claude-plugin/plugin.json +17 -3
- package/LICENSE +190 -0
- package/README.md +151 -527
- package/agents/test-creator.md +4 -2
- package/agents/test-improver.md +5 -3
- package/bin/cli.js +84 -20
- package/commands/capture.md +45 -0
- package/package.json +3 -2
- package/skills/e2e-testing/SKILL.md +3 -2
- package/skills/e2e-testing/references/action-types.md +22 -4
- package/skills/e2e-testing/references/test-json-format.md +23 -0
- package/src/actions.js +321 -14
- package/src/ai-generate.js +81 -0
- package/src/app-pool.js +339 -0
- package/src/config.js +131 -7
- package/src/dashboard.js +209 -11
- package/src/db.js +74 -7
- package/src/index.js +6 -4
- package/src/learner-sqlite.js +154 -0
- package/src/learner.js +70 -3
- package/src/mcp-tools.js +259 -34
- package/src/module-analysis.js +247 -0
- package/src/module-resolver.js +35 -2
- package/src/narrate.js +42 -1
- package/src/pool-manager.js +68 -17
- package/src/pool.js +464 -37
- package/src/reporter.js +4 -1
- package/src/runner.js +410 -63
- package/src/visual-diff.js +515 -0
- 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 +62 -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 +20 -0
- package/templates/dashboard/js/view-live.js +240 -9
- package/templates/dashboard/js/view-runs.js +540 -94
- 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 +36 -0
- package/templates/dashboard/styles/base.css +489 -53
- package/templates/dashboard/styles/components.css +719 -77
- package/templates/dashboard/styles/view-live.css +463 -59
- package/templates/dashboard/styles/view-runs.css +793 -155
- 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 +369 -56
- package/templates/dashboard.html +5375 -901
- package/templates/docker-compose-lightpanda.yml +7 -0
|
@@ -1,26 +1,78 @@
|
|
|
1
1
|
/* ── Global State ── */
|
|
2
2
|
var S={
|
|
3
|
-
ws:null,project:null,view:'
|
|
3
|
+
ws:null,project:null,view:'overview',selectedRun:null,
|
|
4
4
|
liveRuns:{},liveCollapsed:new Set(),liveSSOpen:new Set(),
|
|
5
5
|
runFilter:{status:'all',search:''},
|
|
6
6
|
lastLearningsData:null,
|
|
7
|
-
highlightedRunIdx:-1
|
|
7
|
+
highlightedRunIdx:-1,
|
|
8
|
+
testsSearch:'',testsExpanded:new Set(),
|
|
9
|
+
/* Screencast selection: {runId, name} | null. Composite to avoid name
|
|
10
|
+
collisions between concurrent runs. When null, the live preview
|
|
11
|
+
auto-follows the most recent frame from any running test. */
|
|
12
|
+
screencastSel:null,
|
|
13
|
+
/* Auto-follow latest frame when no test is explicitly pinned. */
|
|
14
|
+
screencastAuto:true,
|
|
15
|
+
/* {runId, name} of the test whose frame is currently shown in auto mode. */
|
|
16
|
+
screencastLast:null,
|
|
17
|
+
/* Ring buffer of recent frames for the filmstrip: {src, name, ts}. */
|
|
18
|
+
screencastFilm:[],
|
|
19
|
+
_filmTs:0
|
|
8
20
|
};
|
|
21
|
+
function screencastKey(s){return s?(s.runId+'::'+s.name):null}
|
|
9
22
|
|
|
10
|
-
/* ──
|
|
23
|
+
/* ── Section labels (used in top bar breadcrumb) ── */
|
|
24
|
+
var VIEW_LABELS={
|
|
25
|
+
overview:'Overview',
|
|
26
|
+
live:'Live',
|
|
27
|
+
run:'Run',
|
|
28
|
+
investigate:'Investigate',
|
|
29
|
+
insights:'Insights',
|
|
30
|
+
tools:'Tools'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/* ── Navigation ──
|
|
34
|
+
Nav items may carry an optional data-tab attribute that activates
|
|
35
|
+
a sub-tab inside the destination view (used by promoted sub-items
|
|
36
|
+
like Suites/Modules/Variables under "Test Definitions"). */
|
|
11
37
|
$$('.nav-item').forEach(function(n){
|
|
12
38
|
n.addEventListener('click',function(){
|
|
13
|
-
showView(n.dataset.view);
|
|
39
|
+
showView(n.dataset.view,n.dataset.tab);
|
|
14
40
|
});
|
|
15
41
|
});
|
|
16
|
-
function showView(v){
|
|
42
|
+
function showView(v,subTab){
|
|
17
43
|
S.view=v;
|
|
18
|
-
|
|
44
|
+
// Match active state by (view + optional tab) so sub-items don't all light
|
|
45
|
+
// up just because they share the same parent view.
|
|
46
|
+
$$('.nav-item').forEach(function(n){
|
|
47
|
+
var sameView=n.dataset.view===v;
|
|
48
|
+
var sameTab=(n.dataset.tab||'')===(subTab||'');
|
|
49
|
+
n.classList.toggle('active',sameView&&sameTab);
|
|
50
|
+
});
|
|
19
51
|
$$('.view').forEach(function(x){x.classList.remove('active')});
|
|
20
52
|
var viewEl=$('#view-'+v);
|
|
21
53
|
if(viewEl)viewEl.classList.add('active');
|
|
22
|
-
|
|
54
|
+
// Activate the requested sub-tab inside the view
|
|
55
|
+
if(subTab&&viewEl){
|
|
56
|
+
var btn=viewEl.querySelector('.tab-btn[data-tab="'+subTab+'"]');
|
|
57
|
+
if(btn&&!btn.classList.contains('active'))btn.click();
|
|
58
|
+
}
|
|
59
|
+
if(v==='overview'&&typeof startWatchPolling==='function')startWatchPolling();
|
|
23
60
|
else if(typeof stopWatchPolling==='function')stopWatchPolling();
|
|
61
|
+
// Re-render Live when switching to it so empty/active panel state matches reality
|
|
62
|
+
if(v==='live'&&typeof renderLive==='function')renderLive();
|
|
63
|
+
updateBreadcrumb();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* ── Breadcrumb (top bar) ── */
|
|
67
|
+
function updateBreadcrumb(subLabel){
|
|
68
|
+
var bc=$('#topbarBreadcrumb');if(!bc)return;
|
|
69
|
+
var label=VIEW_LABELS[S.view]||S.view;
|
|
70
|
+
bc.textContent='';
|
|
71
|
+
bc.appendChild(el('span',{className:'topbar-section'},label));
|
|
72
|
+
if(subLabel){
|
|
73
|
+
bc.appendChild(el('span',{className:'topbar-sep'},'›'));
|
|
74
|
+
bc.appendChild(el('span',{className:'topbar-subsection'},subLabel));
|
|
75
|
+
}
|
|
24
76
|
}
|
|
25
77
|
|
|
26
78
|
/* ── Inner Tabs ── */
|
|
@@ -34,6 +86,8 @@ function initTabs(){
|
|
|
34
86
|
container.querySelectorAll('.tab-pane').forEach(function(p){p.classList.remove('active')});
|
|
35
87
|
var pane=container.querySelector('#'+btn.dataset.tab);
|
|
36
88
|
if(pane)pane.classList.add('active');
|
|
89
|
+
var label=(btn.firstChild&&btn.firstChild.nodeType===3?btn.firstChild.nodeValue:btn.textContent).trim();
|
|
90
|
+
updateBreadcrumb(label);
|
|
37
91
|
});
|
|
38
92
|
});
|
|
39
93
|
});
|
|
@@ -18,7 +18,7 @@ function showToast(message,type,timeout){
|
|
|
18
18
|
function showEnrichedToast(message,type){
|
|
19
19
|
var container=$('#toastContainer');
|
|
20
20
|
var icons={success:'\u2714',error:'\u2718',info:'\u2139'};
|
|
21
|
-
var t=el('div',{className:'toast clickable '+type,onclick:function(){showView('
|
|
21
|
+
var t=el('div',{className:'toast clickable '+type,onclick:function(){showView('insights')}},[
|
|
22
22
|
el('span',null,icons[type]||''),
|
|
23
23
|
el('span',null,message)
|
|
24
24
|
]);
|
|
@@ -162,6 +162,26 @@ function createTriggerBadge(source){
|
|
|
162
162
|
return badge;
|
|
163
163
|
}
|
|
164
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
|
+
|
|
165
185
|
/* ── Pool Distribution Summary ── */
|
|
166
186
|
var POOL_COLORS=['#6366f1','#22d3ee','#f59e0b','#10b981','#ef4444','#8b5cf6','#ec4899','#14b8a6'];
|
|
167
187
|
function buildPoolDistribution(tests){
|
|
@@ -1,26 +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
|
|
|
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;
|
|
169
|
+
}
|
|
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();
|
|
203
|
+
}
|
|
204
|
+
|
|
8
205
|
function renderLive(){
|
|
9
206
|
var panel=$('#livePanel'),grid=$('#liveTests'),navLive=$('#navLive'),liveEmpty=$('#liveEmpty');
|
|
10
207
|
var runs=S.liveRuns;var runIds=Object.keys(runs);
|
|
11
208
|
|
|
12
|
-
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
|
+
}
|
|
13
215
|
|
|
14
|
-
|
|
216
|
+
liveEmpty.style.display='none';panel.classList.add('active');
|
|
15
217
|
|
|
16
218
|
var gTotal=0,gCompleted=0,gPassed=0,gFailed=0,gActive=0,gRunning=false,gDone=true;
|
|
17
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});
|
|
18
220
|
|
|
19
221
|
var badgeActive=0;
|
|
20
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++})});
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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);
|
|
24
227
|
|
|
25
228
|
$('#liveTotal').textContent=gTotal;$('#livePass').textContent=gPassed;$('#liveFail').textContent=gFailed;$('#liveActive').textContent=gActive;
|
|
26
229
|
$('#liveProgressFill').style.width=(gTotal>0?gCompleted/gTotal*100:0)+'%';
|
|
@@ -115,12 +318,26 @@ function renderLive(){
|
|
|
115
318
|
ssEl=el('div',{className:'lt-screenshots'},[toggle,ssGridEl]);
|
|
116
319
|
}
|
|
117
320
|
|
|
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;
|
|
324
|
+
if(t.status==='running'){
|
|
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
|
+
]);
|
|
333
|
+
}
|
|
118
334
|
var serialBadge=t.serial?el('span',{className:'serial-badge'},'Serial'):null;
|
|
119
335
|
var poolBadge=t.poolUrl?el('span',{className:'pool-badge'},t.poolUrl.replace('ws://','').replace('wss://','')):null;
|
|
120
|
-
var
|
|
336
|
+
var cardCls='live-test '+t.status+(isCollapsed?' collapsed':'')+(isWatched?' sc-watching':'');
|
|
337
|
+
var card=el('div',{className:cardCls},[
|
|
121
338
|
el('div',{className:'lt-name'},[
|
|
122
339
|
t.status==='running'?el('span',{className:'spinner'}):el('span',{className:'lt-icon',style:iconColor},iconText),
|
|
123
|
-
document.createTextNode(' '+name),serialBadge,poolBadge,summaryEl
|
|
340
|
+
document.createTextNode(' '+name),scWatchBtn,serialBadge,poolBadge,summaryEl
|
|
124
341
|
]),
|
|
125
342
|
el('div',{className:'lt-meta'},meta),stepsEl
|
|
126
343
|
]);
|
|
@@ -140,4 +357,18 @@ function renderLive(){
|
|
|
140
357
|
});
|
|
141
358
|
grid.appendChild(testGrid);
|
|
142
359
|
});
|
|
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);
|
|
143
374
|
}
|