@matware/e2e-runner 1.3.1 → 1.5.0
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/README.md +110 -21
- package/agents/test-creator.md +4 -2
- package/agents/test-improver.md +5 -3
- package/bin/cli.js +80 -17
- package/package.json +3 -2
- package/skills/e2e-testing/SKILL.md +3 -2
- package/skills/e2e-testing/references/action-types.md +22 -4
- package/skills/e2e-testing/references/test-json-format.md +23 -0
- package/src/actions.js +170 -14
- package/src/config.js +6 -0
- package/src/dashboard.js +135 -4
- package/src/db.js +11 -0
- package/src/mcp-tools.js +8 -2
- package/src/module-analysis.js +247 -0
- package/src/module-resolver.js +35 -2
- package/src/narrate.js +14 -1
- package/src/pool-manager.js +46 -1
- package/src/pool.js +177 -20
- package/src/runner.js +77 -10
- package/src/visual-diff.js +69 -0
- 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/view-live.js +235 -42
- package/templates/dashboard/js/view-runs.js +379 -37
- 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 +719 -84
- package/templates/dashboard/styles/view-live.css +459 -78
- package/templates/dashboard/styles/view-runs.css +779 -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 +354 -56
- package/templates/dashboard.html +5173 -711
- package/templates/docker-compose-lightpanda.yml +7 -0
|
@@ -67,8 +67,8 @@ function renderRunsHealthBanner(){
|
|
|
67
67
|
el('div',{className:'hb-lbl'},'Top Error ('+h.topErrorPattern.count+'x)')
|
|
68
68
|
]));
|
|
69
69
|
}
|
|
70
|
-
banner.appendChild(el('div',{className:'hb-link',onclick:function(){
|
|
71
|
-
el('span',null,'\u2192 View
|
|
70
|
+
banner.appendChild(el('div',{className:'hb-link',onclick:function(){showView('insights')}},[
|
|
71
|
+
el('span',null,'\u2192 View Insights')
|
|
72
72
|
]));
|
|
73
73
|
}).catch(function(){});
|
|
74
74
|
}
|
|
@@ -275,53 +275,121 @@ function loadDetailInline(id,detailTr){
|
|
|
275
275
|
body.appendChild(errDiv);
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
//
|
|
278
|
+
// Storyline \u2014 unified per-step cards with thumbnails, narrative, duration bar
|
|
279
279
|
if(r.actions&&r.actions.length){
|
|
280
280
|
var passCount=r.actions.filter(function(a){return a.success}).length;
|
|
281
281
|
var failCount=r.actions.length-passCount;
|
|
282
|
-
var
|
|
282
|
+
var maxDur=Math.max.apply(null,r.actions.map(function(a){return a.duration||0}).concat([1]));
|
|
283
|
+
var hashes=r.screenshotHashes||{};
|
|
284
|
+
|
|
285
|
+
// "Play replay" button \u2014 only shown if at least one step has a screenshot
|
|
286
|
+
var hasFrames=r.actions.some(function(a){return a.autoScreenshot||a.screenshot})||(!r.success&&r.errorScreenshot);
|
|
287
|
+
var replayBtn=hasFrames?el('button',{
|
|
288
|
+
className:'rd-replay-btn',
|
|
289
|
+
title:'Replay step-by-step',
|
|
290
|
+
onclick:function(e){e.stopPropagation();openReplay(r.actions,r)}
|
|
291
|
+
},'\u25B6 Replay'):null;
|
|
292
|
+
var slHead=el('div',{className:'rd-net-head open'},[
|
|
283
293
|
el('span',{className:'net-arrow'},'\u25B6'),
|
|
284
|
-
el('span',{className:'net-title'},'
|
|
294
|
+
el('span',{className:'net-title'},'Storyline'),
|
|
285
295
|
el('div',{className:'net-stats'},[
|
|
286
296
|
el('span',{className:'net-stat'},[document.createTextNode('Steps: '),el('strong',null,String(r.actions.length))]),
|
|
287
|
-
failCount?el('span',{className:'net-stat has-err'},[document.createTextNode('Failed: '),el('strong',null,String(failCount))]):null
|
|
288
|
-
|
|
297
|
+
failCount?el('span',{className:'net-stat has-err'},[document.createTextNode('Failed: '),el('strong',null,String(failCount))]):null,
|
|
298
|
+
el('span',{className:'net-stat'},[document.createTextNode('Passed: '),el('strong',null,String(passCount))])
|
|
299
|
+
]),
|
|
300
|
+
replayBtn
|
|
289
301
|
]);
|
|
290
|
-
var
|
|
291
|
-
|
|
302
|
+
var slBody=el('div',{className:'storyline'});
|
|
303
|
+
|
|
304
|
+
r.actions.forEach(function(a,idx){
|
|
305
|
+
var stepNum=String(idx+1).padStart(2,'0');
|
|
292
306
|
var label=a.narrative||a.type;
|
|
293
307
|
var durText=a.duration!=null?dur(a.duration):'';
|
|
294
|
-
var
|
|
295
|
-
|
|
296
|
-
|
|
308
|
+
var durPct=a.duration?Math.max(2,Math.round((a.duration/maxDur)*100)):0;
|
|
309
|
+
var stateCls=a.success?'pass':'fail';
|
|
310
|
+
var icon=a.success?'\u2714':'\u2718';
|
|
311
|
+
|
|
312
|
+
// Thumbnail: prefer autoScreenshot, fall back to action's own screenshot
|
|
313
|
+
var thumbPath=a.autoScreenshot||a.screenshot||null;
|
|
314
|
+
var thumb;
|
|
315
|
+
if(thumbPath){
|
|
316
|
+
var src='/api/image?path='+encodeURIComponent(thumbPath);
|
|
317
|
+
var img=document.createElement('img');
|
|
318
|
+
img.src=src;img.alt=label;img.loading='lazy';
|
|
319
|
+
thumb=el('div',{className:'sl-thumb',onclick:function(e){e.stopPropagation();openModal(src)}},[img]);
|
|
320
|
+
}else{
|
|
321
|
+
thumb=el('div',{className:'sl-thumb sl-thumb-empty',title:'No screenshot for this step'},[el('span',null,'\u25A1')]);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Param chips (selector / text / value)
|
|
325
|
+
var chips=el('div',{className:'sl-chips'});
|
|
326
|
+
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)]));
|
|
327
|
+
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))]));
|
|
328
|
+
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))]));
|
|
329
|
+
|
|
330
|
+
var retryBadge=(a.actionRetries&&a.actionRetries>0)?el('span',{className:'sl-retry'},'\u21BB '+a.actionRetries):null;
|
|
331
|
+
|
|
332
|
+
var hashBadge=null;
|
|
333
|
+
if(thumbPath){
|
|
334
|
+
if(hashes[thumbPath]){hashBadge=createHashBadge(hashes[thumbPath])}
|
|
335
|
+
else{(function(holder,fp){ssHash(fp).then(function(h){if(h)holder.appendChild(createHashBadge(h))})})}
|
|
297
336
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
el('span',{className:'
|
|
337
|
+
|
|
338
|
+
var titleRow=el('div',{className:'sl-title'},[
|
|
339
|
+
el('span',{className:'sl-num'},stepNum),
|
|
340
|
+
el('span',{className:'sl-icon '+stateCls},icon),
|
|
341
|
+
el('span',{className:'sl-type'},a.type),
|
|
342
|
+
el('span',{className:'sl-narr'},label),
|
|
301
343
|
retryBadge,
|
|
302
|
-
el('span',{className:'
|
|
303
|
-
])
|
|
304
|
-
});
|
|
305
|
-
actHead.addEventListener('click',function(){actHead.classList.toggle('open')});
|
|
306
|
-
body.appendChild(el('div',{className:'rd-net-panel'},[actHead,actBody]));
|
|
307
|
-
}
|
|
344
|
+
el('span',{className:'sl-dur'},durText)
|
|
345
|
+
]);
|
|
308
346
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
var
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
347
|
+
var durBar=el('div',{className:'sl-bar'},[el('div',{className:'sl-bar-fill '+stateCls,style:'width:'+durPct+'%'})]);
|
|
348
|
+
|
|
349
|
+
var info=el('div',{className:'sl-info'},[
|
|
350
|
+
titleRow,
|
|
351
|
+
chips.children.length?chips:null,
|
|
352
|
+
durBar
|
|
353
|
+
]);
|
|
354
|
+
|
|
355
|
+
var card=el('div',{className:'sl-step '+stateCls},[thumb,info]);
|
|
356
|
+
|
|
357
|
+
if(!a.success&&a.error){
|
|
358
|
+
var errBlock=el('div',{className:'sl-err'},[
|
|
359
|
+
el('span',{className:'sl-err-tag'},'ERROR'),
|
|
360
|
+
el('span',{className:'sl-err-msg'},String(a.error))
|
|
361
|
+
]);
|
|
362
|
+
card.appendChild(errBlock);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
slBody.appendChild(card);
|
|
323
366
|
});
|
|
324
|
-
|
|
367
|
+
|
|
368
|
+
// Final failure context card (uses test-level errorScreenshot if present)
|
|
369
|
+
if(!r.success&&r.errorScreenshot){
|
|
370
|
+
var errSrc='/api/image?path='+encodeURIComponent(r.errorScreenshot);
|
|
371
|
+
var errImg=document.createElement('img');errImg.src=errSrc;errImg.alt='Failure context';errImg.loading='lazy';
|
|
372
|
+
var errCard=el('div',{className:'sl-step sl-final-err'},[
|
|
373
|
+
el('div',{className:'sl-thumb sl-thumb-err',onclick:function(e){e.stopPropagation();openModal(errSrc)}},[errImg]),
|
|
374
|
+
el('div',{className:'sl-info'},[
|
|
375
|
+
el('div',{className:'sl-title'},[
|
|
376
|
+
el('span',{className:'sl-num'},'\u26A0'),
|
|
377
|
+
el('span',{className:'sl-icon fail'},'\u2718'),
|
|
378
|
+
el('span',{className:'sl-narr sl-final-msg'},'Page state at failure')
|
|
379
|
+
]),
|
|
380
|
+
r.error?el('div',{className:'sl-err'},[el('span',{className:'sl-err-tag'},'TEST FAILED'),el('span',{className:'sl-err-msg'},String(r.error))]):null
|
|
381
|
+
])
|
|
382
|
+
]);
|
|
383
|
+
slBody.appendChild(errCard);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
slHead.addEventListener('click',function(){slHead.classList.toggle('open');slBody.classList.toggle('hidden')});
|
|
387
|
+
body.appendChild(el('div',{className:'rd-net-panel rd-storyline-panel'},[slHead,slBody]));
|
|
388
|
+
} else if(r.errorScreenshot){
|
|
389
|
+
// No actions but we have an error screenshot \u2014 show it standalone
|
|
390
|
+
var errSrcOnly='/api/image?path='+encodeURIComponent(r.errorScreenshot);
|
|
391
|
+
var errImgOnly=document.createElement('img');errImgOnly.src=errSrcOnly;errImgOnly.loading='lazy';
|
|
392
|
+
body.appendChild(el('div',{className:'sl-final-err-standalone',onclick:function(e){e.stopPropagation();openModal(errSrcOnly)}},[errImgOnly]));
|
|
325
393
|
}
|
|
326
394
|
|
|
327
395
|
// Console logs
|
|
@@ -401,11 +469,63 @@ function refreshScreenshots(){
|
|
|
401
469
|
var img=document.createElement('img');img.src=src;img.alt=f.name;img.loading='lazy';
|
|
402
470
|
var capEl=el('div',{className:'cap'},[el('span',{className:'cap-name'},f.name)]);
|
|
403
471
|
(function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,f.path);
|
|
404
|
-
gal.appendChild(el('div',{className:'gallery-item',onclick:function(){openModal(src)}},[img,capEl]));
|
|
472
|
+
gal.appendChild(el('div',{className:'gallery-item','data-path':f.path,onclick:function(){openModal(src)}},[img,capEl]));
|
|
405
473
|
});
|
|
474
|
+
resetBlankBar();
|
|
406
475
|
}).catch(function(){});
|
|
407
476
|
}
|
|
408
477
|
|
|
478
|
+
/* ── Blank screenshot scan / delete ── */
|
|
479
|
+
function resetBlankBar(){
|
|
480
|
+
var bar=$('#ssBlankBar');if(bar)bar.hidden=true;
|
|
481
|
+
$$('#screenshotGallery .gallery-item.blank-flagged').forEach(function(it){it.classList.remove('blank-flagged')});
|
|
482
|
+
S.blankPaths=null;
|
|
483
|
+
}
|
|
484
|
+
function scanBlankScreenshots(){
|
|
485
|
+
if(!S.project){showToast('Select a project first','error');return}
|
|
486
|
+
var btn=$('#ssScanBlankBtn');btn.disabled=true;var prev=btn.textContent;btn.textContent='Scanning…';
|
|
487
|
+
api('/api/db/projects/'+S.project+'/screenshots/blank-scan').then(function(data){
|
|
488
|
+
btn.disabled=false;btn.textContent=prev;
|
|
489
|
+
var blanks=(data&&data.blanks)||[];
|
|
490
|
+
$$('#screenshotGallery .gallery-item.blank-flagged').forEach(function(it){it.classList.remove('blank-flagged')});
|
|
491
|
+
if(!blanks.length){
|
|
492
|
+
S.blankPaths=null;$('#ssBlankBar').hidden=true;
|
|
493
|
+
showToast('No blank images found ('+((data&&data.scanned)||0)+' scanned)','info');
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
S.blankPaths=blanks.map(function(b){return b.path});
|
|
497
|
+
var found=0;
|
|
498
|
+
S.blankPaths.forEach(function(p){
|
|
499
|
+
var item=$('#screenshotGallery .gallery-item[data-path="'+(window.CSS&&CSS.escape?CSS.escape(p):p)+'"]');
|
|
500
|
+
if(item){item.classList.add('blank-flagged');found++}
|
|
501
|
+
});
|
|
502
|
+
$('#ssBlankMsg').textContent=blanks.length+' blank image'+(blanks.length===1?'':'s')+' of '+data.scanned+' scanned';
|
|
503
|
+
$('#ssBlankBar').hidden=false;
|
|
504
|
+
}).catch(function(){
|
|
505
|
+
btn.disabled=false;btn.textContent=prev;
|
|
506
|
+
showToast('Blank scan failed','error');
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
function deleteBlankScreenshots(){
|
|
510
|
+
if(!S.blankPaths||!S.blankPaths.length)return;
|
|
511
|
+
var btn=$('#ssBlankDeleteBtn');btn.disabled=true;var prev=btn.textContent;btn.textContent='Deleting…';
|
|
512
|
+
fetch('/api/screenshots/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({paths:S.blankPaths})})
|
|
513
|
+
.then(function(r){return r.json()}).then(function(res){
|
|
514
|
+
btn.disabled=false;btn.textContent=prev;
|
|
515
|
+
if(res&&res.error){showToast('Delete failed: '+res.error,'error');return}
|
|
516
|
+
var n=res.deleted||0,failed=(res.failed&&res.failed.length)||0;
|
|
517
|
+
showToast('Deleted '+n+' blank image'+(n===1?'':'s')+(failed?(' · '+failed+' failed'):''),failed?'error':'success');
|
|
518
|
+
resetBlankBar();
|
|
519
|
+
refreshScreenshots();
|
|
520
|
+
}).catch(function(){
|
|
521
|
+
btn.disabled=false;btn.textContent=prev;
|
|
522
|
+
showToast('Delete failed','error');
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
$('#ssScanBlankBtn').addEventListener('click',scanBlankScreenshots);
|
|
526
|
+
$('#ssBlankDeleteBtn').addEventListener('click',deleteBlankScreenshots);
|
|
527
|
+
$('#ssBlankCancelBtn').addEventListener('click',resetBlankBar);
|
|
528
|
+
|
|
409
529
|
function searchByHash(){
|
|
410
530
|
var container=$('#ssSearchResult');
|
|
411
531
|
container.textContent='';
|
|
@@ -674,3 +794,225 @@ $('#btnExportLearnings').addEventListener('click',function(){
|
|
|
674
794
|
/* ── Modal ── */
|
|
675
795
|
function openModal(src){$('#modalImg').src=src;$('#modal').classList.add('open')}
|
|
676
796
|
$('#modal').addEventListener('click',function(){$('#modal').classList.remove('open')});
|
|
797
|
+
|
|
798
|
+
/* ══════════════════════════════════════════════════════════════════
|
|
799
|
+
Screenshot Replay Player — plays a run's per-step screenshots
|
|
800
|
+
as a video. Pulls frames from r.actions[].autoScreenshot|screenshot.
|
|
801
|
+
══════════════════════════════════════════════════════════════════ */
|
|
802
|
+
var REPLAY={frames:[],idx:0,playing:false,speed:1,timer:null,frameMs:1000};
|
|
803
|
+
|
|
804
|
+
function buildReplayFrames(actions,run){
|
|
805
|
+
var frames=[];
|
|
806
|
+
(actions||[]).forEach(function(a,i){
|
|
807
|
+
var path=a.autoScreenshot||a.screenshot||null;
|
|
808
|
+
frames.push({
|
|
809
|
+
idx:i,
|
|
810
|
+
path:path,
|
|
811
|
+
src:path?'/api/image?path='+encodeURIComponent(path):null,
|
|
812
|
+
type:a.type||'',
|
|
813
|
+
narr:a.narrative||a.type||'',
|
|
814
|
+
duration:a.duration||0,
|
|
815
|
+
success:!!a.success,
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
if(run&&!run.success&&run.errorScreenshot){
|
|
819
|
+
frames.push({
|
|
820
|
+
idx:frames.length,
|
|
821
|
+
path:run.errorScreenshot,
|
|
822
|
+
src:'/api/image?path='+encodeURIComponent(run.errorScreenshot),
|
|
823
|
+
type:'failure',
|
|
824
|
+
narr:'Page state at failure',
|
|
825
|
+
duration:0,
|
|
826
|
+
success:false,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
return frames;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function openReplay(actions,run){
|
|
833
|
+
REPLAY.frames=buildReplayFrames(actions,run);
|
|
834
|
+
if(!REPLAY.frames.length){showToast&&showToast('No screenshots to replay','warn');return}
|
|
835
|
+
REPLAY.idx=0;REPLAY.playing=false;
|
|
836
|
+
$('#replayModal').classList.add('open');
|
|
837
|
+
$('#replayModal').setAttribute('aria-hidden','false');
|
|
838
|
+
renderReplayFrame();
|
|
839
|
+
// Auto-start playback for the "video" feel
|
|
840
|
+
toggleReplayPlay(true);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function closeReplay(){
|
|
844
|
+
stopReplayTimer();
|
|
845
|
+
$('#replayModal').classList.remove('open');
|
|
846
|
+
$('#replayModal').setAttribute('aria-hidden','true');
|
|
847
|
+
// Free image src to avoid lingering downloads
|
|
848
|
+
$('#replayImg').src='';
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function renderReplayFrame(){
|
|
852
|
+
var f=REPLAY.frames[REPLAY.idx];if(!f)return;
|
|
853
|
+
var modal=$('#replayModal');
|
|
854
|
+
var img=$('#replayImg');
|
|
855
|
+
if(f.src){
|
|
856
|
+
modal.classList.remove('empty');
|
|
857
|
+
if(img.src!==location.origin+f.src&&img.src!==f.src)img.src=f.src;
|
|
858
|
+
}else{
|
|
859
|
+
modal.classList.add('empty');
|
|
860
|
+
img.src='';
|
|
861
|
+
}
|
|
862
|
+
$('#replayStepNum').textContent=(REPLAY.idx+1)+' / '+REPLAY.frames.length;
|
|
863
|
+
$('#replayStepType').textContent=f.type;
|
|
864
|
+
$('#replayStepNarr').textContent=f.narr;
|
|
865
|
+
var pct=REPLAY.frames.length>1?(REPLAY.idx/(REPLAY.frames.length-1))*100:100;
|
|
866
|
+
$('#replayProgressFill').style.width=pct+'%';
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function scheduleNextReplayFrame(){
|
|
870
|
+
stopReplayTimer();
|
|
871
|
+
if(!REPLAY.playing)return;
|
|
872
|
+
if(REPLAY.idx>=REPLAY.frames.length-1){toggleReplayPlay(false);return}
|
|
873
|
+
// Uniform pacing: 1 frame per second at 1x, scaled by speed.
|
|
874
|
+
var ms=REPLAY.frameMs/REPLAY.speed;
|
|
875
|
+
REPLAY.timer=setTimeout(function(){
|
|
876
|
+
REPLAY.idx++;
|
|
877
|
+
renderReplayFrame();
|
|
878
|
+
scheduleNextReplayFrame();
|
|
879
|
+
},ms);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function stopReplayTimer(){if(REPLAY.timer){clearTimeout(REPLAY.timer);REPLAY.timer=null}}
|
|
883
|
+
|
|
884
|
+
function toggleReplayPlay(forceState){
|
|
885
|
+
REPLAY.playing=typeof forceState==='boolean'?forceState:!REPLAY.playing;
|
|
886
|
+
// If we're at the last frame and user hits play, restart from 0
|
|
887
|
+
if(REPLAY.playing&&REPLAY.idx>=REPLAY.frames.length-1){REPLAY.idx=0;renderReplayFrame()}
|
|
888
|
+
var btn=$('#replayPlay');if(btn)btn.innerHTML=REPLAY.playing?'❙❙':'▶';
|
|
889
|
+
if(REPLAY.playing)scheduleNextReplayFrame();else stopReplayTimer();
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function stepReplay(delta){
|
|
893
|
+
stopReplayTimer();
|
|
894
|
+
REPLAY.idx=Math.max(0,Math.min(REPLAY.frames.length-1,REPLAY.idx+delta));
|
|
895
|
+
renderReplayFrame();
|
|
896
|
+
if(REPLAY.playing)scheduleNextReplayFrame();
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function setReplaySpeed(s){
|
|
900
|
+
REPLAY.speed=s;
|
|
901
|
+
document.querySelectorAll('.replay-speed-btn').forEach(function(b){
|
|
902
|
+
b.classList.toggle('active',parseFloat(b.dataset.speed)===s);
|
|
903
|
+
});
|
|
904
|
+
if(REPLAY.playing){stopReplayTimer();scheduleNextReplayFrame()}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Wire up controls
|
|
908
|
+
$('#replayPlay').addEventListener('click',function(){toggleReplayPlay()});
|
|
909
|
+
$('#replayPrev').addEventListener('click',function(){stepReplay(-1)});
|
|
910
|
+
$('#replayNext').addEventListener('click',function(){stepReplay(1)});
|
|
911
|
+
$('#replayClose').addEventListener('click',closeReplay);
|
|
912
|
+
document.querySelectorAll('.replay-speed-btn').forEach(function(b){
|
|
913
|
+
b.addEventListener('click',function(){setReplaySpeed(parseFloat(b.dataset.speed))});
|
|
914
|
+
});
|
|
915
|
+
document.addEventListener('keydown',function(e){
|
|
916
|
+
if(!$('#replayModal').classList.contains('open'))return;
|
|
917
|
+
if(e.key==='Escape'){closeReplay()}
|
|
918
|
+
else if(e.key===' '){e.preventDefault();toggleReplayPlay()}
|
|
919
|
+
else if(e.key==='ArrowLeft'){stepReplay(-1)}
|
|
920
|
+
else if(e.key==='ArrowRight'){stepReplay(1)}
|
|
921
|
+
});
|
|
922
|
+
// Click on stage advances; click outside the image closes
|
|
923
|
+
$('#replayModal').addEventListener('click',function(e){
|
|
924
|
+
if(e.target&&e.target.id==='replayModal')closeReplay();
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// Expose to the renderer below so the storyline header can wire its button
|
|
928
|
+
window.openReplay=openReplay;
|
|
929
|
+
|
|
930
|
+
/* ══════════════════════════════════════════════════════════════════
|
|
931
|
+
Network tab — cross-run network query (Investigate › Network)
|
|
932
|
+
══════════════════════════════════════════════════════════════════ */
|
|
933
|
+
function refreshNetwork(){
|
|
934
|
+
var box=$('#netResults');var empty=$('#networkEmpty');
|
|
935
|
+
if(!box)return;
|
|
936
|
+
box.textContent='';
|
|
937
|
+
box.appendChild(el('div',{style:'padding:20px;text-align:center;color:var(--text3);font-size:11px'},'Loading...'));
|
|
938
|
+
var runsUrl=S.project?'/api/db/projects/'+S.project+'/runs':'/api/db/runs';
|
|
939
|
+
api(runsUrl).then(function(runs){
|
|
940
|
+
if(!Array.isArray(runs)||runs.length===0){
|
|
941
|
+
box.textContent='';if(empty)empty.style.display='block';return;
|
|
942
|
+
}
|
|
943
|
+
if(empty)empty.style.display='none';
|
|
944
|
+
var top=runs.slice(0,30);
|
|
945
|
+
var promises=top.map(function(r){
|
|
946
|
+
return api('/api/db/runs/'+r.id+'/network-logs').catch(function(){return []}).then(function(logs){
|
|
947
|
+
return {run:r,logs:Array.isArray(logs)?logs:[]};
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
Promise.all(promises).then(function(results){renderNetworkResults(box,results)});
|
|
951
|
+
}).catch(function(){box.textContent='';if(empty)empty.style.display='block'});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function renderNetworkResults(box,results){
|
|
955
|
+
var statusFilter=$('#netStatusFilter')?$('#netStatusFilter').value:'errors';
|
|
956
|
+
var urlFilter=($('#netUrlFilter')?$('#netUrlFilter').value:'').toLowerCase().trim();
|
|
957
|
+
var rows=[];
|
|
958
|
+
results.forEach(function(r){
|
|
959
|
+
r.logs.forEach(function(n){
|
|
960
|
+
var s=n.status||0;
|
|
961
|
+
var keep=true;
|
|
962
|
+
if(statusFilter==='errors')keep=s>=400||s===0;
|
|
963
|
+
else if(statusFilter==='slow')keep=(n.duration||0)>=1000;
|
|
964
|
+
if(keep&&urlFilter&&(n.url||'').toLowerCase().indexOf(urlFilter)<0)keep=false;
|
|
965
|
+
if(keep)rows.push({run:r.run,n:n});
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
box.textContent='';
|
|
969
|
+
if(rows.length===0){
|
|
970
|
+
box.appendChild(el('div',{style:'padding:30px;text-align:center;color:var(--text3);font-size:11px'},'No matching network records.'));
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
rows.sort(function(a,b){return (b.n.duration||0)-(a.n.duration||0)});
|
|
974
|
+
var head=el('div',{className:'net-row net-head'},[
|
|
975
|
+
el('span',{className:'net-col-run'},'Run'),
|
|
976
|
+
el('span',{className:'net-col-method'},'Method'),
|
|
977
|
+
el('span',{className:'net-col-status'},'Status'),
|
|
978
|
+
el('span',{className:'net-col-url'},'URL'),
|
|
979
|
+
el('span',{className:'net-col-dur'},'Duration')
|
|
980
|
+
]);
|
|
981
|
+
box.appendChild(head);
|
|
982
|
+
rows.slice(0,200).forEach(function(rr){
|
|
983
|
+
var n=rr.n;var s=n.status||0;
|
|
984
|
+
var sCls=s===0?'s5xx':s<300?'s2xx':s<400?'s3xx':s<500?'s4xx':'s5xx';
|
|
985
|
+
var mCls=(n.method||'GET').toLowerCase();
|
|
986
|
+
var row=el('div',{className:'net-row clickable',onclick:function(){
|
|
987
|
+
showView('investigate');
|
|
988
|
+
var btn=document.querySelector('.tab-btn[data-tab="runsTabHistory"]');if(btn)btn.click();
|
|
989
|
+
}},[
|
|
990
|
+
el('span',{className:'net-col-run'},'#'+rr.run.id),
|
|
991
|
+
el('span',{className:'net-col-method m-'+mCls},n.method||'GET'),
|
|
992
|
+
el('span',{className:'net-col-status st-'+sCls},String(s||'ERR')),
|
|
993
|
+
el('span',{className:'net-col-url',title:n.url||''},n.url||''),
|
|
994
|
+
el('span',{className:'net-col-dur'},dur(n.duration||0))
|
|
995
|
+
]);
|
|
996
|
+
box.appendChild(row);
|
|
997
|
+
});
|
|
998
|
+
if(rows.length>200){
|
|
999
|
+
box.appendChild(el('div',{style:'padding:10px;text-align:center;color:var(--text3);font-size:11px'},'Showing 200 of '+rows.length+' results'));
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
(function(){
|
|
1004
|
+
var btn=$('#btnRefreshNetwork');if(btn)btn.addEventListener('click',refreshNetwork);
|
|
1005
|
+
var sel=$('#netStatusFilter');if(sel)sel.addEventListener('change',refreshNetwork);
|
|
1006
|
+
var inp=$('#netUrlFilter');
|
|
1007
|
+
if(inp){
|
|
1008
|
+
var deb;
|
|
1009
|
+
inp.addEventListener('input',function(){clearTimeout(deb);deb=setTimeout(refreshNetwork,200)});
|
|
1010
|
+
}
|
|
1011
|
+
// Lazy: only fetch first time the Network tab is clicked
|
|
1012
|
+
var netTabBtn=document.querySelector('.tab-btn[data-tab="investigateTabNetwork"]');
|
|
1013
|
+
if(netTabBtn){
|
|
1014
|
+
netTabBtn.addEventListener('click',function(){
|
|
1015
|
+
if(!netTabBtn.dataset.loaded){netTabBtn.dataset.loaded='1';refreshNetwork()}
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
})();
|