@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
@@ -14,6 +14,10 @@ function triggerRun(suite,projectId){
14
14
  function renderPool(d){
15
15
  if(!d)return;
16
16
  var poolList=$('#poolList');
17
+ // Telemetry strip — driver + sessions
18
+ var teleDriver=(d.pools&&d.pools[0]&&d.pools[0].driver)||d.driver||'';
19
+ var teleAvail=false;
20
+ var teleSessNow=0,teleSessMax=0;
17
21
  if(d.pools&&d.pools.length>1){
18
22
  var anyAvail=d.availableCount>0;
19
23
  $('#poolDot').className='pool-dot '+(anyAvail?'on':'off');
@@ -29,34 +33,87 @@ function renderPool(d){
29
33
  var sess=el('span',{className:'pool-sessions'},(p.running||0)+'/'+(p.maxConcurrent||0));
30
34
  poolList.appendChild(el('div',{className:'pool-item'},[dot,name,status,sess]));
31
35
  });
36
+ teleAvail=anyAvail;teleSessNow=d.totalRunning||0;teleSessMax=d.totalMaxConcurrent||0;
37
+ teleDriver=teleDriver||(d.pools.length+' pools');
32
38
  }else if(d.pools&&d.pools.length===1){
33
39
  var p=d.pools[0];
34
40
  $('#poolDot').className='pool-dot '+(p.error||!p.available?'off':'on');
35
41
  $('#poolLabel').textContent=p.error?'offline':p.available?'ready':'busy';
36
42
  $('#poolSessions').textContent=(p.running||0)+'/'+(p.maxConcurrent||0);
37
43
  poolList.style.display='none';
44
+ teleAvail=!p.error&&p.available;teleSessNow=p.running||0;teleSessMax=p.maxConcurrent||0;
45
+ teleDriver=teleDriver||p.driver||'cdp';
38
46
  }else{
39
47
  $('#poolDot').className='pool-dot '+(d.error||!d.available?'off':'on');
40
48
  $('#poolLabel').textContent=d.error?'offline':d.available?'ready':'busy';
41
49
  $('#poolSessions').textContent=(d.running||0)+'/'+(d.maxConcurrent||0);
42
50
  poolList.style.display='none';
51
+ teleAvail=!d.error&&d.available;teleSessNow=d.running||0;teleSessMax=d.maxConcurrent||0;
43
52
  }
53
+ // Telemetry pills (best-effort — elements may not exist on older templates)
54
+ var dotEl=$('#telePoolDot');if(dotEl)dotEl.className='tele-pill-dot '+(teleAvail?'on':'off');
55
+ var valEl=$('#telePoolValue');if(valEl)valEl.textContent=teleDriver||'--';
56
+ var sessEl=$('#teleSessionsValue');if(sessEl)sessEl.textContent=teleSessNow+'/'+teleSessMax;
57
+ }
58
+ function renderRunningTelemetry(n){
59
+ var v=$('#teleRunningValue');if(!v)return;
60
+ v.textContent=String(n||0);
61
+ var pill=$('#teleRunning');if(pill)pill.classList.toggle('has-running',(n||0)>0);
62
+ }
63
+ function refreshTodayTelemetry(){
64
+ var v=$('#teleTodayValue');if(!v)return;
65
+ var url=S.project?'/api/db/projects/'+S.project+'/runs':'/api/db/runs';
66
+ api(url).then(function(rows){
67
+ if(!Array.isArray(rows))return;
68
+ var today=new Date();today.setHours(0,0,0,0);var t=today.getTime();
69
+ var c=0;
70
+ rows.forEach(function(r){
71
+ var d=r.generated_at||r.started_at||r.created_at;
72
+ if(!d)return;
73
+ var ts=new Date(d).getTime();
74
+ if(ts>=t)c++;
75
+ });
76
+ v.textContent=String(c);
77
+ }).catch(function(){});
78
+ }
79
+ function refreshStatus(){
80
+ api('/api/status').then(function(d){
81
+ renderPool(d.pool);
82
+ // Telemetry: running count from dashboard.running flag fallback
83
+ if(d.dashboard&&typeof d.dashboard.runningTests==='number'){
84
+ renderRunningTelemetry(d.dashboard.runningTests);
85
+ }
86
+ }).catch(function(){});
44
87
  }
