@matware/e2e-runner 1.1.0 → 1.1.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.
@@ -53,12 +53,13 @@ a{color:var(--accent);text-decoration:none}
53
53
  .ws-dot{width:6px;height:6px;border-radius:50%;display:inline-block;margin-right:4px}
54
54
 
55
55
  /* ── Main ── */
56
- .main{margin-left:232px;flex:1;min-height:100vh}
56
+ .main{margin-left:232px;flex:1;min-height:100vh;display:flex;flex-direction:column}
57
57
  .main-header{padding:16px 24px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;background:var(--surface);position:sticky;top:0;z-index:40}
58
58
  .main-header .title{font-family:var(--sans);font-size:16px;font-weight:600}
59
59
  .main-header .actions{margin-left:auto;display:flex;gap:8px}
60
60
  .view{display:none;padding:24px}
61
61
  .view.active{display:block}
62
+ #view-live.active{display:flex;flex-direction:column;flex:1;min-height:calc(100vh - 0px);padding:16px}
62
63
 
63
64
  /* ── Buttons ── */
64
65
  .btn{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:var(--r);font-family:var(--mono);font-size:11px;font-weight:500;cursor:pointer;border:1px solid var(--border);background:var(--surface2);color:var(--text);transition:all .15s;white-space:nowrap}
@@ -116,8 +117,8 @@ tbody tr.selected td{background:var(--accent-dim)}
116
117
  .suite-card-tests li::before{content:">";position:absolute;left:0;color:var(--text3)}
117
118
 
118
119
  /* ── Live Execution ── */
119
- .live-panel{display:none;background:var(--surface);border:1px solid var(--purple);border-radius:var(--r);overflow:hidden;animation:fadeSlide .3s ease}
120
- .live-panel.active{display:block}
120
+ .live-panel{display:none;background:var(--surface);border:1px solid var(--purple);border-radius:var(--r);overflow:hidden;animation:fadeSlide .3s ease;flex-direction:column}
121
+ .live-panel.active{display:flex;flex:1;min-height:0}
121
122
  @keyframes fadeSlide{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
122
123
  .live-header{padding:14px 16px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--border);background:var(--purple-dim)}
123
124
  .live-header .label{font-weight:600;color:var(--purple);font-size:12px;display:flex;align-items:center;gap:8px}
@@ -128,7 +129,7 @@ tbody tr.selected td{background:var(--accent-dim)}
128
129
  .live-stats span strong{color:var(--text)}
129
130
  .live-progress{height:3px;background:var(--surface3)}
130
131
  .live-progress-fill{height:100%;background:var(--purple);transition:width .4s;border-radius:0 2px 2px 0}
131
- .live-tests{padding:12px 16px;display:flex;flex-direction:column;gap:2px;max-height:calc(100vh - 200px);overflow-y:auto}
132
+ .live-tests{padding:12px 16px;display:flex;flex-direction:column;gap:2px;flex:1;overflow-y:auto;min-height:0}
132
133
  .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)}
133
134
  .live-test.running{border-left-color:var(--purple)}
134
135
  .live-test.passed{border-left-color:var(--green)}
@@ -175,6 +176,10 @@ tbody tr.selected td{background:var(--accent-dim)}
175
176
  .live-done.has-failures{background:var(--red-dim);color:var(--red)}
176
177
  .live-close{padding:4px 10px;font-size:10px;background:transparent;border:1px solid var(--border);border-radius:var(--r);color:var(--text2);cursor:pointer}
177
178
  .live-close:hover{color:var(--text);border-color:var(--border-hi)}
179
+ .live-clear-btn{padding:5px 12px;font-size:10px;font-family:var(--mono);font-weight:500;background:var(--surface2);border:1px solid var(--border);border-radius:var(--r);color:var(--text2);cursor:pointer;display:none;transition:all .15s}
180
+ .live-clear-btn:hover{color:var(--text);border-color:var(--border-hi);background:var(--surface3)}
181
+ .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}
182
+ .lr-dismiss:hover{color:var(--red);border-color:rgba(239,68,68,.3);background:var(--red-dim)}
178
183
 
