@matware/e2e-runner 1.5.0 → 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.
@@ -775,6 +775,23 @@ tbody tr.selected td{
775
775
  cursor:default;
776
776
  box-shadow:0 0 0 1px rgba(158,242,106,.12),0 40px 80px rgba(0,0,0,.6);
777
777
  }
778
+ .modal pre{
779
+ max-width:min(900px,100%);max-height:90vh;overflow:auto;
780
+ margin:0;padding:20px 24px;
781
+ background:var(--bg-2);
782
+ border:1px solid var(--border);border-radius:var(--r);
783
+ font-family:var(--mono);font-size:12px;line-height:1.6;
784
+ color:var(--text);cursor:text;user-select:text;
785
+ box-shadow:0 0 0 1px rgba(158,242,106,.12),0 40px 80px rgba(0,0,0,.6);
786
+ }
787
+ .modal pre[hidden],.modal img[hidden]{display:none}
788
+
789
+ /* ── JSON syntax highlighting (modal + network body panels) ── */
790
+ .jn-key{color:var(--beacon)}
791
+ .jn-str{color:var(--amber)}
792
+ .jn-num{color:var(--phosphor)}
793
+ .jn-bool{color:var(--red);font-weight:600}
794
+ .jn-null{color:var(--red);opacity:.65;font-style:italic}
778
795
 
779
796
  /* ── Toasts ── */