45
- function refreshStatus(){api('/api/status').then(function(d){renderPool(d.pool)}).catch(function(){})}
46
88
 
47
89
  /* ── Projects ── */
48
90
  function refreshProjects(){
49
91
  api('/api/db/projects').then(function(projects){
50
- var sel=$('#projectSelect'),prev=sel.value;
92
+ var sel=$('#projectSelect');
93
+ // Prefer in-memory state, then the persisted selection, then whatever the
94
+ // browser restored into the <select> on reload.
95
+ var saved=null;try{saved=localStorage.getItem('e2e-project')}catch(e){}
96
+ var prev=(S.project!=null?String(S.project):'')||saved||sel.value;
51
97
  while(sel.options.length>1)sel.remove(1);
52
98
  if(Array.isArray(projects))projects.forEach(function(p){
53
99
  var o=document.createElement('option');o.value=p.id;o.textContent=p.name;sel.appendChild(o);
54
100
  });
55
- sel.value=prev||'';
101
+ // Only keep the previous value if it still maps to a real option.
102
+ var valid=prev&&Array.prototype.some.call(sel.options,function(o){return o.value===prev});
103
+ sel.value=valid?prev:'';
104
+ // Sync app state to the restored <select> value. The browser restores the
105
+ // dropdown's visual value across reloads, but never fires a 'change' event,
106
+ // so S.project would otherwise stay null and views render "select a project".
107
+ var resolved=sel.value?parseInt(sel.value,10):null;
108
+ if(resolved!==S.project){
109
+ S.project=resolved;
110
+ refreshRuns();refreshSuites();refreshScreenshots();refreshLearnings();refreshWatch();
111
+ }
56
112
  }).catch(function(){});
57
113
  }
58
114
  $('#projectSelect').addEventListener('change',function(){
59
115
  S.project=this.value?parseInt(this.value,10):null;
60
116
  S.selectedRun=null;
117
+ try{S.project!=null?localStorage.setItem('e2e-project',String(S.project)):localStorage.removeItem('e2e-project')}catch(e){}
61
118
  refreshRuns();refreshSuites();refreshScreenshots();refreshLearnings();refreshWatch();
62
119
  });
@@ -11,3 +11,49 @@ refreshScreenshots();
11
11
  refreshLearnings();
12
12
  refreshVariables();
13
13
  startWatchPolling();
14
+ updateBreadcrumb();
15
+ syncTopbarLive(false,0,0);
16
+ if(typeof refreshTodayTelemetry==='function'){
17
+ refreshTodayTelemetry();
18
+ setInterval(refreshTodayTelemetry,30000);
19
+ }
20
+ // Keep pool telemetry fresh independent of the WS pool stream
21
+ setInterval(refreshStatus,8000);
22
+
23
+ /* ── Top bar handlers ── */
24
+ (function(){
25
+ var liveBtn=$('#topbarLive');
26
+ if(liveBtn)liveBtn.addEventListener('click',function(){showView('live')});
27
+ var runBtn=$('#topbarRunBtn');
28
+ if(runBtn)runBtn.addEventListener('click',function(){
29
+ if(typeof triggerRun==='function')triggerRun();
30
+ });
31
+ })();
32
+
33
+ /* ── Screencast toggle persistence (default ON) ── */
34
+ (function(){
35
+ var sc=$('#screencastToggle');
36
+ if(!sc)return;
37
+ var saved=null;try{saved=localStorage.getItem('e2e-screencast')}catch(e){}
38
+ sc.checked=saved===null?true:saved==='1';
39
+ sc.addEventListener('change',function(){try{localStorage.setItem('e2e-screencast',sc.checked?'1':'0')}catch(e){}});
40
+ })();
41
+
42
+ /* ── Theme toggle ── */
43
+ (function(){
44
+ var btn=$('#themeToggle');
45
+ var lbl=$('#themeToggleLabel');
46
+ if(!btn)return;
47
+ function syncLabel(){
48
+ var t=document.documentElement.getAttribute('data-theme')||'dark';
49
+ if(lbl)lbl.textContent=(t==='dark'?'Light':'Dark');
50
+ }
51
+ syncLabel();
52
+ btn.addEventListener('click',function(){
53
+ var cur=document.documentElement.getAttribute('data-theme')||'dark';
54
+ var next=cur==='dark'?'light':'dark';
55
+ document.documentElement.setAttribute('data-theme',next);
56
+ try{localStorage.setItem('e2e-theme',next)}catch(e){}
57
+ syncLabel();
58
+ });
59
+ })();
@@ -1,5 +1,5 @@
1
1
  /* ══════════════════════════════════════════════════════════════════
2
- Keyboard Shortcuts (Updated: 1=Watch, 2=Tests, 3=Runs, 4=Live)
2
+ Keyboard Shortcuts (1=Overview, 2=Live, 3=Run, 4=Investigate, 5=Insights)
3
3
  ══════════════════════════════════════════════════════════════════ */