179
184
  .live-nav-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--purple);animation:pulse 1.5s infinite}
180
185
  .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}
@@ -189,6 +194,16 @@ tbody tr.selected td{background:var(--accent-dim)}
189
194
  .gallery-item .cap{padding:6px 10px;font-size:10px;color:var(--text2);display:flex;align-items:center;gap:4px}
190
195
  .gallery-item .cap .cap-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
191
196
  .gallery-item .cap .ss-hash{flex-shrink:0}
197
+ .ss-search{display:flex;gap:8px;margin-bottom:16px;align-items:center}
198
+ .ss-search input{flex:1;max-width:320px;padding:8px 12px;border-radius:var(--r);border:1px solid var(--border);background:var(--surface2);color:var(--text);font-family:var(--mono);font-size:12px}
199
+ .ss-search input:focus{outline:none;border-color:var(--accent)}
200
+ .ss-search input::placeholder{color:var(--text3)}
201
+ .ss-search button{padding:8px 16px;border-radius:var(--r);border:1px solid var(--border);background:var(--surface2);color:var(--text);font-family:var(--mono);font-size:12px;cursor:pointer;transition:background .15s,border-color .15s}
202
+ .ss-search button:hover{background:var(--surface3);border-color:var(--accent)}
203
+ .ss-search-result{margin-bottom:20px;padding:12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r)}
204
+ .ss-search-result img{max-width:100%;max-height:500px;border-radius:var(--r);cursor:pointer;display:block;margin-top:8px}
205
+ .ss-search-result .ss-result-label{font-size:11px;color:var(--text2);display:flex;align-items:center;gap:6px}
206
+ .ss-search-error{font-size:11px;color:var(--red);margin-bottom:12px}
192
207
 
193
208
  /* ── Inline Run Detail ── */
194
209
  .run-detail-row td{padding:0!important;border-bottom:2px solid var(--border-hi)}
@@ -217,6 +232,34 @@ tbody tr.selected td{background:var(--accent-dim)}
217
232
  .rd-log-item{font-size:10px;padding:3px 8px;border-left:2px solid var(--border);margin-bottom:2px;color:var(--text2);word-break:break-all}
218
233
  .rd-log-item.error{border-left-color:var(--red);color:var(--red)}
219
234
  .rd-log-item.warning,.rd-log-item.warn{border-left-color:var(--amber);color:var(--amber)}