780
797
  .toast-container{
@@ -2396,10 +2413,49 @@ tr.expanded td:first-child::before{
2396
2413
  .hb-link span{font-size:10px;color:var(--beacon);font-weight:700;letter-spacing:.18em;text-transform:uppercase}
2397
2414
 
2398
2415
  /* ═══════════════════ Screenshots ═══════════════════ */
2416
+ .gallery-group{
2417
+ border:1px solid var(--border);
2418
+ border-radius:var(--r);
2419
+ background:var(--surface);
2420
+ margin-bottom:10px;
2421
+ overflow:hidden;
2422
+ }
2423
+ .gallery-group-header{
2424
+ display:flex;align-items:center;gap:10px;
2425
+ padding:10px 14px;cursor:pointer;user-select:none;
2426
+ background:var(--bg-2);
2427
+ transition:background .15s;
2428
+ }
2429
+ .gallery-group-header:hover{background:var(--surface)}
2430
+ .gallery-group .gg-arrow{
2431
+ font-size:9px;color:var(--text2);
2432
+ transition:transform .15s;flex-shrink:0;
2433
+ }
2434
+ .gallery-group.open .gg-arrow{transform:rotate(90deg)}
2435
+ .gallery-group .gg-name{
2436
+ font-family:var(--mono);font-size:12px;font-weight:600;
2437
+ color:var(--text);
2438
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;
2439
+ }
2440
+ .gallery-group .gg-count{
2441
+ font-family:var(--mono);font-size:10px;color:var(--text2);
2442
+ padding:2px 8px;border-radius:999px;
2443
+ background:var(--bg);border:1px solid var(--border);
2444
+ flex-shrink:0;margin-left:auto;
2445
+ }
2446
+ .gallery-group .gg-blank-badge{
2447
+ font-family:var(--mono);font-size:9px;font-weight:700;
2448
+ padding:2px 8px;border-radius:999px;
2449
+ background:var(--amber-dim);color:var(--amber);border:1px solid var(--amber);
2450
+ flex-shrink:0;
2451
+ }
2452
+ .gallery-group .gg-blank-badge[hidden]{display:none}
2453
+ .gallery-group-body{padding:12px}
2454
+ .gallery-group-body[hidden]{display:none}
2399
2455
  .gallery{
2400
2456
  display:grid;
2401
- grid-template-columns:repeat(auto-fill,minmax(230px,1fr));
2402
- gap:14px;
2457
+ grid-template-columns:repeat(auto-fill,minmax(150px,1fr));
2458
+ gap:10px;
2403
2459
  }
2404
2460
  .gallery-item{
2405
2461
  background:var(--surface);
@@ -2413,11 +2469,11 @@ tr.expanded td:first-child::before{
2413
2469
  content:'';position:absolute;top:0;left:0;width:18px;height:1px;
2414
2470
  background:var(--phosphor);opacity:.4;z-index:1;
2415
2471
  }
2416
-
2472
+ .gallery-item:hover{
2417
2473
  border-color:var(--ui-accent);transform:translateY(-2px);
2418
2474
  box-shadow:0 12px 28px rgba(0,0,0,.4);
2419
2475
  }
2420
- .gallery-item img{width:100%;height:160px;object-fit:cover;display:block}
2476
+ .gallery-item img{width:100%;aspect-ratio:1/1;height:auto;object-fit:cover;object-position:top center;display:block}
2421
2477
  .gallery-item .cap{
2422
2478
  padding:8px 12px;font-size:10px;
2423
2479
  color:var(--text2);display:flex;align-items:center;gap:6px;
@@ -2692,6 +2748,11 @@ tr.expanded td:first-child::before{
2692
2748
  background:repeating-linear-gradient(45deg,var(--surface2),var(--surface2) 6px,var(--bg-2) 6px,var(--bg-2) 12px);
2693
2749
  }
2694
2750
  .sl-thumb-empty:hover{transform:none;box-shadow:none}
2751
+ .sl-thumb-json{
2752
+ display:flex;align-items:center;justify-content:center;
2753
+ color:var(--beacon);font-family:var(--mono);font-size:18px;font-weight:700;
2754
+ background:var(--bg-2);
2755
+ }
2695
2756
  .sl-thumb-err{border-color:var(--crimson)}
2696
2757
 
2697
2758
  .sl-info{display:flex;flex-direction:column;gap:6px;min-width:0}
@@ -2754,6 +2815,9 @@ tr.expanded td:first-child::before{
2754
2815
  }
2755
2816
  .sl-chip-v{color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:280px}
2756
2817
  .sl-chip-sel .sl-chip-v{color:var(--beacon)}
2818
+ .sl-chip-json{cursor:pointer;border-color:var(--beacon)}
2819
+ .sl-chip-json .sl-chip-k{color:var(--beacon)}
2820
+ .sl-chip-json:hover{background:var(--beacon-dim)}
2757
2821
 
2758
2822
  .sl-bar{
2759
2823
  width:100%;height:3px;
@@ -3740,7 +3804,7 @@ tr.expanded td:first-child::before{
3740
3804
  <button class="btn sm" id="ssBlankCancelBtn">Cancel</button>
3741
3805
  </div>
3742
3806
  </div>
3743
- <div class="gallery" id="screenshotGallery"></div>
3807
+ <div class="gallery-groups" id="screenshotGallery"></div>
3744
3808
  <div class="empty" id="screenshotsEmpty" style="display:none">
3745
3809
  <div class="empty-icon">&#9635;</div>
3746
3810
  <p>Select a project to view screenshots.</p>
@@ -3986,7 +4050,7 @@ tr.expanded td:first-child::before{
3986
4050
  </div>
3987
4051
  </div>
3988
4052
  </div>
3989
- <div class="modal" id="modal"><img id="modalImg" src="" alt=""></div>
4053
+ <div class="modal" id="modal"><img id="modalImg" src="" alt=""><pre id="modalJson" hidden></pre></div>
3990
4054
 
3991
4055
  <!-- ── Quick Search Palette ── -->
3992
4056
  <div class="qs-modal" id="qsModal" aria-hidden="true">
@@ -4076,6 +4140,27 @@ function prettyJson(str){
4076
4140
  try{return JSON.stringify(JSON.parse(str),null,2)}catch(e){return str}
4077
4141
  }
4078
4142
 
4143
+ /* Lightweight JSON syntax highlighter. Escapes HTML, then wraps tokens in
4144
+ colored spans. Input should already be pretty-printed (prettyJson). */
4145
+ function highlightJson(text){
4146
+ var esc=String(text).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
4147
+ return esc.replace(/"(?:\\.|[^"\\])*"|\b(?:true|false|null)\b|-?\b\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?\b/g,function(m,off,s){
4148
+ var cls;
4149
+ if(m[0]==='"'){cls=/^\s*:/.test(s.slice(off+m.length))?'jn-key':'jn-str'}
4150
+ else if(m==='true'||m==='false'){cls='jn-bool'}
4151
+ else if(m==='null'){cls='jn-null'}
4152
+ else{cls='jn-num'}
4153
+ return '<span class="'+cls+'">'+m+'</span>';
4154
+ });
4155
+ }
4156
+
4157
+ /* Builds a <pre> with syntax-highlighted JSON content. */
4158
+ function jsonPre(text){
4159
+ var p=document.createElement('pre');
4160
+ p.innerHTML=highlightJson(text);
4161
+ return p;
4162
+ }
4163
+
4079
4164
  function fmtHeaders(h){
4080
4165
  if(!h||typeof h!=='object')return '';
4081
4166
  return Object.keys(h).map(function(k){return k+': '+h[k]}).join('\n');
@@ -4162,7 +4247,7 @@ function buildNetRow(n){
4162
4247
  }
4163
4248
  if(n.requestBody){
4164
4249
  var rbText=prettyJson(n.requestBody);
4165
- sections.push(buildNdSection('Request Body',el('pre',null,rbText),null,rbText));
4250
+ sections.push(buildNdSection('Request Body',jsonPre(rbText),null,rbText));
4166
4251
  }
4167
4252
  if(n.responseHeaders){
4168
4253
  var rhCount=Object.keys(n.responseHeaders).length;
@@ -4170,7 +4255,7 @@ function buildNetRow(n){
4170
4255
  }
4171
4256
  if(n.responseBody){
4172
4257
  var respText=prettyJson(n.responseBody);
4173
- sections.push(buildNdSection('Response Body',el('pre',null,respText),null,respText));
4258
+ sections.push(buildNdSection('Response Body',jsonPre(respText),null,respText));
4174
4259
  }
4175
4260
  detail=el('div',{className:'rd-net-detail'},sections);
4176
4261
  row.addEventListener('click',function(e){e.stopPropagation();row.classList.toggle('open')});
@@ -5676,7 +5761,8 @@ function loadDetailInline(id,detailTr){
5676
5761
  var stateCls=a.success?'pass':'fail';
5677
5762
  var icon=a.success?'\u2714':'\u2718';
5678
5763
 
5679
- // Thumbnail: prefer autoScreenshot, fall back to action's own screenshot
5764
+ // Thumbnail: prefer autoScreenshot, fall back to action's own screenshot.
5765
+ // Data-capture steps (raw JSON saved instead of a screenshot) get a {} thumb.
5680
5766
  var thumbPath=a.autoScreenshot||a.screenshot||null;
5681
5767
  var thumb;
5682
5768
  if(thumbPath){
@@ -5684,6 +5770,10 @@ function loadDetailInline(id,detailTr){
5684
5770
  var img=document.createElement('img');
5685
5771
  img.src=src;img.alt=label;img.loading='lazy';
5686
5772
  thumb=el('div',{className:'sl-thumb',onclick:function(e){e.stopPropagation();openModal(src)}},[img]);
5773
+ }else if(a.dataCapture){
5774
+ (function(dc){
5775
+ thumb=el('div',{className:'sl-thumb sl-thumb-json',title:'Raw JSON response \u2014 click to view',onclick:function(e){e.stopPropagation();openJsonModal(dc)}},[el('span',null,'{\u2009}')]);
5776
+ })(a.dataCapture);
5687
5777
  }else{
5688
5778
  thumb=el('div',{className:'sl-thumb sl-thumb-empty',title:'No screenshot for this step'},[el('span',null,'\u25A1')]);
5689
5779
  }
@@ -5693,6 +5783,12 @@ function loadDetailInline(id,detailTr){
5693
5783
  if(a.selector)chips.appendChild(el('span',{className:'sl-chip sl-chip-sel'},[el('span',{className:'sl-chip-k'},'sel'),el('span',{className:'sl-chip-v'},a.selector)]));
5694
5784
  if(a.text)chips.appendChild(el('span',{className:'sl-chip'},[el('span',{className:'sl-chip-k'},'text'),el('span',{className:'sl-chip-v'},String(a.text))]));
5695
5785
  if(a.value!=null&&a.value!=='')chips.appendChild(el('span',{className:'sl-chip'},[el('span',{className:'sl-chip-k'},'val'),el('span',{className:'sl-chip-v'},String(a.value))]));
5786
+ if(a.dataCapture)(function(dc){
5787
+ chips.appendChild(el('span',{className:'sl-chip sl-chip-json',title:'Raw JSON response saved for this step \u2014 click to view',onclick:function(e){e.stopPropagation();openJsonModal(dc)}},[
5788
+ el('span',{className:'sl-chip-k'},'JSON'),
5789
+ el('span',{className:'sl-chip-v'},dc.split('/').pop())
5790
+ ]));
5791
+ })(a.dataCapture);
5696
5792
 
5697
5793
  var retryBadge=(a.actionRetries&&a.actionRetries>0)?el('span',{className:'sl-retry'},'\u21BB '+a.actionRetries):null;
5698
5794
 
@@ -5823,6 +5919,15 @@ function loadDetailInline(id,detailTr){
5823
5919
  }
5824
5920
 
5825
5921
  /* ── Screenshots ── */
5922
+ /* Session memory: which test groups the user expanded (survives refreshes, not reloads) */
5923
+ var SS_EXPANDED={};
5924
+ function buildGalleryItem(f){
5925
+ var src='/api/image?path='+encodeURIComponent(f.path);
5926
+ var img=document.createElement('img');img.src=src;img.alt=f.name;img.loading='lazy';
5927
+ var capEl=el('div',{className:'cap'},[el('span',{className:'cap-name'},f.name)]);
5928
+ (function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,f.path);
5929
+ return el('div',{className:'gallery-item','data-path':f.path,onclick:function(){openModal(src)}},[img,capEl]);
5930
+ }
5826
5931
  function refreshScreenshots(){
5827
5932
  var gal=$('#screenshotGallery'),empty=$('#screenshotsEmpty');
5828
5933
  gal.textContent='';
@@ -5831,12 +5936,50 @@ function refreshScreenshots(){
5831
5936
  if(!Array.isArray(files)||!files.length){empty.style.display='block';empty.querySelector('p').textContent='No screenshots for this project.';$('#badgeScreenshots').textContent='0';return}
5832
5937
  empty.style.display='none';
5833
5938
  $('#badgeScreenshots').textContent=files.length;
5939
+ /* Group by test name; files without one go to a trailing "Other" bucket */
5940
+ var groups={},order=[];
5834
5941
  files.forEach(function(f){
5835
- var src='/api/image?path='+encodeURIComponent(f.path);
5836
- var img=document.createElement('img');img.src=src;img.alt=f.name;img.loading='lazy';
5837
- var capEl=el('div',{className:'cap'},[el('span',{className:'cap-name'},f.name)]);
5838
- (function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,f.path);
5839
- gal.appendChild(el('div',{className:'gallery-item','data-path':f.path,onclick:function(){openModal(src)}},[img,capEl]));
5942
+ var key=f.testName||'__other__';
5943
+ if(!groups[key]){groups[key]=[];order.push(key)}
5944
+ groups[key].push(f);
5945
+ });
5946
+ function fileTs(f){var m=f.name.match(/(\d{10,})\.[a-z]+$/i);return m?parseInt(m[1],10):0}
5947
+ order.sort(function(a,b){
5948
+ if(a==='__other__')return 1;
5949
+ if(b==='__other__')return -1;
5950
+ var ma=0,mb=0;
5951
+ groups[a].forEach(function(f){var t=fileTs(f);if(t>ma)ma=t});
5952
+ groups[b].forEach(function(f){var t=fileTs(f);if(t>mb)mb=t});
5953
+ return mb-ma;
5954
+ });
5955
+ order.forEach(function(key){
5956
+ var items=groups[key],label=key==='__other__'?'Other':key;
5957
+ var grid=el('div',{className:'gallery'});
5958
+ var materialized=false;
5959
+ function materialize(){
5960
+ if(materialized)return;materialized=true;
5961
+ items.forEach(function(f){grid.appendChild(buildGalleryItem(f))});
5962
+ }
5963
+ var expanded=!!SS_EXPANDED[key];
5964
+ var body=el('div',{className:'gallery-group-body'},[grid]);
5965
+ var group=el('div',{className:'gallery-group'+(expanded?' open':'')},[]);
5966
+ var header=el('div',{className:'gallery-group-header',onclick:function(){
5967
+ expanded=!expanded;
5968
+ if(expanded){SS_EXPANDED[key]=true;materialize()}else{delete SS_EXPANDED[key]}
5969
+ group.classList.toggle('open',expanded);
5970
+ if(expanded)body.removeAttribute('hidden');else body.setAttribute('hidden','');
5971
+ }},[
5972
+ el('span',{className:'gg-arrow'},'▶'),
5973
+ el('span',{className:'gg-name'},label),
5974
+ el('span',{className:'gg-count'},items.length+' shot'+(items.length===1?'':'s')),
5975
+ el('span',{className:'gg-blank-badge',hidden:''},''),
5976
+ ]);
5977
+ group.appendChild(header);
5978
+ group.appendChild(body);
5979
+ if(expanded){materialize()}else{body.setAttribute('hidden','')}
5980
+ /* Expose to the blank-scan so it can materialize collapsed groups on demand */
5981
+ group._ssItems=items;group._ssMaterialize=materialize;
5982
+ gal.appendChild(group);
5840
5983
  });
5841
5984
  resetBlankBar();
5842
5985
  }).catch(function(){});
@@ -5846,6 +5989,7 @@ function refreshScreenshots(){
5846
5989
  function resetBlankBar(){
5847
5990
  var bar=$('#ssBlankBar');if(bar)bar.hidden=true;
5848
5991
  $$('#screenshotGallery .gallery-item.blank-flagged').forEach(function(it){it.classList.remove('blank-flagged')});
5992
+ $$('#screenshotGallery .gg-blank-badge').forEach(function(b){b.textContent='';b.setAttribute('hidden','')});
5849
5993
  S.blankPaths=null;
5850
5994
  }
5851
5995
  function scanBlankScreenshots(){
@@ -5861,10 +6005,18 @@ function scanBlankScreenshots(){
5861
6005
  return;
5862
6006
  }
5863
6007
  S.blankPaths=blanks.map(function(b){return b.path});
5864
- var found=0;
6008
+ /* Materialize collapsed groups that contain blanks, flag items, badge the headers */
6009
+ var blankSet={};S.blankPaths.forEach(function(p){blankSet[p]=true});
6010
+ $$('#screenshotGallery .gallery-group').forEach(function(g){
6011
+ var inGroup=(g._ssItems||[]).filter(function(it){return blankSet[it.path]}).length;
6012
+ if(!inGroup)return;
6013
+ if(g._ssMaterialize)g._ssMaterialize();
6014
+ var badge=g.querySelector('.gg-blank-badge');
6015
+ if(badge){badge.textContent=inGroup+' blank'+(inGroup===1?'':'s');badge.removeAttribute('hidden')}
6016
+ });
5865
6017
  S.blankPaths.forEach(function(p){
5866
6018
  var item=$('#screenshotGallery .gallery-item[data-path="'+(window.CSS&&CSS.escape?CSS.escape(p):p)+'"]');
5867
- if(item){item.classList.add('blank-flagged');found++}
6019
+ if(item)item.classList.add('blank-flagged');
5868
6020
  });
5869
6021
  $('#ssBlankMsg').textContent=blanks.length+' blank image'+(blanks.length===1?'':'s')+' of '+data.scanned+' scanned';
5870
6022
  $('#ssBlankBar').hidden=false;
@@ -6159,8 +6311,26 @@ $('#btnExportLearnings').addEventListener('click',function(){
6159
6311
  });
6160
6312
 
6161
6313
  /* ── Modal ── */
6162
- function openModal(src){$('#modalImg').src=src;$('#modal').classList.add('open')}
6314
+ function openModal(src){
6315
+ $('#modalImg').src=src;$('#modalImg').removeAttribute('hidden');
6316
+ $('#modalJson').setAttribute('hidden','');
6317
+ $('#modal').classList.add('open');
6318
+ }
6319
+ function openJsonModal(filePath){
6320
+ fetch('/api/image?path='+encodeURIComponent(filePath)).then(function(r){
6321
+ if(!r.ok)throw new Error('not found');
6322
+ return r.text();
6323
+ }).then(function(text){
6324
+ $('#modalImg').setAttribute('hidden','');
6325
+ var pre=$('#modalJson');
6326
+ pre.innerHTML=highlightJson(prettyJson(text));
6327
+ pre.removeAttribute('hidden');
6328
+ $('#modal').classList.add('open');
6329
+ }).catch(function(){showToast('Could not load JSON capture','error')});
6330
+ }
6163
6331
  $('#modal').addEventListener('click',function(){$('#modal').classList.remove('open')});
6332
+ /* Allow selecting/copying JSON text without closing the modal */
6333
+ $('#modalJson').addEventListener('click',function(e){e.stopPropagation()});
6164
6334
 
6165
6335
  /* ══════════════════════════════════════════════════════════════════
6166
6336
  Screenshot Replay Player — plays a run's per-step screenshots