@matware/e2e-runner 1.3.0 → 1.3.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.
@@ -88,7 +88,7 @@ function refreshRuns(){
88
88
  var htr=document.createElement('tr');
89
89
  var cols=[];
90
90
  if(!S.project)cols.push('Project');
91
- cols=cols.concat(['Suite','Source','Date','Total','Pass','Fail','Rate','Time']);
91
+ cols=cols.concat(['Suite','Driver','Source','Date','Total','Pass','Fail','Rate','Time']);
92
92
  cols.forEach(function(c){htr.appendChild(el('th',null,c))});
93
93
  head.textContent='';head.appendChild(htr);
94
94
  var colSpan=cols.length;
@@ -107,6 +107,7 @@ function refreshRuns(){
107
107
  if(r.id===S.selectedRun)tr.classList.add('expanded');
108
108
  if(!S.project)tr.appendChild(el('td',{style:'font-weight:600'},r.project_name||'-'));
109
109
  tr.appendChild(el('td',{style:'color:var(--accent)'},r.suite_name||'all'));
110
+ var driverTd=document.createElement('td');driverTd.appendChild(createDriverBadge(r.pool_driver));tr.appendChild(driverTd);
110
111
  var srcTd=document.createElement('td');srcTd.appendChild(createTriggerBadge(r.triggered_by));tr.appendChild(srcTd);
111
112
  tr.appendChild(el('td',null,fdate(r.generated_at)));
112
113
  tr.appendChild(el('td',null,String(r.total||0)));
@@ -203,8 +204,10 @@ function loadDetailInline(id,detailTr){
203
204
  ])
204
205
  ]);
205
206
  var srcBlock=el('div',null,[el('div',{className:'rd-s-label'},'Source'),el('div',{style:'margin-top:4px'},[createTriggerBadge(d.triggeredBy)])]);