235
+ .rd-net-toggle{display:flex;align-items:center;gap:6px;cursor:pointer;font-size:10px;color:var(--text3);font-family:var(--mono);padding:2px 0;user-select:none;margin-top:10px}
236
+ .rd-net-toggle:hover{color:var(--text)}
237
+ .rd-net-toggle .net-arrow{transition:transform .2s;font-size:8px}
238
+ .rd-net-toggle.open .net-arrow{transform:rotate(90deg)}
239
+ .rd-net-list{display:none;margin-top:6px}
240
+ .rd-net-toggle.open+.rd-net-list{display:block}
241
+ .rd-net-row{display:flex;align-items:center;gap:8px;padding:3px 8px;font-size:10px;font-family:var(--mono);border-left:2px solid var(--border);margin-bottom:0;cursor:pointer;transition:background .15s}
242
+ .rd-net-row:hover{background:var(--surface2)}
243
+ .rd-net-row.has-error{border-left-color:var(--red)}
244
+ .rd-net-method{display:inline-block;padding:1px 5px;border-radius:3px;font-size:9px;font-weight:700;min-width:36px;text-align:center}
245
+ .rd-net-method.get{background:var(--accent-dim);color:var(--accent)}
246
+ .rd-net-method.post{background:var(--green-dim);color:var(--green)}
247
+ .rd-net-method.put,.rd-net-method.patch{background:var(--amber-dim);color:var(--amber)}
248
+ .rd-net-method.delete{background:var(--red-dim);color:var(--red)}
249
+ .rd-net-status{display:inline-block;padding:1px 5px;border-radius:3px;font-size:9px;font-weight:600;min-width:28px;text-align:center}
250
+ .rd-net-status.s2xx{background:var(--green-dim);color:var(--green)}
251
+ .rd-net-status.s3xx{background:var(--amber-dim);color:var(--amber)}
252
+ .rd-net-status.s4xx,.rd-net-status.s5xx{background:var(--red-dim);color:var(--red)}
253
+ .rd-net-url{color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
254
+ .rd-net-dur{color:var(--text3);flex-shrink:0}
255
+ .rd-net-expand{font-size:8px;color:var(--text3);transition:transform .2s;flex-shrink:0}
256
+ .rd-net-row.open .rd-net-expand{transform:rotate(90deg)}
257
+ .rd-net-detail{display:none;margin:0 0 4px 0;padding:8px 12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r);font-size:10px;font-family:var(--mono);overflow:hidden}
258
+ .rd-net-row.open+.rd-net-detail{display:block}
259
+ .rd-net-detail-section{margin-bottom:8px}
260
+ .rd-net-detail-section:last-child{margin-bottom:0}
261
+ .rd-net-detail-title{font-size:9px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.08em;margin-bottom:4px}
262
+ .rd-net-detail-body{white-space:pre-wrap;word-break:break-all;color:var(--text2);max-height:300px;overflow-y:auto;line-height:1.5}
220
263
  .rd-retries{font-size:10px;color:var(--amber);margin-bottom:6px}
221
264
  .rd-summary{display:flex;gap:16px;margin-bottom:14px;padding:10px 14px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r);font-size:11px;align-items:center}
222
265
  .rd-summary .rd-s-label{color:var(--text3);font-size:9px;text-transform:uppercase;letter-spacing:.08em}
@@ -240,6 +283,14 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
240
283
  .ss-hash.copied{border-color:var(--green);color:var(--green);background:var(--green-dim)}
241
284
  .ss-hash .ss-icon{font-size:10px;line-height:1}
242
285
 
286
+ /* ── Trigger Source Badges ── */
287
+ .trigger-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}
288
+ .trigger-badge.src-dashboard{background:rgba(127,140,162,.10);color:var(--text2)}
289
+ .trigger-badge.src-mcp{background:var(--purple-dim);color:var(--purple)}
290
+ .trigger-badge.src-cli{background:var(--accent-dim);color:var(--accent)}
291
+ .trigger-badge.src-unknown{background:rgba(70,75,98,.15);color:var(--text3)}
292
+ .trigger-badge .trig-icon{font-size:11px;line-height:1}
293
+
243
294
  /* ── Responsive ── */