4
4
  document.addEventListener('keydown',function(e){
5
5
  var tag=document.activeElement.tagName;
@@ -20,16 +20,17 @@ document.addEventListener('keydown',function(e){
20
20
  return;
21
21
  }
22
22
  if(e.key==='?'){$('#kbModal').classList.toggle('open');return}
23
- var viewMap={'1':'watch','2':'tests','3':'runs','4':'live'};
23
+ var viewMap={'1':'overview','2':'live','3':'run','4':'investigate','5':'insights'};
24
24
  if(viewMap[e.key]){showView(viewMap[e.key]);return}
25
25
  if(e.key==='r'){
26
- if(S.view==='watch')refreshWatch();
27
- else if(S.view==='tests'){refreshSuites();refreshVariables()}
28
- else if(S.view==='runs'){refreshRuns();refreshScreenshots();refreshLearnings()}
26
+ if(S.view==='overview')refreshWatch();
27
+ else if(S.view==='run'){refreshSuites();refreshVariables()}
28
+ else if(S.view==='investigate'){refreshRuns();refreshScreenshots();if(typeof refreshNetwork==='function')refreshNetwork()}
29
+ else if(S.view==='insights')refreshLearnings();
29
30
  else if(S.view==='live')renderLive();
30
31
  return;
31
32
  }
32
- if(S.view==='runs'&&(e.key==='j'||e.key==='k')){
33
+ if(S.view==='investigate'&&(e.key==='j'||e.key==='k')){
33
34
  var visible=_allRunRows.filter(function(item){return item.tr.style.display!=='none'});
34
35
  if(!visible.length)return;
35
36
  if(e.key==='j')S.highlightedRunIdx=Math.min(S.highlightedRunIdx+1,visible.length-1);
@@ -37,7 +38,7 @@ document.addEventListener('keydown',function(e){
37
38
  visible.forEach(function(item,i){if(i===S.highlightedRunIdx){item.tr.classList.add('selected');item.tr.scrollIntoView({block:'nearest'})}else item.tr.classList.remove('selected')});
38
39
  return;
39
40
  }
40
- if(S.view==='runs'&&e.key==='Enter'){
41
+ if(S.view==='investigate'&&e.key==='Enter'){
41
42
  var visible2=_allRunRows.filter(function(item){return item.tr.style.display!=='none'});
42
43
  if(S.highlightedRunIdx>=0&&S.highlightedRunIdx<visible2.length){visible2[S.highlightedRunIdx].tr.click()}
43
44
  return;
@@ -0,0 +1,277 @@
1
+ /* ══════════════════════════════════════════════════════════════════
2
+ Quick Search palette — Ctrl/⌘+K (or /) to open, searches across
3
+ suites, tests within suites, and reusable modules. Jumps to the
4
+ right view + tab on Enter / click.
5
+ ══════════════════════════════════════════════════════════════════ */
6
+
7
+ var QS = { index: [], filtered: [], active: 0, lastFetch: 0 };
8
+
9
+ function qsModalEl(){return document.getElementById('qsModal')}
10
+ function qsOpen(){
11
+ var m=qsModalEl();if(!m)return;
12
+ m.classList.add('open');m.setAttribute('aria-hidden','false');
13
+ var inp=document.getElementById('qsInput');
14
+ if(inp){inp.value='';inp.focus()}
15
+ // Refresh index opportunistically (cached for 20s)
16
+ if(Date.now()-QS.lastFetch>20000)qsBuildIndex();
17
+ else qsRender('');
18
+ }
19
+ function qsClose(){
20
+ var m=qsModalEl();if(!m)return;
21
+ m.classList.remove('open');m.setAttribute('aria-hidden','true');
22
+ }
23
+
24
+ /* Build a flat index of suites, modules and tests across all projects. */
25
+ function qsBuildIndex(){
26
+ var empty=document.getElementById('qsEmpty');
27
+ if(empty)empty.textContent='Loading index...';
28
+ api('/api/db/projects').then(function(projects){
29
+ if(!Array.isArray(projects))projects=[];
30
+ var pending=projects.length*2;
31
+ if(pending===0){QS.index=[];QS.lastFetch=Date.now();qsRender('');return}
32
+ var idx=[];
33
+ projects.forEach(function(proj){
34
+ api('/api/db/projects/'+proj.id+'/suites').then(function(suites){
35
+ if(Array.isArray(suites)){
36
+ suites.forEach(function(s){
37
+ idx.push({
38
+ kind:'suite',
39
+ name:s.name,
40
+ sub:proj.name,
41
+ meta:(s.testCount||0)+' tests',
42
+ project:proj,
43
+ suite:s,
44
+ });
45
+ (s.tests||[]).forEach(function(t){
46
+ idx.push({
47
+ kind:'test',
48
+ name:t.name||'(unnamed test)',
49
+ sub:proj.name+' › '+s.name,
50
+ meta:(t.actionCount||(t.actions&&t.actions.length)||0)+' steps',
51
+ project:proj,
52
+ suite:s,
53
+ test:t,
54
+ });
55
+ });
56
+ });
57
+ }
58
+ }).catch(function(){}).then(function(){
59
+ pending--;if(pending===0){QS.index=idx;QS.lastFetch=Date.now();qsRender('')}
60
+ });
61
+ api('/api/db/projects/'+proj.id+'/modules').then(function(modules){
62
+ if(Array.isArray(modules)){
63
+ modules.forEach(function(m){
64
+ idx.push({
65
+ kind:'module',
66
+ name:m.name,
67
+ sub:proj.name+(m.description?' — '+m.description:''),
68
+ meta:(m.actionCount||0)+' actions'+(m.params&&m.params.length?' · '+m.params.length+' params':''),
69
+ project:proj,
70
+ module:m,
71
+ });
72
+ });
73
+ }
74
+ }).catch(function(){}).then(function(){
75
+ pending--;if(pending===0){QS.index=idx;QS.lastFetch=Date.now();qsRender('')}
76
+ });
77
+ });
78
+ }).catch(function(){
79
+ QS.index=[];QS.lastFetch=Date.now();qsRender('');
80
+ });
81
+ }
82
+
83
+ /* Fuzzy-ish scoring: subsequence match + bonuses for word starts and exact. */
84
+ function qsScore(text,q){
85
+ if(!q)return 0;
86
+ text=(text||'').toLowerCase();q=q.toLowerCase();
87
+ if(text===q)return 1000;
88
+ if(text.indexOf(q)===0)return 700;
89
+ var i=text.indexOf(q);
90
+ if(i>=0){
91
+ // Word-boundary bonus
92
+ var prev=i>0?text.charAt(i-1):'';
93
+ var wb=prev===' '||prev==='-'||prev==='_'||prev==='.'||prev==='/'||prev===':';
94
+ return 400+(wb?80:0);
95
+ }
96
+ // Subsequence fallback
97
+ var ti=0,qi=0;
98
+ while(ti<text.length&&qi<q.length){
99
+ if(text.charAt(ti)===q.charAt(qi))qi++;
100
+ ti++;
101
+ }
102
+ return qi===q.length?100:0;
103
+ }
104
+
105
+ function qsEscape(s){
106
+ return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
107
+ }
108
+ function qsHighlight(name,q){
109
+ if(!q)return name;
110
+ var lc=name.toLowerCase();var lq=q.toLowerCase();
111
+ var i=lc.indexOf(lq);
112
+ if(i<0)return name;
113
+ return name.slice(0,i)+'<mark>'+name.slice(i,i+q.length)+'</mark>'+name.slice(i+q.length);
114
+ }
115
+
116
+ function qsRender(q){
117
+ var results=document.getElementById('qsResults');
118
+ var empty=document.getElementById('qsEmpty');
119
+ var modal=qsModalEl();if(!results||!modal)return;
120
+ results.textContent='';
121
+ if(!QS.index.length){
122
+ modal.classList.remove('has-results');
123
+ if(empty)empty.textContent=QS.lastFetch?'No suites or modules indexed yet.':'Loading index...';
124
+ QS.filtered=[];QS.active=0;return;
125
+ }
126
+ var query=(q||'').trim();
127
+ var scored=QS.index.map(function(it){
128
+ return {item:it,score:query?qsScore(it.name,query)+0.5*qsScore(it.sub||'',query):1};
129
+ }).filter(function(s){return s.score>0});
130
+ scored.sort(function(a,b){return b.score-a.score});
131
+ var top=scored.slice(0,40);
132
+ if(!top.length){
133
+ modal.classList.remove('has-results');
134
+ if(empty)empty.textContent='No matches for "'+query+'"';
135
+ QS.filtered=[];QS.active=0;return;
136
+ }
137
+ modal.classList.add('has-results');
138
+ QS.filtered=top.map(function(s){return s.item});
139
+ QS.active=0;
140
+ // Group by kind in display
141
+ var groups={suite:[],test:[],module:[]};
142
+ QS.filtered.forEach(function(it,idx){groups[it.kind].push({item:it,idx:idx})});
143
+ var labelMap={suite:'Suites',test:'Tests',module:'Modules'};
144
+ ['suite','test','module'].forEach(function(k){
145
+ if(!groups[k].length)return;
146
+ results.appendChild(el('div',{className:'qs-group-label'},labelMap[k]));
147
+ groups[k].forEach(function(entry){
148
+ var it=entry.item;var idx=entry.idx;
149
+ var nameEl=el('div',{className:'qs-item-name'});
150
+ nameEl.innerHTML=qsHighlight(qsEscape(it.name),qsEscape(query));
151
+ var row=el('div',{className:'qs-item',dataIdx:String(idx)},[
152
+ el('span',{className:'qs-item-kind '+it.kind},it.kind),
153
+ el('div',{className:'qs-item-main'},[
154
+ nameEl,
155
+ el('div',{className:'qs-item-sub'},it.sub||'')
156
+ ]),
157
+ el('span',{className:'qs-item-meta'},it.meta||'')
158
+ ]);
159
+ row.addEventListener('click',function(){qsJump(it)});
160
+ results.appendChild(row);
161
+ });
162
+ });
163
+ qsUpdateActive();
164
+ }
165
+
166
+ function qsUpdateActive(){
167
+ var nodes=document.querySelectorAll('.qs-item');
168
+ nodes.forEach(function(n,i){
169
+ n.classList.toggle('active',i===QS.active);
170
+ });
171
+ var act=nodes[QS.active];
172
+ if(act&&act.scrollIntoView)act.scrollIntoView({block:'nearest'});
173
+ }
174
+
175
+ function qsMove(delta){
176
+ if(!QS.filtered.length)return;
177
+ QS.active=(QS.active+delta+QS.filtered.length)%QS.filtered.length;
178
+ qsUpdateActive();
179
+ }
180
+
181
+ function qsJump(it){
182
+ if(!it)return;
183
+ qsClose();
184
+ // Set project selector if needed
185
+ if(it.project&&S.project!==it.project.id){
186
+ var sel=document.getElementById('projectSelect');
187
+ if(sel){
188
+ sel.value=String(it.project.id);
189
+ S.project=it.project.id;
190
+ if(typeof S.selectedRun!=='undefined')S.selectedRun=null;
191
+ // Trigger refresh chain
192
+ if(typeof refreshSuites==='function')refreshSuites();
193
+ if(typeof refreshRuns==='function')refreshRuns();
194
+ if(typeof refreshScreenshots==='function')refreshScreenshots();
195
+ if(typeof refreshLearnings==='function')refreshLearnings();
196
+ if(typeof refreshVariables==='function')refreshVariables();
197
+ }
198
+ }
199
+ // Route to the correct view + tab
200
+ if(it.kind==='suite'||it.kind==='test'){
201
+ showView('run','testsTabSuites');
202
+ setTimeout(function(){qsScrollToSuite(it)},250);
203
+ }else if(it.kind==='module'){
204
+ showView('run','testsTabModules');
205
+ setTimeout(function(){qsScrollToModule(it)},250);
206
+ }
207
+ }
208
+
209
+ function qsScrollToSuite(it){
210
+ if(!it||!it.suite)return;
211
+ var name=(it.suite.name||'').toLowerCase();
212
+ var cards=document.querySelectorAll('.suite-card');
213
+ for(var i=0;i<cards.length;i++){
214
+ var n=(cards[i].dataset.suiteName||cards[i].textContent||'').toLowerCase();
215
+ if(n.indexOf(name)>=0){
216
+ cards[i].scrollIntoView({behavior:'smooth',block:'center'});
217
+ cards[i].classList.add('qs-flash');
218
+ setTimeout(function(c){return function(){c.classList.remove('qs-flash')}}(cards[i]),1500);
219
+ // If user wanted a test, also click the suite card to open its modal
220
+ if(it.kind==='test'&&cards[i].click)cards[i].click();
221
+ return;
222
+ }
223
+ }
224
+ }
225
+ function qsScrollToModule(it){
226
+ if(!it||!it.module)return;
227
+ var name=(it.module.name||'').toLowerCase();
228
+ var cards=document.querySelectorAll('.module-card');
229
+ for(var i=0;i<cards.length;i++){
230
+ var n=(cards[i].textContent||'').toLowerCase();
231
+ if(n.indexOf(name)===0||(' '+n).indexOf(' '+name)>=0){
232
+ cards[i].scrollIntoView({behavior:'smooth',block:'center'});
233
+ cards[i].classList.add('qs-flash');
234
+ setTimeout(function(c){return function(){c.classList.remove('qs-flash')}}(cards[i]),1500);
235
+ return;
236
+ }
237
+ }
238
+ }
239
+
240
+ /* Wire up triggers + keyboard */
241
+ (function(){
242
+ var inp=document.getElementById('qsInput');
243
+ if(inp){
244
+ inp.addEventListener('input',function(){qsRender(inp.value)});
245
+ inp.addEventListener('keydown',function(e){
246
+ if(e.key==='ArrowDown'){e.preventDefault();qsMove(1)}
247
+ else if(e.key==='ArrowUp'){e.preventDefault();qsMove(-1)}
248
+ else if(e.key==='Enter'){
249
+ e.preventDefault();
250
+ var it=QS.filtered[QS.active];if(it)qsJump(it);
251
+ }
252
+ else if(e.key==='Escape'){qsClose()}
253
+ });
254
+ }
255
+ var trigger=document.getElementById('topbarSearchTrigger');
256
+ if(trigger)trigger.addEventListener('click',qsOpen);
257
+ // Backdrop close
258
+ var modal=qsModalEl();
259
+ if(modal){
260
+ modal.addEventListener('click',function(e){
261
+ if(e.target===modal)qsClose();
262
+ });
263
+ }
264
+ // Global keyboard binding
265
+ document.addEventListener('keydown',function(e){
266
+ var typingHere=document.activeElement&&(document.activeElement.tagName==='INPUT'||document.activeElement.tagName==='TEXTAREA'||document.activeElement.isContentEditable);
267
+ var open=modal&&modal.classList.contains('open');
268
+ if((e.metaKey||e.ctrlKey)&&e.key==='k'){
269
+ e.preventDefault();
270
+ if(open)qsClose();else qsOpen();
271
+ return;
272
+ }
273
+ if(!typingHere&&!open&&e.key==='/'){
274
+ e.preventDefault();qsOpen();
275
+ }
276
+ });
277
+ })();
@@ -1,26 +1,78 @@
1
1
  /* ── Global State ── */
2
2
  var S={
3
- ws:null,project:null,view:'watch',selectedRun:null,
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
- /* ── Navigation ── */
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
- $$('.nav-item').forEach(function(n){n.classList.toggle('active',n.dataset.view===v)});
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
- if(v==='watch'&&typeof startWatchPolling==='function')startWatchPolling();
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('runs');var lb=$('#runsTabLearnings');if(lb)lb.click()}},[
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
  ]);
@@ -23,6 +23,27 @@ function prettyJson(str){
23
23
  try{return JSON.stringify(JSON.parse(str),null,2)}catch(e){return str}
24
24
  }
25
25
 
26
+ /* Lightweight JSON syntax highlighter. Escapes HTML, then wraps tokens in
27
+ colored spans. Input should already be pretty-printed (prettyJson). */
28
+ function highlightJson(text){
29
+ var esc=String(text).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
30
+ return esc.replace(/"(?:\\.|[^"\\])*"|\b(?:true|false|null)\b|-?\b\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?\b/g,function(m,off,s){
31
+ var cls;
32
+ if(m[0]==='"'){cls=/^\s*:/.test(s.slice(off+m.length))?'jn-key':'jn-str'}
33
+ else if(m==='true'||m==='false'){cls='jn-bool'}
34
+ else if(m==='null'){cls='jn-null'}
35
+ else{cls='jn-num'}
36
+ return '<span class="'+cls+'">'+m+'</span>';
37
+ });
38
+ }
39
+
40
+ /* Builds a <pre> with syntax-highlighted JSON content. */
41
+ function jsonPre(text){
42
+ var p=document.createElement('pre');
43
+ p.innerHTML=highlightJson(text);
44
+ return p;
45
+ }
46
+
26
47
  function fmtHeaders(h){
27
48
  if(!h||typeof h!=='object')return '';
28
49
  return Object.keys(h).map(function(k){return k+': '+h[k]}).join('\n');
@@ -109,7 +130,7 @@ function buildNetRow(n){
109
130
  }
110
131
  if(n.requestBody){
111
132
  var rbText=prettyJson(n.requestBody);
112
- sections.push(buildNdSection('Request Body',el('pre',null,rbText),null,rbText));
133
+ sections.push(buildNdSection('Request Body',jsonPre(rbText),null,rbText));
113
134
  }
114
135
  if(n.responseHeaders){
115
136
  var rhCount=Object.keys(n.responseHeaders).length;
@@ -117,7 +138,7 @@ function buildNetRow(n){
117
138
  }
118
139
  if(n.responseBody){
119
140
  var respText=prettyJson(n.responseBody);
120
- sections.push(buildNdSection('Response Body',el('pre',null,respText),null,respText));
141
+ sections.push(buildNdSection('Response Body',jsonPre(respText),null,respText));
121
142
  }
122
143
  detail=el('div',{className:'rd-net-detail'},sections);
123
144
  row.addEventListener('click',function(e){e.stopPropagation();row.classList.toggle('open')});