207
+ var drvBlock=el('div',null,[el('div',{className:'rd-s-label'},'Driver'),el('div',{style:'margin-top:4px'},[createDriverBadge(d.poolDriver)])]);
206
208
  var summ=el('div',{className:'rd-summary'},[
207
209
  el('div',null,[el('div',{className:'rd-s-label'},'Suite'),el('div',{className:'rd-s-val',style:'font-size:14px;color:var(--accent)'},d.suiteName||'all')]),
210
+ drvBlock,
208
211
  srcBlock,
209
212
  el('div',null,[el('div',{className:'rd-s-label'},'Total'),el('div',{className:'rd-s-val'},String(d.summary.total))]),
210
213
  el('div',null,[el('div',{className:'rd-s-label'},'Passed'),el('div',{className:'rd-s-val',style:'color:var(--green)'},String(d.summary.passed))]),
@@ -436,10 +439,8 @@ function refreshLearnings(){
436
439
  fetch(url).then(function(r){return r.json()}).then(function(data){
437
440
  if(!data||data.totalRuns===0){
438
441
  $('#learningsEmpty').style.display='block';
439
- $('#learningsOverview').textContent='';$('#learningsTrend').textContent='';
440
- $('#learningsFlaky').textContent='';$('#learningsSelectors').textContent='';
441
- $('#learningsPages').textContent='';$('#learningsApis').textContent='';
442
- $('#learningsErrors').textContent='';
442
+ $('#learnHero').textContent='';$('#learnCards').textContent='';
443
+ $('#learnTrend').textContent='';$('#learnBottom').textContent='';
443
444
  $('#badgeLearnings').textContent='-';
444
445
  return;
445
446
  }
@@ -464,48 +465,139 @@ function refreshLearnings(){
464
465
  $('#badgeLearnings').textContent='\u2714';
465
466
  $('#badgeLearnings').style.background='var(--green-dim)';$('#badgeLearnings').style.color='var(--green)';
466
467
  }
467
- renderLearnOverview(data);
468
+ renderLearnHero(data);
469
+ renderLearnCards(data);
468
470
  renderLearnTrend(data.recentTrend||[]);
469
- renderLearnFlaky(data.flakyTests||[]);
470
- renderLearnSelectors(data.unstableSelectors||[]);
471
- renderLearnPages(data.failingPages||[]);
472
- renderLearnApis(data.apiIssues||[]);
473
- renderLearnErrors(data.topErrors||[]);
471
+ renderLearnBottomRow(data);
474
472
  }).catch(function(){$('#learningsEmpty').style.display='block'});
475
473
  }
476
474
 
477
- function renderLearnOverview(d){
478
- var container=$('#learningsOverview');container.textContent='';
479
- var grid=document.createElement('div');grid.className='learn-grid';
480
- [{val:d.totalRuns,lbl:'Runs',cls:'accent'},{val:d.totalTests,lbl:'Tests',cls:'accent'},
481
- {val:d.overallPassRate+'%',lbl:'Pass Rate',cls:d.overallPassRate>=90?'green':d.overallPassRate>=70?'':'red'},
482
- {val:d.avgDurationMs<1000?d.avgDurationMs+'ms':(d.avgDurationMs/1000).toFixed(1)+'s',lbl:'Avg Duration',cls:'purple'},
483
- {val:(d.flakyTests?d.flakyTests.length:0),lbl:'Flaky Tests',cls:d.flakyTests&&d.flakyTests.length>0?'red':'green'},
484
- {val:(d.unstableSelectors?d.unstableSelectors.length:0),lbl:'Unstable Selectors',cls:d.unstableSelectors&&d.unstableSelectors.length>0?'red':'green'}
485
- ].forEach(function(item){
486
- var stat=document.createElement('div');stat.className='learn-stat';
487
- var valEl=document.createElement('div');valEl.className='learn-stat-val '+item.cls;valEl.textContent=item.val;
488
- var lblEl=document.createElement('div');lblEl.className='learn-stat-lbl';lblEl.textContent=item.lbl;
489
- stat.appendChild(valEl);stat.appendChild(lblEl);grid.appendChild(stat);
475
+ function rateColor(v){return v>=90?'var(--green)':v>=70?'var(--amber)':'var(--red)'}
476
+ function rateClass(v){return v>=90?'good':v>=70?'warn':'bad'}
477
+ function durFmt(ms){return ms<1000?Math.round(ms)+'ms':(ms/1000).toFixed(1)+'s'}
478
+
479
+ function renderLearnHero(d){
480
+ var c=$('#learnHero');c.textContent='';
481
+ var wrap=document.createElement('div');wrap.className='learn-hero';
482
+ var passRate=d.overallPassRate||0;
483
+ var ns='http://www.w3.org/2000/svg';
484
+ var ringWrap=document.createElement('div');ringWrap.className='learn-hero-ring';
485
+ var svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 36 36');
486
+ var bgCircle=document.createElementNS(ns,'circle');bgCircle.setAttribute('cx','18');bgCircle.setAttribute('cy','18');bgCircle.setAttribute('r','15.9');bgCircle.className.baseVal='learn-hero-ring-bg';svg.appendChild(bgCircle);
487
+ var fgCircle=document.createElementNS(ns,'circle');fgCircle.setAttribute('cx','18');fgCircle.setAttribute('cy','18');fgCircle.setAttribute('r','15.9');fgCircle.className.baseVal='learn-hero-ring-fg';
488
+ var circ=2*Math.PI*15.9;fgCircle.setAttribute('stroke-dasharray',circ.toFixed(1));fgCircle.setAttribute('stroke-dashoffset',(circ*(1-passRate/100)).toFixed(1));fgCircle.setAttribute('stroke',rateColor(passRate));
489
+ svg.appendChild(fgCircle);ringWrap.appendChild(svg);
490
+ var pctEl=document.createElement('div');pctEl.className='learn-hero-pct';pctEl.style.color=rateColor(passRate);pctEl.textContent=passRate+'%';
491
+ ringWrap.appendChild(pctEl);wrap.appendChild(ringWrap);
492
+
493
+ var stats=document.createElement('div');stats.className='learn-hero-stats';
494
+ var badSels=d.unstableSelectors?d.unstableSelectors.length:0;
495
+ var slowTests=d.failingPages?d.failingPages.length:0;
496
+ var apiIssues=d.apiIssues?d.apiIssues.length:0;
497
+ var topErr=d.topErrors&&d.topErrors.length>0?d.topErrors[0].occurrence_count:0;
498
+ var flakyCount=d.flakyTests?d.flakyTests.length:0;
499
+ var items=[
500
+ {val:String(d.totalRuns),lbl:'Runs',color:'var(--accent)'},
501
+ {val:String(d.totalTests),lbl:'Tests',color:'var(--accent)'},
502
+ {val:durFmt(d.avgDurationMs||0),lbl:'Avg Duration',color:'var(--purple)'},
503
+ {val:String(flakyCount),lbl:'Flaky',color:flakyCount>0?'var(--amber)':'var(--green)'},
504
+ {val:String(badSels),lbl:'Bad Selectors',color:badSels>0?'var(--red)':'var(--green)'},
505
+ {val:String(slowTests),lbl:'Slow Pages',color:slowTests>0?'var(--amber)':'var(--green)'},
506
+ {val:String(apiIssues),lbl:'API Issues',color:apiIssues>0?'var(--red)':'var(--green)'},
507
+ {val:String(topErr),lbl:'Top Error Hits',color:topErr>0?'var(--red)':'var(--green)'}
508
+ ];
509
+ items.forEach(function(it){
510
+ var statEl=document.createElement('div');statEl.className='learn-hero-stat';
511
+ var valEl=document.createElement('div');valEl.className='learn-hero-stat-val';valEl.style.color=it.color;valEl.textContent=it.val;
512
+ var lblEl=document.createElement('div');lblEl.className='learn-hero-stat-lbl';lblEl.textContent=it.lbl;
513
+ statEl.appendChild(valEl);statEl.appendChild(lblEl);stats.appendChild(statEl);
490
514
  });
491
- container.appendChild(grid);
515
+ wrap.appendChild(stats);c.appendChild(wrap);
516
+ }
517
+
518
+ function makeLearnItem(label,sub,pct,valText,color){
519
+ var item=document.createElement('div');item.className='learn-item';
520
+ var barWrap=document.createElement('div');barWrap.className='learn-item-bar';
521
+ var lblEl=document.createElement('div');lblEl.className='learn-item-label';
522
+ var codeEl=document.createElement('code');codeEl.textContent=label;lblEl.appendChild(codeEl);
523
+ barWrap.appendChild(lblEl);
524
+ if(sub){var subEl=document.createElement('div');subEl.className='learn-item-sub';subEl.textContent=sub;barWrap.appendChild(subEl)}
525
+ var bar=document.createElement('div');bar.className='learn-bar';
526
+ var fill=document.createElement('div');fill.className='learn-bar-fill';fill.style.width=Math.min(pct,100)+'%';fill.style.background=color;
527
+ bar.appendChild(fill);barWrap.appendChild(bar);
528
+ item.appendChild(barWrap);
529
+ var valEl=document.createElement('div');valEl.className='learn-item-val';valEl.style.color=color;valEl.textContent=valText;
530
+ item.appendChild(valEl);
531
+ return item;
532
+ }
533
+
534
+ function makeLearnCard(icon,title,emptyMsg){
535
+ var card=document.createElement('div');card.className='learn-card';
536
+ var titleEl=document.createElement('div');titleEl.className='learn-card-title';
537
+ var iconEl=document.createElement('span');iconEl.className='lc-icon';iconEl.textContent=icon;
538
+ titleEl.appendChild(iconEl);titleEl.appendChild(document.createTextNode(title));
539
+ card.appendChild(titleEl);
540
+ card._empty=emptyMsg;
541
+ return card;
542
+ }
543
+
544
+ function renderLearnCards(d){
545
+ var c=$('#learnCards');c.textContent='';
546
+
547
+ var selCard=makeLearnCard('\u26A0','Risky Selectors','No unstable selectors');
548
+ var sels=d.unstableSelectors||[];
549
+ if(!sels.length){var e1=document.createElement('div');e1.className='learn-card-empty';e1.textContent=selCard._empty;selCard.appendChild(e1)}
550
+ else{sels.slice(0,5).forEach(function(s){
551
+ var sel=s.selector.length>40?s.selector.slice(0,37)+'...':s.selector;
552
+ selCard.appendChild(makeLearnItem(sel,s.action_type+' \u00B7 '+s.total_uses+' uses',parseFloat(s.fail_rate),s.fail_rate+'%',parseFloat(s.fail_rate)>30?'var(--red)':'var(--amber)'));
553
+ })}
554
+ c.appendChild(selCard);
555
+
556
+ var pageCard=makeLearnCard('\u23F1','Problem Pages','No failing pages');
557
+ var pages=d.failingPages||[];
558
+ if(!pages.length){var e2=document.createElement('div');e2.className='learn-card-empty';e2.textContent=pageCard._empty;pageCard.appendChild(e2)}
559
+ else{pages.slice(0,5).forEach(function(p){
560
+ pageCard.appendChild(makeLearnItem(p.url_path,p.total_visits+' visits \u00B7 '+p.console_errors+' console errs',parseFloat(p.fail_rate),p.fail_rate+'%',parseFloat(p.fail_rate)>30?'var(--red)':'var(--amber)'));
561
+ })}
562
+ c.appendChild(pageCard);
563
+
564
+ var flakyCard=makeLearnCard('\u223C','Flaky Tests','No flaky tests detected');
565
+ var flaky=d.flakyTests||[];
566
+ if(!flaky.length){var e3=document.createElement('div');e3.className='learn-card-empty';e3.textContent=flakyCard._empty;flakyCard.appendChild(e3)}
567
+ else{flaky.slice(0,5).forEach(function(f){
568
+ flakyCard.appendChild(makeLearnItem(f.test_name,'Attempt avg '+f.avg_attempts+' \u00B7 '+f.total_runs+' runs',parseFloat(f.flaky_rate),f.flaky_rate+'%',parseFloat(f.flaky_rate)>30?'var(--red)':'var(--amber)'));
569
+ })}
570
+ c.appendChild(flakyCard);
571
+
572
+ var apiCard=makeLearnCard('\u21C4','API Issues','No API issues');
573
+ var apis=d.apiIssues||[];
574
+ if(!apis.length){var e4=document.createElement('div');e4.className='learn-card-empty';e4.textContent=apiCard._empty;apiCard.appendChild(e4)}
575
+ else{apis.slice(0,5).forEach(function(a){
576
+ var ep=a.endpoint.length>40?a.endpoint.slice(0,37)+'...':a.endpoint;
577
+ apiCard.appendChild(makeLearnItem(ep,a.total_calls+' calls \u00B7 '+durFmt(a.avg_duration_ms),parseFloat(a.error_rate),a.error_rate+'%',parseFloat(a.error_rate)>20?'var(--red)':'var(--amber)'));
578
+ })}
579
+ c.appendChild(apiCard);
492
580
  }
493
581
 
494
582
  function renderLearnTrend(trend){
495
- var container=$('#learningsTrend');container.textContent='';
583
+ var container=$('#learnTrend');container.textContent='';
496
584
  if(!trend.length)return;
497
- var card=document.createElement('div');card.className='card';
498
- var label=document.createElement('div');label.className='card-label';label.textContent='Pass Rate Trend (7 days)';card.appendChild(label);
499
- var chartDiv=document.createElement('div');chartDiv.className='learn-trend-chart';
585
+ var card=document.createElement('div');card.className='learn-card';
586
+ var titleEl=document.createElement('div');titleEl.className='learn-card-title';
587
+ var iconEl=document.createElement('span');iconEl.className='lc-icon';iconEl.textContent='\u2197';
588
+ titleEl.appendChild(iconEl);titleEl.appendChild(document.createTextNode('Pass Rate Trend'));
589
+ card.appendChild(titleEl);
590
+ var chartDiv=document.createElement('div');chartDiv.style.cssText='height:80px;width:100%';
500
591
  var w=100/trend.length;var ns='http://www.w3.org/2000/svg';
501
- var svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 100 100');svg.setAttribute('preserveAspectRatio','none');
592
+ var svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 100 100');svg.setAttribute('preserveAspectRatio','none');svg.style.cssText='width:100%;height:100%';
502
593
  var bg=document.createElementNS(ns,'rect');bg.setAttribute('x','0');bg.setAttribute('y','0');bg.setAttribute('width','100');bg.setAttribute('height','100');bg.setAttribute('fill','var(--surface2)');bg.setAttribute('rx','2');svg.appendChild(bg);
503
594
  var gridLine=document.createElementNS(ns,'line');gridLine.setAttribute('x1','0');gridLine.setAttribute('y1','50');gridLine.setAttribute('x2','100');gridLine.setAttribute('y2','50');gridLine.setAttribute('stroke','var(--border)');gridLine.setAttribute('stroke-width','0.3');gridLine.setAttribute('stroke-dasharray','2,2');svg.appendChild(gridLine);
504
595
  var pts=trend.map(function(t,i){return(i*w+w/2)+','+(100-t.pass_rate)}).join(' ');
505
596
  var poly=document.createElementNS(ns,'polygon');poly.setAttribute('points',(0*w+w/2)+',100 '+pts+' '+((trend.length-1)*w+w/2)+',100');poly.setAttribute('fill','var(--accent-dim)');svg.appendChild(poly);
506
597
  var pl=document.createElementNS(ns,'polyline');pl.setAttribute('points',pts);pl.setAttribute('fill','none');pl.setAttribute('stroke','var(--accent)');pl.setAttribute('stroke-width','1.5');svg.appendChild(pl);
507
598
  trend.forEach(function(t,i){
508
- var circle=document.createElementNS(ns,'circle');circle.setAttribute('cx',''+(i*w+w/2));circle.setAttribute('cy',''+(100-t.pass_rate));circle.setAttribute('r','2');circle.setAttribute('fill','var(--accent)');
599
+ var color=rateColor(t.pass_rate);
600
+ var circle=document.createElementNS(ns,'circle');circle.setAttribute('cx',''+(i*w+w/2));circle.setAttribute('cy',''+(100-t.pass_rate));circle.setAttribute('r','2.5');circle.setAttribute('fill',color);
509
601
  var title=document.createElementNS(ns,'title');title.textContent=t.date+': '+t.pass_rate+'% ('+t.total_tests+' tests)';circle.appendChild(title);svg.appendChild(circle);
510
602
  });
511
603
  chartDiv.appendChild(svg);card.appendChild(chartDiv);
@@ -514,35 +606,47 @@ function renderLearnTrend(trend){
514
606
  card.appendChild(dates);container.appendChild(card);
515
607
  }
516
608
 
517
- function buildLearnTable(title,headers,rows){
518
- var card=document.createElement('div');card.className='card learn-section';
519
- var h=document.createElement('div');h.className='learn-section-title';h.textContent=title;card.appendChild(h);
520
- var wrap=document.createElement('div');wrap.className='tbl-wrap';
521
- var tbl=document.createElement('table');tbl.className='learn-table';
522
- var thead=document.createElement('thead');var hr=document.createElement('tr');
523
- headers.forEach(function(hdr){var th=document.createElement('th');th.textContent=hdr;hr.appendChild(th)});
524
- thead.appendChild(hr);tbl.appendChild(thead);
525
- var tbody=document.createElement('tbody');
526
- rows.forEach(function(cells){
527
- var tr=document.createElement('tr');
528
- cells.forEach(function(cell){
529
- var td=document.createElement('td');
530
- if(cell.code){var code=document.createElement('code');code.textContent=cell.code;td.appendChild(code)}
531
- else if(cell.badge){var span=document.createElement('span');span.className='badge '+cell.cls;span.textContent=cell.badge;td.appendChild(span)}
532
- else{td.textContent=cell.text!==undefined&&cell.text!==null?cell.text:(typeof cell==='object'?'-':cell)}
533
- tr.appendChild(td);
609
+ function renderLearnBottomRow(d){
610
+ var c=$('#learnBottom');c.textContent='';
611
+
612
+ var errCard=makeLearnCard('\u2718','Most Common Errors','No errors recorded');
613
+ var errors=d.topErrors||[];
614
+ if(!errors.length){var e1=document.createElement('div');e1.className='learn-card-empty';e1.textContent=errCard._empty;errCard.appendChild(e1)}
615
+ else{errors.slice(0,5).forEach(function(e){
616
+ var pat=e.pattern.length>45?e.pattern.slice(0,42)+'...':e.pattern;
617
+ var maxCount=errors[0].occurrence_count||1;
618
+ var pct=(e.occurrence_count/maxCount)*100;
619
+ var verdictEl=document.createElement('div');verdictEl.className='learn-verdict '+rateClass(100-(pct));verdictEl.textContent=e.category.replace(/-/g,' ');
620
+ var item=makeLearnItem(pat,(e.last_seen||'').split('T')[0]+' \u00B7 '+e.occurrence_count+'x',pct,e.occurrence_count+'x','var(--red)');
621
+ item.insertBefore(verdictEl,item.lastChild);
622
+ errCard.appendChild(item);
623
+ })}
624
+ c.appendChild(errCard);
625
+
626
+ var slowCard=makeLearnCard('\u23F3','Slowest Tests','No slow test data');
627
+ var trend=d.recentTrend||[];
628
+ var slowTests=[];
629
+ if(d.flakyTests){
630
+ d.flakyTests.forEach(function(f){
631
+ if(f.avg_duration_ms&&f.avg_duration_ms>2000){slowTests.push({name:f.test_name,dur:f.avg_duration_ms})}
534
632
  });
535
- tbody.appendChild(tr);
536
- });
537
- tbl.appendChild(tbody);wrap.appendChild(tbl);card.appendChild(wrap);return card;
633
+ }
634
+ if(d.failingPages){
635
+ d.failingPages.forEach(function(p){
636
+ if(p.avg_load_time_ms&&p.avg_load_time_ms>3000){slowTests.push({name:p.url_path,dur:p.avg_load_time_ms})}
637
+ });
638
+ }
639
+ slowTests.sort(function(a,b){return b.dur-a.dur});
640
+ if(!slowTests.length){var e2=document.createElement('div');e2.className='learn-card-empty';e2.textContent=slowCard._empty;slowCard.appendChild(e2)}
641
+ else{
642
+ var maxDur=slowTests[0].dur;
643
+ slowTests.slice(0,5).forEach(function(t){
644
+ slowCard.appendChild(makeLearnItem(t.name,'','',durFmt(t.dur),(t.dur/maxDur)*100,t.dur>5000?'var(--red)':'var(--amber)'));
645
+ });
646
+ }
647
+ c.appendChild(slowCard);
538
648
  }
539
649
 
540
- function renderLearnFlaky(flaky){var c=$('#learningsFlaky');c.textContent='';if(!flaky.length)return;c.appendChild(buildLearnTable('Flaky Tests',['Test','Flaky Rate','Occurrences','Total Runs','Last Flaky','Avg Attempts'],flaky.map(function(f){return[{code:f.test_name},{badge:f.flaky_rate+'%',cls:f.flaky_rate>30?'fail':'flaky'},{text:f.flaky_count},{text:f.total_runs},{text:(f.last_flaky||'-').split('T')[0]},{text:f.avg_attempts}]})))}
541
- function renderLearnSelectors(sels){var c=$('#learningsSelectors');c.textContent='';if(!sels.length)return;c.appendChild(buildLearnTable('Unstable Selectors',['Selector','Action','Fail Rate','Uses','Tests','Page'],sels.map(function(s){var sel=s.selector.length>45?s.selector.slice(0,42)+'...':s.selector;return[{code:sel},{text:s.action_type},{badge:s.fail_rate+'%',cls:s.fail_rate>30?'fail':'flaky'},{text:s.total_uses},{text:s.used_by_tests},{text:s.page_url||'-'}]})))}
542
- function renderLearnPages(pages){var c=$('#learningsPages');c.textContent='';if(!pages.length)return;c.appendChild(buildLearnTable('Failing Pages',['Page','Fail Rate','Visits','Console Errors','Network Errors'],pages.map(function(p){return[{code:p.url_path},{badge:p.fail_rate+'%',cls:p.fail_rate>30?'fail':'flaky'},{text:p.total_visits},{text:p.console_errors},{text:p.network_errors}]})))}
543
- function renderLearnApis(apis){var c=$('#learningsApis');c.textContent='';if(!apis.length)return;c.appendChild(buildLearnTable('API Issues',['Endpoint','Error Rate','Calls','Avg Duration','Status Codes'],apis.map(function(a){var ep=a.endpoint.length>45?a.endpoint.slice(0,42)+'...':a.endpoint;var d=a.avg_duration_ms<1000?Math.round(a.avg_duration_ms)+'ms':(a.avg_duration_ms/1000).toFixed(1)+'s';return[{code:ep},{badge:a.error_rate+'%',cls:a.error_rate>20?'fail':'flaky'},{text:a.total_calls},{text:d},{text:a.status_codes||'-'}]})))}
544
- function renderLearnErrors(errors){var c=$('#learningsErrors');c.textContent='';if(!errors.length)return;c.appendChild(buildLearnTable('Error Patterns',['Pattern','Category','Count','First Seen','Last Seen','Example Test'],errors.map(function(e){var pat=e.pattern.length>50?e.pattern.slice(0,47)+'...':e.pattern;return[{text:pat},{badge:e.category,cls:'run'},{text:e.occurrence_count},{text:(e.first_seen||'-').split('T')[0]},{text:(e.last_seen||'-').split('T')[0]},{code:e.example_test||'-'}]})))}
545
-
546
650
  $('#btnRefreshLearnings').addEventListener('click',refreshLearnings);
547
651
  $('#learningsDays').addEventListener('change',refreshLearnings);
548
652
 
@@ -104,6 +104,12 @@ function handleWS(m){
104
104
  var r7=getLiveRun(m);if(r7){r7.on=false;r7.done=true;r7.tests.__error={status:'failed',error:m.error}}
105
105
  showToast('Run error: '+m.error,'error');
106
106
  renderLive();break;
107
+ case 'test:frame':
108
+ if(S.screencastTest===m.name&&m.data){
109
+ var img=$('#screencastImg');
110
+ if(img)img.src='data:image/jpeg;base64,'+m.data;
111
+ }
112
+ break;
107
113
  case 'db:updated':
108
114
  refreshRuns();refreshProjects();refreshScreenshots();refreshLearnings();refreshWatch();break;
109
115
  }
@@ -77,6 +77,13 @@ tbody tr.selected td{background:var(--accent-dim)}
77
77
  .trigger-badge.src-unknown{background:rgba(70,75,98,.15);color:var(--text3)}
78
78
  .trigger-badge .trig-icon{font-size:11px;line-height:1}
79
79
 
80
+ /* ── Driver Badges ── */
81
+ .driver-badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:600;font-family:var(--mono);white-space:nowrap}
82
+ .driver-badge.drv-browserless{background:var(--accent-dim)}
83
+ .driver-badge.drv-cdp{background:var(--purple-dim)}
84
+ .driver-badge.drv-steel{background:var(--amber-dim)}
85
+ .driver-badge .drv-icon{font-size:11px;line-height:1}
86
+
80
87
  /* ── Filter Bar ── */
81
88
  .filter-bar{display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap}
82
89
  .filter-btn{padding:5px 12px;border-radius:var(--r);border:1px solid var(--border);background:var(--surface2);color:var(--text2);font-family:var(--mono);font-size:11px;cursor:pointer;transition:all .15s}
@@ -11,7 +11,7 @@
11
11
  .live-stats span strong{color:var(--text)}
12
12
  .live-progress{height:3px;background:var(--surface3)}
13
13
  .live-progress-fill{height:100%;background:var(--purple);transition:width .4s;border-radius:0 2px 2px 0}
14
- .live-tests{padding:12px 16px;display:flex;flex-direction:column;gap:2px;flex:1;overflow-y:auto;min-height:0}
14
+ .live-tests{padding:12px 16px;display:flex;flex-direction:column;gap:2px;overflow-y:auto;min-height:0;flex:1}
15
15
  .live-test{padding:10px 12px;border-radius:var(--r);border-left:3px solid var(--text3);background:var(--surface2);font-size:11px;transition:border-color .2s,padding .25s,max-height .35s cubic-bezier(.4,0,.2,1)}
16
16
  .live-test.running{border-left-color:var(--purple)}
17
17
  .live-test.passed{border-left-color:var(--green)}
@@ -68,6 +68,29 @@
68
68
  .lr-dismiss{padding:2px 6px;font-size:9px;font-family:var(--mono);background:transparent;border:1px solid transparent;border-radius:4px;color:var(--text3);cursor:pointer;transition:all .15s;margin-left:auto}
69
69
  .lr-dismiss:hover{color:var(--red);border-color:rgba(239,68,68,.3);background:var(--red-dim)}
70
70
 
71
+ /* ── Screencast Panel ── */
72
+ .live-body{display:flex;flex:1;min-height:0;overflow:hidden}
73
+ .live-body .live-tests{flex:1;min-width:0}
74
+ .screencast-panel{width:420px;flex-shrink:0;display:flex;flex-direction:column;border-left:1px solid var(--border);background:var(--surface2)}
75
+ .screencast-header{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--border);background:var(--surface3)}
76
+ .screencast-label{font-size:11px;font-weight:600;color:var(--purple);white-space:nowrap}
77
+ .screencast-select{flex:1;padding:4px 8px;font-size:10px;font-family:var(--mono);background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);outline:none;cursor:pointer}
78
+ .screencast-select:focus{border-color:var(--purple)}
79
+ .screencast-viewport{flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;background:#000;position:relative}
80
+ .screencast-viewport img{max-width:100%;max-height:100%;object-fit:contain;display:none}
81
+ .screencast-placeholder{display:flex;align-items:center;justify-content:center;width:100%;height:100%;color:var(--text3);font-size:12px;font-family:var(--mono)}
82
+
83
+ /* ── Screencast focus badge on test cards ── */
84
+ .sc-focus-badge{cursor:pointer;font-size:10px;padding:1px 4px;border-radius:3px;opacity:.4;transition:all .15s}
85
+ .sc-focus-badge:hover{opacity:.8}
86
+ .sc-focus-badge.active{opacity:1;background:var(--purple-dim);border-radius:3px}
87
+
88
+ /* ── Screencast toggle in Tests view ── */
89
+ .screencast-toggle-label{display:flex;align-items:center;gap:4px;cursor:pointer;font-size:14px;padding:4px 8px;border-radius:4px;border:1px solid var(--border);background:var(--surface2);transition:all .15s;user-select:none}
90
+ .screencast-toggle-label:hover{border-color:var(--purple);background:var(--surface3)}
91
+ .screencast-toggle-label input{display:none}
92
+ .screencast-toggle-label:has(input:checked){border-color:var(--purple);background:var(--purple-dim);color:var(--purple)}
93
+
71
94
  .live-nav-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--purple);animation:pulse 1.5s infinite}
72
95
  .spinner{display:inline-block;width:12px;height:12px;border:2px solid var(--border);border-top-color:var(--purple);border-radius:50%;animation:spin .6s linear infinite;vertical-align:middle}
73
96
  .spinner-small{display:inline-block;width:8px;height:8px;border:1.5px solid var(--border);border-top-color:var(--purple);border-radius:50%;animation:spin .6s linear infinite;vertical-align:middle}
@@ -200,6 +200,42 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
200
200
  .learn-trend-chart{width:100%;height:100px;margin-bottom:20px}
201
201
  .learn-trend-chart svg{width:100%;height:100%}
202
202
 
203
+ /* ── Learnings Dashboard (visual cards) ── */
204
+ .learn-hero{display:flex;align-items:center;gap:24px;margin-bottom:20px;padding:20px 24px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r)}
205
+ .learn-hero-ring{position:relative;width:100px;height:100px;flex-shrink:0}
206
+ .learn-hero-ring svg{width:100%;height:100%;transform:rotate(-90deg)}
207
+ .learn-hero-ring-bg{fill:none;stroke:var(--surface3);stroke-width:8}
208
+ .learn-hero-ring-fg{fill:none;stroke-width:8;stroke-linecap:round;transition:stroke-dashoffset .6s ease}
209
+ .learn-hero-pct{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:700;font-family:var(--mono)}
210
+ .learn-hero-stats{flex:1;display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
211
+ .learn-hero-stat{text-align:center}
212
+ .learn-hero-stat-val{font-size:18px;font-weight:700;font-family:var(--mono)}
213
+ .learn-hero-stat-lbl{font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.08em;margin-top:2px}
214
+
215
+ .learn-cols{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px}
216
+ @media(max-width:900px){.learn-cols{grid-template-columns:1fr}}
217
+
218
+ .learn-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:14px 16px}
219
+ .learn-card-title{font-size:11px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px;display:flex;align-items:center;gap:6px}
220
+ .learn-card-title .lc-icon{font-size:13px}
221
+ .learn-card-empty{font-size:11px;color:var(--text3);font-style:italic}
222
+
223
+ .learn-item{display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--border)}
224
+ .learn-item:last-child{border-bottom:none}
225
+ .learn-item-bar{flex:1;min-width:0}
226
+ .learn-item-label{font-size:11px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:3px}
227
+ .learn-item-label code{background:var(--surface3);padding:1px 4px;border-radius:3px;font-size:10px}
228
+ .learn-item-sub{font-size:9px;color:var(--text3)}
229
+ .learn-item-val{font-size:13px;font-weight:700;font-family:var(--mono);flex-shrink:0;min-width:44px;text-align:right}
230
+
231
+ .learn-bar{height:4px;border-radius:2px;background:var(--surface3);overflow:hidden;margin-top:3px}
232
+ .learn-bar-fill{height:100%;border-radius:2px;transition:width .4s ease}
233
+
234
+ .learn-verdict{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:10px;font-size:10px;font-weight:600}
235
+ .learn-verdict.good{background:var(--green-dim);color:var(--green)}
236
+ .learn-verdict.warn{background:var(--amber-dim);color:var(--amber)}
237
+ .learn-verdict.bad{background:var(--red-dim);color:var(--red)}
238
+
203
239
  /* ── Pool Distribution ── */
204
240
  .pool-dist{display:flex;align-items:stretch;gap:0;border-radius:6px;overflow:hidden;height:22px;margin:8px 0;font-size:10px;font-weight:600;font-family:var(--mono)}
205
241
  .pool-dist-seg{display:flex;align-items:center;justify-content:center;gap:4px;padding:0 8px;color:#fff;white-space:nowrap;min-width:40px;transition:flex .3s}
@@ -79,7 +79,11 @@
79
79
  <div class="view" id="view-tests">
80
80
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
81
81
  <div style="font-family:var(--sans);font-size:16px;font-weight:600">Tests</div>
82
- <div style="display:flex;gap:8px">
82
+ <div style="display:flex;gap:8px;align-items:center">
83
+ <label class="screencast-toggle-label" title="Enable live browser screencast during test runs">
84
+ <input type="checkbox" id="screencastToggle" />
85
+ <span>&#128249;</span>
86
+ </label>
83
87
  <button class="btn sm primary" id="btnRunAll">&#9655; Run All</button>
84
88
  </div>
85
89
  </div>
@@ -181,13 +185,12 @@
181
185
  <button class="btn sm" id="btnRefreshLearnings">Refresh</button>
182
186
  </div>
183
187
  </div>
184
- <div id="learningsOverview"></div>
185
- <div id="learningsTrend"></div>
186
- <div id="learningsFlaky"></div>
187
- <div id="learningsSelectors"></div>
188
- <div id="learningsPages"></div>
189
- <div id="learningsApis"></div>
190
- <div id="learningsErrors"></div>
188
+ <div id="learnDash">
189
+ <div id="learnHero"></div>
190
+ <div id="learnCards" class="learn-cols"></div>
191
+ <div id="learnTrend"></div>
192
+ <div id="learnBottom" class="learn-cols"></div>
193
+ </div>
191
194
  <div class="empty" id="learningsEmpty" style="display:none">
192
195
  <div class="empty-icon">&#9733;</div>
193
196
  <p>No learnings data yet. Run some tests to start building knowledge.</p>
@@ -214,7 +217,19 @@
214
217
  <button class="live-clear-btn" id="liveClearBtn">Clear All</button>
215
218
  </div>
216
219
  <div class="live-progress"><div class="live-progress-fill" id="liveProgressFill" style="width:0"></div></div>
217
- <div class="live-tests" id="liveTests"></div>
220
+ <div class="live-body">
221
+ <div class="live-tests" id="liveTests"></div>
222
+ <div class="screencast-panel" id="screencastPanel" style="display:none">
223
+ <div class="screencast-header">
224
+ <span class="screencast-label">&#128249; Screencast</span>
225
+ <select id="screencastSelect" class="screencast-select"><option value="">Select test...</option></select>
226
+ </div>
227
+ <div class="screencast-viewport">
228
+ <img id="screencastImg" alt="Browser screencast" />
229
+ <div class="screencast-placeholder" id="screencastPlaceholder">Select a running test to watch</div>
230
+ </div>
231
+ </div>
232
+ </div>
218
233
  </div>
219
234
  <div class="empty" id="liveEmpty">
220
235
  <div class="empty-icon" style="font-size:48px;opacity:.3">&#9679;</div>