244
295
  @media(max-width:768px){
245
296
  .sidebar{width:60px}.sidebar-logo h1,.sidebar-section-label,.nav-item span:not(.icon),.pool-info,.sidebar select,.sidebar-logo .ver{display:none}
@@ -312,6 +363,7 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
312
363
  <span style="color:var(--red)">Fail: <strong id="liveFail">0</strong></span>
313
364
  <span style="color:var(--purple)">Active: <strong id="liveActive">0</strong></span>
314
365
  </div>
366
+ <button class="live-clear-btn" id="liveClearBtn">Clear All</button>
315
367
  </div>
316
368
  <div class="live-progress"><div class="live-progress-fill" id="liveProgressFill" style="width:0"></div></div>
317
369
  <div class="live-tests" id="liveTests"></div>
@@ -360,6 +412,11 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
360
412
  <!-- Screenshots View -->
361
413
  <div class="view" id="view-screenshots">
362
414
  <div style="font-family:var(--sans);font-size:16px;font-weight:600;margin-bottom:20px">Screenshots</div>
415
+ <div class="ss-search">
416
+ <input type="text" id="ssHashInput" placeholder="Search by hash (e.g. ss:a3f2b1c9)" spellcheck="false">
417
+ <button id="ssHashBtn">Search</button>
418
+ </div>
419
+ <div id="ssSearchResult"></div>
363
420
  <div class="gallery" id="screenshotGallery"></div>
364
421
  <div class="empty" id="screenshotsEmpty" style="display:none">
365
422
  <div class="empty-icon">&#9635;</div>
@@ -393,6 +450,60 @@ function css(n){return n.replace(/[^a-zA-Z0-9\-_]/g,'_')}
393
450
  function dur(ms){return ms>=1000?(ms/1000).toFixed(1)+'s':ms+'ms'}
394
451
  function fdate(iso){return iso?new Date(iso).toLocaleString():'--'}
395
452
 
453
+ /** Pretty-print a string if it's JSON, otherwise return as-is */
454
+ function prettyJson(str){
455
+ if(!str)return '';
456
+ try{return JSON.stringify(JSON.parse(str),null,2)}catch(e){return str}
457
+ }
458
+
459
+ /** Format headers object as readable string */
460
+ function fmtHeaders(h){
461
+ if(!h||typeof h!=='object')return '';
462
+ return Object.keys(h).map(function(k){return k+': '+h[k]}).join('\n');
463
+ }
464
+
465
+ /** Build a clickable network row + expandable detail panel */
466
+ function buildNetRow(n){
467
+ var mCls='rd-net-method '+n.method.toLowerCase();
468
+ var sCls='rd-net-status '+(n.status<300?'s2xx':n.status<400?'s3xx':n.status<500?'s4xx':'s5xx');
469
+ var hasDetail=n.requestBody||n.responseBody||n.requestHeaders||n.responseHeaders;
470
+ var rowCls='rd-net-row'+(n.status>=400?' has-error':'');
471
+ var row=el('div',{className:rowCls},[
472
+ hasDetail?el('span',{className:'rd-net-expand'},'\u25B6'):null,
473
+ el('span',{className:mCls},n.method),
474
+ el('span',{className:sCls},String(n.status)+(n.statusText?' '+n.statusText:'')),
475
+ el('span',{className:'rd-net-url'},n.url),
476
+ el('span',{className:'rd-net-dur'},dur(n.duration))
477
+ ]);
478
+ var detail=null;
479
+ if(hasDetail){
480
+ var sections=[];
481
+ if(n.requestHeaders){
482
+ var s=el('div',{className:'rd-net-detail-section'},[el('div',{className:'rd-net-detail-title'},'Request Headers')]);
483
+ s.appendChild(el('div',{className:'rd-net-detail-body'},fmtHeaders(n.requestHeaders)));
484
+ sections.push(s);
485
+ }
486
+ if(n.requestBody){
487
+ var s2=el('div',{className:'rd-net-detail-section'},[el('div',{className:'rd-net-detail-title'},'Request Body')]);
488
+ s2.appendChild(el('div',{className:'rd-net-detail-body'},prettyJson(n.requestBody)));
489
+ sections.push(s2);
490
+ }
491
+ if(n.responseHeaders){
492
+ var s3=el('div',{className:'rd-net-detail-section'},[el('div',{className:'rd-net-detail-title'},'Response Headers')]);
493
+ s3.appendChild(el('div',{className:'rd-net-detail-body'},fmtHeaders(n.responseHeaders)));
494
+ sections.push(s3);
495
+ }
496
+ if(n.responseBody){
497
+ var s4=el('div',{className:'rd-net-detail-section'},[el('div',{className:'rd-net-detail-title'},'Response Body')]);
498
+ s4.appendChild(el('div',{className:'rd-net-detail-body'},prettyJson(n.responseBody)));
499
+ sections.push(s4);
500
+ }
501
+ detail=el('div',{className:'rd-net-detail'},sections);
502
+ row.addEventListener('click',function(e){e.stopPropagation();row.classList.toggle('open')});
503
+ }
504
+ return {row:row,detail:detail};
505
+ }
506
+
396
507
  /* ── Screenshot hash helpers ── */
397
508
  var ssHashCache={};
398
509
  async function ssHash(filePath){
@@ -419,6 +530,18 @@ function createHashBadge(hash){
419
530
  return badge;
420
531
  }
421
532
 
533
+ /* ── Trigger source badge helper ── */
534
+ function createTriggerBadge(source){
535
+ var s=source||'unknown';
536
+ var labels={dashboard:'Dashboard',mcp:'MCP',cli:'CLI',unknown:'--'};
537
+ var icons={dashboard:'\u{1F464}',mcp:'\u{1F916}',cli:'>_',unknown:'\u2022'};
538
+ var badge=el('span',{className:'trigger-badge src-'+s},[
539
+ el('span',{className:'trig-icon'},icons[s]||icons.unknown),
540
+ document.createTextNode(labels[s]||s)
541
+ ]);
542
+ return badge;
543
+ }
544
+
422
545
  /* ── State ── */
423
546
  var S={
424
547
  ws:null,project:null,view:'suites',selectedRun:null,
@@ -454,21 +577,30 @@ function connectWS(){
454
577
 
455
578
  function getLiveRun(m){
456
579
  var rid=m.runId;if(!rid)return null;
457
- if(!S.liveRuns[rid])S.liveRuns[rid]={on:true,done:false,total:0,completed:0,passed:0,failed:0,active:0,tests:{},project:m.project||null,cwd:m.cwd||null,runId:rid,_lastEvent:Date.now()};
580
+ if(!S.liveRuns[rid])S.liveRuns[rid]={on:true,done:false,total:0,completed:0,passed:0,failed:0,active:0,tests:{},project:m.project||null,cwd:m.cwd||null,triggeredBy:m.triggeredBy||null,runId:rid,_lastEvent:Date.now()};
458
581
  S.liveRuns[rid]._lastEvent=Date.now();
459
582
  return S.liveRuns[rid];
460
583
  }
461
584
  function anyLiveRunning(){for(var k in S.liveRuns)if(S.liveRuns[k].on)return true;return false}
462
585
 
463
- /* Staleness guard: if a live run gets no events for 15s and all tests are complete, auto-finish it */
586
+ /* Staleness guard: auto-finish stuck runs, garbage-collect old finished runs */
464
587
  setInterval(function(){
465
588
  var changed=false;
466
589
  for(var k in S.liveRuns){
467
590
  var r=S.liveRuns[k];
468
- if(r.on&&!r.done&&Date.now()-r._lastEvent>15000){
469
- if(r.completed>=r.total&&r.total>0){r.on=false;r.done=true;r.active=0;changed=true}
470
- else if(Date.now()-r._lastEvent>30000){r.on=false;r.done=true;r.stale=true;r.active=0;changed=true}
591
+ var age=Date.now()-r._lastEvent;
592
+ /* Mark stuck runs as done */
593
+ if(r.on&&!r.done){
594
+ /* 0/0 runs (never received any tests) — mark stale after 10s */
595
+ if(r.total===0&&age>10000){r.on=false;r.done=true;r.stale=true;r.active=0;changed=true}
596
+ /* All tests completed but run:complete never arrived */
597
+ else if(r.completed>=r.total&&r.total>0&&age>15000){r.on=false;r.done=true;r.active=0;changed=true}
598
+ /* General staleness — no events for 30s */
599
+ else if(age>30000){r.on=false;r.done=true;r.stale=true;r.active=0;changed=true}
471
600
  }
601
+ /* Auto-remove stale 0/0 runs after 15s, finished runs after 120s */
602
+ if(r.done&&r.stale&&r.total===0&&age>15000){delete S.liveRuns[k];changed=true}
603
+ else if(r.done&&age>120000){delete S.liveRuns[k];changed=true}
472
604
  }
473
605
  if(changed)renderLive();
474
606
  },5000);
@@ -477,6 +609,8 @@ function handleWS(m){
477
609
  switch(m.event){
478
610
  case 'pool:status':renderPool(m.data);break;
479
611
  case 'run:start':
612
+ /* Clear all finished/stale runs when a new one starts */
613
+ for(var dk in S.liveRuns){if(S.liveRuns[dk].done)delete S.liveRuns[dk]}
480
614
  var r=getLiveRun(m);
481
615
  r.total=m.total;r.on=true;r.done=false;
482
616
  S.liveExpanded=new Set();S.liveSSOpen=new Set();
@@ -506,6 +640,7 @@ function handleWS(m){
506
640
  r.tests[m.name].duration=m.duration;
507
641
  if(m.screenshots&&m.screenshots.length)r.tests[m.name].screenshots=m.screenshots;
508
642
  if(m.errorScreenshot)r.tests[m.name].errorScreenshot=m.errorScreenshot;
643
+ if(m.networkLogs&&m.networkLogs.length)r.tests[m.name].networkLogs=m.networkLogs;
509
644
  }
510
645
  r.active=Math.max(0,r.active-1);
511
646
  renderLive();break;
@@ -619,7 +754,7 @@ function refreshRuns(){
619
754
  var htr=document.createElement('tr');
620
755
  var cols=[];
621
756
  if(!S.project)cols.push('Project');
622
- cols=cols.concat(['Suite','Date','Total','Pass','Fail','Rate','Time']);
757
+ cols=cols.concat(['Suite','Source','Date','Total','Pass','Fail','Rate','Time']);
623
758
  cols.forEach(function(c){htr.appendChild(el('th',null,c))});
624
759
  head.textContent='';head.appendChild(htr);
625
760
  var colSpan=cols.length;
@@ -640,6 +775,7 @@ function refreshRuns(){
640
775
  if(r.id===S.selectedRun)tr.classList.add('expanded');
641
776
  if(!S.project)tr.appendChild(el('td',{style:'font-weight:600'},r.project_name||'-'));
642
777
  tr.appendChild(el('td',{style:'color:var(--accent)'},r.suite_name||'all'));
778
+ var srcTd=document.createElement('td');srcTd.appendChild(createTriggerBadge(r.triggered_by));tr.appendChild(srcTd);
643
779
  tr.appendChild(el('td',null,fdate(r.generated_at)));
644
780
  tr.appendChild(el('td',null,String(r.total||0)));
645
781
  tr.appendChild(el('td',{style:'color:var(--green)'},String(r.passed||0)));
@@ -726,8 +862,10 @@ function loadDetailInline(id,detailTr){
726
862
  var results=d.results||[];
727
863
 
728
864
  // Summary bar
865
+ var srcBlock=el('div',null,[el('div',{className:'rd-s-label'},'Source'),el('div',{style:'margin-top:4px'},[createTriggerBadge(d.triggeredBy)])]);
729
866
  var summ=el('div',{className:'rd-summary'},[
730
867
  el('div',null,[el('div',{className:'rd-s-label'},'Suite'),el('div',{className:'rd-s-val',style:'font-size:13px;color:var(--accent)'},d.suiteName||'all')]),
868
+ srcBlock,
731
869
  el('div',null,[el('div',{className:'rd-s-label'},'Total'),el('div',{className:'rd-s-val'},String(d.summary.total))]),
732
870
  el('div',null,[el('div',{className:'rd-s-label'},'Passed'),el('div',{className:'rd-s-val',style:'color:var(--green)'},String(d.summary.passed))]),
733
871
  el('div',null,[el('div',{className:'rd-s-label'},'Failed'),el('div',{className:'rd-s-val',style:'color:var(--red)'},String(d.summary.failed))]),
@@ -812,6 +950,27 @@ function loadDetailInline(id,detailTr){
812
950
  body.appendChild(netSec);
813
951
  }
814
952
 
953
+ // Network API logs (collapsible, clickable rows)
954
+ if(r.networkLogs&&r.networkLogs.length){
955
+ var errCount=r.networkLogs.filter(function(n){return n.status>=400}).length;
956
+ var netLabel='Network ('+r.networkLogs.length+' request'+(r.networkLogs.length!==1?'s':'')+(errCount?', '+errCount+' error'+(errCount!==1?'s':''):'')+')';
957
+ var netToggle=el('div',{className:'rd-net-toggle'},[
958
+ el('span',{className:'net-arrow'},'\u25B6'),
959
+ el('span',{},netLabel)
960
+ ]);
961
+ var netList=el('div',{className:'rd-net-list'});
962
+ r.networkLogs.forEach(function(n){
963
+ var built=buildNetRow(n);
964
+ netList.appendChild(built.row);
965
+ if(built.detail)netList.appendChild(built.detail);
966
+ });
967
+ netToggle.addEventListener('click',function(){
968
+ netToggle.classList.toggle('open');
969
+ });
970
+ body.appendChild(netToggle);
971
+ body.appendChild(netList);
972
+ }
973
+
815
974
  var testCard=el('div',{className:'rd-test'},[head,body]);
816
975
  inner.appendChild(testCard);
817
976
  });
@@ -849,7 +1008,56 @@ function refreshScreenshots(){
849
1008
  }).catch(function(){});
850
1009
  }
851
1010
 
1011
+ /* ── Screenshot Hash Search ── */
1012
+ function searchByHash(){
1013
+ var container=$('#ssSearchResult');
1014
+ container.textContent='';
1015
+ var raw=$('#ssHashInput').value.trim();
1016
+ if(!raw)return;
1017
+ var hash=raw.replace(/^ss:/,'');
1018
+ if(!/^[a-f0-9]{1,8}$/i.test(hash)){
1019
+ container.appendChild(el('div',{className:'ss-search-error'},'Invalid hash format. Expected 8 hex characters (e.g. ss:a3f2b1c9).'));
1020
+ return;
1021
+ }
1022
+ fetch('/api/screenshot-hash/'+hash).then(function(res){
1023
+ if(!res.ok){
1024
+ container.appendChild(el('div',{className:'ss-search-error'},'Screenshot not found for hash: ss:'+hash));
1025
+ return;
1026
+ }
1027
+ return res.blob();
1028
+ }).then(function(blob){
1029
+ if(!blob)return;
1030
+ var url=URL.createObjectURL(blob);
1031
+ var wrap=el('div',{className:'ss-search-result'},[
1032
+ el('div',{className:'ss-result-label'},[
1033
+ createHashBadge(hash),
1034
+ el('span',{},'Found')
1035
+ ])
1036
+ ]);
1037
+ var img=document.createElement('img');
1038
+ img.src=url;
1039
+ img.alt='ss:'+hash;
1040
+ img.addEventListener('click',function(){openModal(url)});
1041
+ wrap.appendChild(img);
1042
+ container.appendChild(wrap);
1043
+ }).catch(function(){
1044
+ container.appendChild(el('div',{className:'ss-search-error'},'Error searching for screenshot.'));
1045
+ });
1046
+ }
1047
+ $('#ssHashBtn').addEventListener('click',searchByHash);
1048
+ $('#ssHashInput').addEventListener('keydown',function(e){if(e.key==='Enter')searchByHash()});
1049
+
852
1050
  /* ── Live Execution ── */
1051
+ function clearFinishedLiveRuns(){
1052
+ for(var k in S.liveRuns){if(S.liveRuns[k].done||!S.liveRuns[k].on)delete S.liveRuns[k]}
1053
+ renderLive();
1054
+ }
1055
+ function dismissLiveRun(rid){
1056
+ delete S.liveRuns[rid];
1057
+ renderLive();
1058
+ }
1059
+ $('#liveClearBtn').addEventListener('click',clearFinishedLiveRuns);
1060
+
853
1061
  function renderLive(){
854
1062
  var panel=$('#livePanel'),grid=$('#liveTests'),navLive=$('#navLive'),liveEmpty=$('#liveEmpty');
855
1063
  var runs=S.liveRuns;
@@ -859,6 +1067,7 @@ function renderLive(){
859
1067
  panel.classList.remove('active');
860
1068
  navLive.style.display='none';
861
1069
  liveEmpty.style.display='block';
1070
+ $('#liveClearBtn').style.display='none';
862
1071
  return;
863
1072
  }
864
1073
 
@@ -889,6 +1098,10 @@ function renderLive(){
889
1098
  // Hide single-project info (now shown per-section)
890
1099
  $('#liveProject').style.display='none';
891
1100
 
1101
+ // Show "Clear All" when there are finished/stale runs
1102
+ var hasFinished=runIds.some(function(rid){return runs[rid].done||!runs[rid].on});
1103
+ $('#liveClearBtn').style.display=hasFinished?'inline-block':'none';
1104
+
892
1105
  // Header state
893
1106
  var lbl=panel.querySelector('.live-header .label');
894
1107
  var anyStale=runIds.some(function(rid){return runs[rid].stale});
@@ -913,13 +1126,19 @@ function renderLive(){
913
1126
  // Project section header
914
1127
  var projLabel=L.project||(L.cwd?L.cwd.split('/').pop():'Run');
915
1128
  var runStatus=L.done?(L.failed>0?'fail':'pass'):'running';
1129
+ var dismissBtn=null;
1130
+ if(L.done||!L.on){
1131
+ dismissBtn=el('button',{className:'lr-dismiss',onclick:function(e){e.stopPropagation();dismissLiveRun(rid)}},'\u2715');
1132
+ }
916
1133
  var sectionHeader=el('div',{className:'lr-section-header '+runStatus},[
917
1134
  el('span',{className:'lr-project-name'},projLabel),
1135
+ createTriggerBadge(L.triggeredBy),
918
1136
  el('span',{className:'lr-section-stats'},[
919
1137
  el('span',{},L.completed+'/'+L.total),
920
1138
  L.failed>0?el('span',{style:'color:var(--red);margin-left:6px'},L.failed+' failed'):null,
921
1139
  L.on?el('span',{className:'spinner-small',style:'margin-left:6px'}):null
922
- ])
1140
+ ]),
1141
+ dismissBtn
923
1142
  ]);
924
1143
  grid.appendChild(sectionHeader);
925
1144
 
@@ -1009,6 +1228,24 @@ function renderLive(){
1009
1228
  stepsEl
1010
1229
  ]);
1011
1230
  if(ssEl)card.appendChild(ssEl);
1231
+ // Network API logs in live view (clickable rows)
1232
+ if(t.networkLogs&&t.networkLogs.length&&!isCollapsed){
1233
+ var liveErrCount=t.networkLogs.filter(function(n){return n.status>=400}).length;
1234
+ var liveNetLabel='Network ('+t.networkLogs.length+(liveErrCount?', '+liveErrCount+' error'+(liveErrCount!==1?'s':'')+')':')');
1235
+ var liveNetToggle=el('div',{className:'rd-net-toggle',style:'margin:6px 0 0;padding-left:0'},[
1236
+ el('span',{className:'net-arrow'},'\u25B6'),
1237
+ el('span',{},liveNetLabel)
1238
+ ]);
1239
+ var liveNetList=el('div',{className:'rd-net-list'});
1240
+ t.networkLogs.forEach(function(n){
1241
+ var built=buildNetRow(n);
1242
+ liveNetList.appendChild(built.row);
1243
+ if(built.detail)liveNetList.appendChild(built.detail);
1244
+ });
1245
+ liveNetToggle.addEventListener('click',function(e){e.stopPropagation();liveNetToggle.classList.toggle('open')});
1246
+ card.appendChild(liveNetToggle);
1247
+ card.appendChild(liveNetList);
1248
+ }
1012
1249
  if(isFinished){
1013
1250
  card.addEventListener('click',function(){
1014
1251
  if(S.liveExpanded.has(testKey))S.liveExpanded.delete(testKey);