@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
|
@@ -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')
|
|
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
|
-
|
|
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 (
|
|
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':'
|
|
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==='
|
|
27
|
-
else if(S.view==='
|
|
28
|
-
else if(S.view==='
|
|
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==='
|
|
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==='
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
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:'
|
|
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
|
]);
|
|
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
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',
|
|
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',
|
|
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')});
|