@matware/e2e-runner 1.3.1 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +4 -4
- package/.claude-plugin/plugin.json +2 -2
- package/LICENSE +1 -1
- package/README.md +491 -225
- package/agents/test-creator.md +4 -2
- package/agents/test-improver.md +7 -4
- package/bin/cli.js +93 -19
- package/package.json +4 -3
- package/skills/e2e-testing/SKILL.md +5 -3
- package/skills/e2e-testing/references/action-types.md +35 -18
- package/skills/e2e-testing/references/test-json-format.md +23 -0
- package/skills/e2e-testing/references/troubleshooting.md +2 -26
- package/src/actions.js +181 -15
- package/src/config.js +6 -0
- package/src/dashboard.js +185 -9
- package/src/db.js +26 -0
- package/src/mcp-tools.js +238 -69
- package/src/module-analysis.js +247 -0
- package/src/module-resolver.js +35 -2
- package/src/narrate.js +33 -1
- package/src/pool-manager.js +46 -1
- package/src/pool.js +177 -20
- package/src/runner.js +144 -19
- package/src/visual-diff.js +74 -4
- package/src/websocket.js +14 -3
- package/src/wizard.js +184 -0
- package/templates/build-dashboard.js +3 -0
- package/templates/dashboard/js/api.js +60 -3
- package/templates/dashboard/js/init.js +46 -0
- package/templates/dashboard/js/keyboard.js +8 -7
- package/templates/dashboard/js/quicksearch.js +277 -0
- package/templates/dashboard/js/state.js +61 -7
- package/templates/dashboard/js/toast.js +1 -1
- package/templates/dashboard/js/utils.js +23 -2
- package/templates/dashboard/js/view-live.js +235 -42
- package/templates/dashboard/js/view-runs.js +469 -42
- package/templates/dashboard/js/view-tests.js +157 -16
- package/templates/dashboard/js/view-tools.js +234 -0
- package/templates/dashboard/js/view-watch.js +2 -2
- package/templates/dashboard/js/websocket.js +33 -3
- package/templates/dashboard/styles/base.css +489 -53
- package/templates/dashboard/styles/components.css +736 -84
- package/templates/dashboard/styles/view-live.css +459 -78
- package/templates/dashboard/styles/view-runs.css +826 -177
- package/templates/dashboard/styles/view-tests.css +440 -77
- package/templates/dashboard/styles/view-tools.css +206 -0
- package/templates/dashboard/styles/view-watch.css +198 -41
- package/templates/dashboard/template.html +356 -58
- package/templates/dashboard.html +5354 -722
- package/templates/docker-compose-lightpanda.yml +7 -0
|
@@ -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,132 @@ 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
|
+
// Data-capture steps (raw JSON saved instead of a screenshot) get a {} thumb.
|
|
314
|
+
var thumbPath=a.autoScreenshot||a.screenshot||null;
|
|
315
|
+
var thumb;
|
|
316
|
+
if(thumbPath){
|
|
317
|
+
var src='/api/image?path='+encodeURIComponent(thumbPath);
|
|
318
|
+
var img=document.createElement('img');
|
|
319
|
+
img.src=src;img.alt=label;img.loading='lazy';
|
|
320
|
+
thumb=el('div',{className:'sl-thumb',onclick:function(e){e.stopPropagation();openModal(src)}},[img]);
|
|
321
|
+
}else if(a.dataCapture){
|
|
322
|
+
(function(dc){
|
|
323
|
+
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}')]);
|
|
324
|
+
})(a.dataCapture);
|
|
325
|
+
}else{
|
|
326
|
+
thumb=el('div',{className:'sl-thumb sl-thumb-empty',title:'No screenshot for this step'},[el('span',null,'\u25A1')]);
|
|
297
327
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
328
|
+
|
|
329
|
+
// Param chips (selector / text / value)
|
|
330
|
+
var chips=el('div',{className:'sl-chips'});
|
|
331
|
+
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)]));
|
|
332
|
+
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))]));
|
|
333
|
+
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))]));
|
|
334
|
+
if(a.dataCapture)(function(dc){
|
|
335
|
+
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)}},[
|
|
336
|
+
el('span',{className:'sl-chip-k'},'JSON'),
|
|
337
|
+
el('span',{className:'sl-chip-v'},dc.split('/').pop())
|
|
338
|
+
]));
|
|
339
|
+
})(a.dataCapture);
|
|
340
|
+
|
|
341
|
+
var retryBadge=(a.actionRetries&&a.actionRetries>0)?el('span',{className:'sl-retry'},'\u21BB '+a.actionRetries):null;
|
|
342
|
+
|
|
343
|
+
var hashBadge=null;
|
|
344
|
+
if(thumbPath){
|
|
345
|
+
if(hashes[thumbPath]){hashBadge=createHashBadge(hashes[thumbPath])}
|
|
346
|
+
else{(function(holder,fp){ssHash(fp).then(function(h){if(h)holder.appendChild(createHashBadge(h))})})}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
var titleRow=el('div',{className:'sl-title'},[
|
|
350
|
+
el('span',{className:'sl-num'},stepNum),
|
|
351
|
+
el('span',{className:'sl-icon '+stateCls},icon),
|
|
352
|
+
el('span',{className:'sl-type'},a.type),
|
|
353
|
+
el('span',{className:'sl-narr'},label),
|
|
301
354
|
retryBadge,
|
|
302
|
-
el('span',{className:'
|
|
303
|
-
])
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
355
|
+
el('span',{className:'sl-dur'},durText)
|
|
356
|
+
]);
|
|
357
|
+
|
|
358
|
+
var durBar=el('div',{className:'sl-bar'},[el('div',{className:'sl-bar-fill '+stateCls,style:'width:'+durPct+'%'})]);
|
|
359
|
+
|
|
360
|
+
var info=el('div',{className:'sl-info'},[
|
|
361
|
+
titleRow,
|
|
362
|
+
chips.children.length?chips:null,
|
|
363
|
+
durBar
|
|
364
|
+
]);
|
|
365
|
+
|
|
366
|
+
var card=el('div',{className:'sl-step '+stateCls},[thumb,info]);
|
|
367
|
+
|
|
368
|
+
if(!a.success&&a.error){
|
|
369
|
+
var errBlock=el('div',{className:'sl-err'},[
|
|
370
|
+
el('span',{className:'sl-err-tag'},'ERROR'),
|
|
371
|
+
el('span',{className:'sl-err-msg'},String(a.error))
|
|
372
|
+
]);
|
|
373
|
+
card.appendChild(errBlock);
|
|
374
|
+
}
|
|
308
375
|
|
|
309
|
-
|
|
310
|
-
var shots=[];
|
|
311
|
-
var hashes=r.screenshotHashes||{};
|
|
312
|
-
(r.screenshots||[]).forEach(function(p){shots.push({path:p,label:p.split('/').pop(),type:'screenshot',hash:hashes[p]||null})});
|
|
313
|
-
if(r.errorScreenshot){shots.push({path:r.errorScreenshot,label:r.errorScreenshot.split('/').pop(),type:'error',hash:hashes[r.errorScreenshot]||null})}
|
|
314
|
-
if(shots.length){
|
|
315
|
-
var shotsWrap=el('div',{className:'rd-shots'});
|
|
316
|
-
shots.forEach(function(s){
|
|
317
|
-
var src='/api/image?path='+encodeURIComponent(s.path);
|
|
318
|
-
var img=document.createElement('img');img.src=src;img.alt=s.label;img.loading='lazy';
|
|
319
|
-
var capEl=el('div',{className:'rd-shot-cap'},[el('span',{className:'cap-name'},s.label)]);
|
|
320
|
-
if(s.hash){capEl.appendChild(createHashBadge(s.hash))}
|
|
321
|
-
else{(function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,s.path)}
|
|
322
|
-
shotsWrap.appendChild(el('div',{className:'rd-shot'+(s.type==='error'?' err-shot':''),onclick:function(e){e.stopPropagation();openModal(src)}},[img,capEl]));
|
|
376
|
+
slBody.appendChild(card);
|
|
323
377
|
});
|
|
324
|
-
|
|
378
|
+
|
|
379
|
+
// Final failure context card (uses test-level errorScreenshot if present)
|
|
380
|
+
if(!r.success&&r.errorScreenshot){
|
|
381
|
+
var errSrc='/api/image?path='+encodeURIComponent(r.errorScreenshot);
|
|
382
|
+
var errImg=document.createElement('img');errImg.src=errSrc;errImg.alt='Failure context';errImg.loading='lazy';
|
|
383
|
+
var errCard=el('div',{className:'sl-step sl-final-err'},[
|
|
384
|
+
el('div',{className:'sl-thumb sl-thumb-err',onclick:function(e){e.stopPropagation();openModal(errSrc)}},[errImg]),
|
|
385
|
+
el('div',{className:'sl-info'},[
|
|
386
|
+
el('div',{className:'sl-title'},[
|
|
387
|
+
el('span',{className:'sl-num'},'\u26A0'),
|
|
388
|
+
el('span',{className:'sl-icon fail'},'\u2718'),
|
|
389
|
+
el('span',{className:'sl-narr sl-final-msg'},'Page state at failure')
|
|
390
|
+
]),
|
|
391
|
+
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
|
|
392
|
+
])
|
|
393
|
+
]);
|
|
394
|
+
slBody.appendChild(errCard);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
slHead.addEventListener('click',function(){slHead.classList.toggle('open');slBody.classList.toggle('hidden')});
|
|
398
|
+
body.appendChild(el('div',{className:'rd-net-panel rd-storyline-panel'},[slHead,slBody]));
|
|
399
|
+
} else if(r.errorScreenshot){
|
|
400
|
+
// No actions but we have an error screenshot \u2014 show it standalone
|
|
401
|
+
var errSrcOnly='/api/image?path='+encodeURIComponent(r.errorScreenshot);
|
|
402
|
+
var errImgOnly=document.createElement('img');errImgOnly.src=errSrcOnly;errImgOnly.loading='lazy';
|
|
403
|
+
body.appendChild(el('div',{className:'sl-final-err-standalone',onclick:function(e){e.stopPropagation();openModal(errSrcOnly)}},[errImgOnly]));
|
|
325
404
|
}
|
|
326
405
|
|
|
327
406
|
// Console logs
|
|
@@ -388,6 +467,15 @@ function loadDetailInline(id,detailTr){
|
|
|
388
467
|
}
|
|
389
468
|
|
|
390
469
|
/* ── Screenshots ── */
|
|
470
|
+
/* Session memory: which test groups the user expanded (survives refreshes, not reloads) */
|
|
471
|
+
var SS_EXPANDED={};
|
|
472
|
+
function buildGalleryItem(f){
|
|
473
|
+
var src='/api/image?path='+encodeURIComponent(f.path);
|
|
474
|
+
var img=document.createElement('img');img.src=src;img.alt=f.name;img.loading='lazy';
|
|
475
|
+
var capEl=el('div',{className:'cap'},[el('span',{className:'cap-name'},f.name)]);
|
|
476
|
+
(function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,f.path);
|
|
477
|
+
return el('div',{className:'gallery-item','data-path':f.path,onclick:function(){openModal(src)}},[img,capEl]);
|
|
478
|
+
}
|
|
391
479
|
function refreshScreenshots(){
|
|
392
480
|
var gal=$('#screenshotGallery'),empty=$('#screenshotsEmpty');
|
|
393
481
|
gal.textContent='';
|
|
@@ -396,16 +484,115 @@ function refreshScreenshots(){
|
|
|
396
484
|
if(!Array.isArray(files)||!files.length){empty.style.display='block';empty.querySelector('p').textContent='No screenshots for this project.';$('#badgeScreenshots').textContent='0';return}
|
|
397
485
|
empty.style.display='none';
|
|
398
486
|
$('#badgeScreenshots').textContent=files.length;
|
|
487
|
+
/* Group by test name; files without one go to a trailing "Other" bucket */
|
|
488
|
+
var groups={},order=[];
|
|
399
489
|
files.forEach(function(f){
|
|
400
|
-
var
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
490
|
+
var key=f.testName||'__other__';
|
|
491
|
+
if(!groups[key]){groups[key]=[];order.push(key)}
|
|
492
|
+
groups[key].push(f);
|
|
493
|
+
});
|
|
494
|
+
function fileTs(f){var m=f.name.match(/(\d{10,})\.[a-z]+$/i);return m?parseInt(m[1],10):0}
|
|
495
|
+
order.sort(function(a,b){
|
|
496
|
+
if(a==='__other__')return 1;
|
|
497
|
+
if(b==='__other__')return -1;
|
|
498
|
+
var ma=0,mb=0;
|
|
499
|
+
groups[a].forEach(function(f){var t=fileTs(f);if(t>ma)ma=t});
|
|
500
|
+
groups[b].forEach(function(f){var t=fileTs(f);if(t>mb)mb=t});
|
|
501
|
+
return mb-ma;
|
|
502
|
+
});
|
|
503
|
+
order.forEach(function(key){
|
|
504
|
+
var items=groups[key],label=key==='__other__'?'Other':key;
|
|
505
|
+
var grid=el('div',{className:'gallery'});
|
|
506
|
+
var materialized=false;
|
|
507
|
+
function materialize(){
|
|
508
|
+
if(materialized)return;materialized=true;
|
|
509
|
+
items.forEach(function(f){grid.appendChild(buildGalleryItem(f))});
|
|
510
|
+
}
|
|
511
|
+
var expanded=!!SS_EXPANDED[key];
|
|
512
|
+
var body=el('div',{className:'gallery-group-body'},[grid]);
|
|
513
|
+
var group=el('div',{className:'gallery-group'+(expanded?' open':'')},[]);
|
|
514
|
+
var header=el('div',{className:'gallery-group-header',onclick:function(){
|
|
515
|
+
expanded=!expanded;
|
|
516
|
+
if(expanded){SS_EXPANDED[key]=true;materialize()}else{delete SS_EXPANDED[key]}
|
|
517
|
+
group.classList.toggle('open',expanded);
|
|
518
|
+
if(expanded)body.removeAttribute('hidden');else body.setAttribute('hidden','');
|
|
519
|
+
}},[
|
|
520
|
+
el('span',{className:'gg-arrow'},'▶'),
|
|
521
|
+
el('span',{className:'gg-name'},label),
|
|
522
|
+
el('span',{className:'gg-count'},items.length+' shot'+(items.length===1?'':'s')),
|
|
523
|
+
el('span',{className:'gg-blank-badge',hidden:''},''),
|
|
524
|
+
]);
|
|
525
|
+
group.appendChild(header);
|
|
526
|
+
group.appendChild(body);
|
|
527
|
+
if(expanded){materialize()}else{body.setAttribute('hidden','')}
|
|
528
|
+
/* Expose to the blank-scan so it can materialize collapsed groups on demand */
|
|
529
|
+
group._ssItems=items;group._ssMaterialize=materialize;
|
|
530
|
+
gal.appendChild(group);
|
|
405
531
|
});
|
|
532
|
+
resetBlankBar();
|
|
406
533
|
}).catch(function(){});
|
|
407
534
|
}
|
|
408
535
|
|
|
536
|
+
/* ── Blank screenshot scan / delete ── */
|
|
537
|
+
function resetBlankBar(){
|
|
538
|
+
var bar=$('#ssBlankBar');if(bar)bar.hidden=true;
|
|
539
|
+
$$('#screenshotGallery .gallery-item.blank-flagged').forEach(function(it){it.classList.remove('blank-flagged')});
|
|
540
|
+
$$('#screenshotGallery .gg-blank-badge').forEach(function(b){b.textContent='';b.setAttribute('hidden','')});
|
|
541
|
+
S.blankPaths=null;
|
|
542
|
+
}
|
|
543
|
+
function scanBlankScreenshots(){
|
|
544
|
+
if(!S.project){showToast('Select a project first','error');return}
|
|
545
|
+
var btn=$('#ssScanBlankBtn');btn.disabled=true;var prev=btn.textContent;btn.textContent='Scanning…';
|
|
546
|
+
api('/api/db/projects/'+S.project+'/screenshots/blank-scan').then(function(data){
|
|
547
|
+
btn.disabled=false;btn.textContent=prev;
|
|
548
|
+
var blanks=(data&&data.blanks)||[];
|
|
549
|
+
$$('#screenshotGallery .gallery-item.blank-flagged').forEach(function(it){it.classList.remove('blank-flagged')});
|
|
550
|
+
if(!blanks.length){
|
|
551
|
+
S.blankPaths=null;$('#ssBlankBar').hidden=true;
|
|
552
|
+
showToast('No blank images found ('+((data&&data.scanned)||0)+' scanned)','info');
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
S.blankPaths=blanks.map(function(b){return b.path});
|
|
556
|
+
/* Materialize collapsed groups that contain blanks, flag items, badge the headers */
|
|
557
|
+
var blankSet={};S.blankPaths.forEach(function(p){blankSet[p]=true});
|
|
558
|
+
$$('#screenshotGallery .gallery-group').forEach(function(g){
|
|
559
|
+
var inGroup=(g._ssItems||[]).filter(function(it){return blankSet[it.path]}).length;
|
|
560
|
+
if(!inGroup)return;
|
|
561
|
+
if(g._ssMaterialize)g._ssMaterialize();
|
|
562
|
+
var badge=g.querySelector('.gg-blank-badge');
|
|
563
|
+
if(badge){badge.textContent=inGroup+' blank'+(inGroup===1?'':'s');badge.removeAttribute('hidden')}
|
|
564
|
+
});
|
|
565
|
+
S.blankPaths.forEach(function(p){
|
|
566
|
+
var item=$('#screenshotGallery .gallery-item[data-path="'+(window.CSS&&CSS.escape?CSS.escape(p):p)+'"]');
|
|
567
|
+
if(item)item.classList.add('blank-flagged');
|
|
568
|
+
});
|
|
569
|
+
$('#ssBlankMsg').textContent=blanks.length+' blank image'+(blanks.length===1?'':'s')+' of '+data.scanned+' scanned';
|
|
570
|
+
$('#ssBlankBar').hidden=false;
|
|
571
|
+
}).catch(function(){
|
|
572
|
+
btn.disabled=false;btn.textContent=prev;
|
|
573
|
+
showToast('Blank scan failed','error');
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
function deleteBlankScreenshots(){
|
|
577
|
+
if(!S.blankPaths||!S.blankPaths.length)return;
|
|
578
|
+
var btn=$('#ssBlankDeleteBtn');btn.disabled=true;var prev=btn.textContent;btn.textContent='Deleting…';
|
|
579
|
+
fetch('/api/screenshots/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({paths:S.blankPaths})})
|
|
580
|
+
.then(function(r){return r.json()}).then(function(res){
|
|
581
|
+
btn.disabled=false;btn.textContent=prev;
|
|
582
|
+
if(res&&res.error){showToast('Delete failed: '+res.error,'error');return}
|
|
583
|
+
var n=res.deleted||0,failed=(res.failed&&res.failed.length)||0;
|
|
584
|
+
showToast('Deleted '+n+' blank image'+(n===1?'':'s')+(failed?(' · '+failed+' failed'):''),failed?'error':'success');
|
|
585
|
+
resetBlankBar();
|
|
586
|
+
refreshScreenshots();
|
|
587
|
+
}).catch(function(){
|
|
588
|
+
btn.disabled=false;btn.textContent=prev;
|
|
589
|
+
showToast('Delete failed','error');
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
$('#ssScanBlankBtn').addEventListener('click',scanBlankScreenshots);
|
|
593
|
+
$('#ssBlankDeleteBtn').addEventListener('click',deleteBlankScreenshots);
|
|
594
|
+
$('#ssBlankCancelBtn').addEventListener('click',resetBlankBar);
|
|
595
|
+
|
|
409
596
|
function searchByHash(){
|
|
410
597
|
var container=$('#ssSearchResult');
|
|
411
598
|
container.textContent='';
|
|
@@ -672,5 +859,245 @@ $('#btnExportLearnings').addEventListener('click',function(){
|
|
|
672
859
|
});
|
|
673
860
|
|
|
674
861
|
/* ── Modal ── */
|
|
675
|
-
function openModal(src){
|
|
862
|
+
function openModal(src){
|
|
863
|
+
$('#modalImg').src=src;$('#modalImg').removeAttribute('hidden');
|
|
864
|
+
$('#modalJson').setAttribute('hidden','');
|
|
865
|
+
$('#modal').classList.add('open');
|
|
866
|
+
}
|
|
867
|
+
function openJsonModal(filePath){
|
|
868
|
+
fetch('/api/image?path='+encodeURIComponent(filePath)).then(function(r){
|
|
869
|
+
if(!r.ok)throw new Error('not found');
|
|
870
|
+
return r.text();
|
|
871
|
+
}).then(function(text){
|
|
872
|
+
$('#modalImg').setAttribute('hidden','');
|
|
873
|
+
var pre=$('#modalJson');
|
|
874
|
+
pre.innerHTML=highlightJson(prettyJson(text));
|
|
875
|
+
pre.removeAttribute('hidden');
|
|
876
|
+
$('#modal').classList.add('open');
|
|
877
|
+
}).catch(function(){showToast('Could not load JSON capture','error')});
|
|
878
|
+
}
|
|
676
879
|
$('#modal').addEventListener('click',function(){$('#modal').classList.remove('open')});
|
|
880
|
+
/* Allow selecting/copying JSON text without closing the modal */
|
|
881
|
+
$('#modalJson').addEventListener('click',function(e){e.stopPropagation()});
|
|
882
|
+
|
|
883
|
+
/* ══════════════════════════════════════════════════════════════════
|
|
884
|
+
Screenshot Replay Player — plays a run's per-step screenshots
|
|
885
|
+
as a video. Pulls frames from r.actions[].autoScreenshot|screenshot.
|
|
886
|
+
══════════════════════════════════════════════════════════════════ */
|
|
887
|
+
var REPLAY={frames:[],idx:0,playing:false,speed:1,timer:null,frameMs:1000};
|
|
888
|
+
|
|
889
|
+
function buildReplayFrames(actions,run){
|
|
890
|
+
var frames=[];
|
|
891
|
+
(actions||[]).forEach(function(a,i){
|
|
892
|
+
var path=a.autoScreenshot||a.screenshot||null;
|
|
893
|
+
frames.push({
|
|
894
|
+
idx:i,
|
|
895
|
+
path:path,
|
|
896
|
+
src:path?'/api/image?path='+encodeURIComponent(path):null,
|
|
897
|
+
type:a.type||'',
|
|
898
|
+
narr:a.narrative||a.type||'',
|
|
899
|
+
duration:a.duration||0,
|
|
900
|
+
success:!!a.success,
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
if(run&&!run.success&&run.errorScreenshot){
|
|
904
|
+
frames.push({
|
|
905
|
+
idx:frames.length,
|
|
906
|
+
path:run.errorScreenshot,
|
|
907
|
+
src:'/api/image?path='+encodeURIComponent(run.errorScreenshot),
|
|
908
|
+
type:'failure',
|
|
909
|
+
narr:'Page state at failure',
|
|
910
|
+
duration:0,
|
|
911
|
+
success:false,
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
return frames;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function openReplay(actions,run){
|
|
918
|
+
REPLAY.frames=buildReplayFrames(actions,run);
|
|
919
|
+
if(!REPLAY.frames.length){showToast&&showToast('No screenshots to replay','warn');return}
|
|
920
|
+
REPLAY.idx=0;REPLAY.playing=false;
|
|
921
|
+
$('#replayModal').classList.add('open');
|
|
922
|
+
$('#replayModal').setAttribute('aria-hidden','false');
|
|
923
|
+
renderReplayFrame();
|
|
924
|
+
// Auto-start playback for the "video" feel
|
|
925
|
+
toggleReplayPlay(true);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function closeReplay(){
|
|
929
|
+
stopReplayTimer();
|
|
930
|
+
$('#replayModal').classList.remove('open');
|
|
931
|
+
$('#replayModal').setAttribute('aria-hidden','true');
|
|
932
|
+
// Free image src to avoid lingering downloads
|
|
933
|
+
$('#replayImg').src='';
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function renderReplayFrame(){
|
|
937
|
+
var f=REPLAY.frames[REPLAY.idx];if(!f)return;
|
|
938
|
+
var modal=$('#replayModal');
|
|
939
|
+
var img=$('#replayImg');
|
|
940
|
+
if(f.src){
|
|
941
|
+
modal.classList.remove('empty');
|
|
942
|
+
if(img.src!==location.origin+f.src&&img.src!==f.src)img.src=f.src;
|
|
943
|
+
}else{
|
|
944
|
+
modal.classList.add('empty');
|
|
945
|
+
img.src='';
|
|
946
|
+
}
|
|
947
|
+
$('#replayStepNum').textContent=(REPLAY.idx+1)+' / '+REPLAY.frames.length;
|
|
948
|
+
$('#replayStepType').textContent=f.type;
|
|
949
|
+
$('#replayStepNarr').textContent=f.narr;
|
|
950
|
+
var pct=REPLAY.frames.length>1?(REPLAY.idx/(REPLAY.frames.length-1))*100:100;
|
|
951
|
+
$('#replayProgressFill').style.width=pct+'%';
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function scheduleNextReplayFrame(){
|
|
955
|
+
stopReplayTimer();
|
|
956
|
+
if(!REPLAY.playing)return;
|
|
957
|
+
if(REPLAY.idx>=REPLAY.frames.length-1){toggleReplayPlay(false);return}
|
|
958
|
+
// Uniform pacing: 1 frame per second at 1x, scaled by speed.
|
|
959
|
+
var ms=REPLAY.frameMs/REPLAY.speed;
|
|
960
|
+
REPLAY.timer=setTimeout(function(){
|
|
961
|
+
REPLAY.idx++;
|
|
962
|
+
renderReplayFrame();
|
|
963
|
+
scheduleNextReplayFrame();
|
|
964
|
+
},ms);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function stopReplayTimer(){if(REPLAY.timer){clearTimeout(REPLAY.timer);REPLAY.timer=null}}
|
|
968
|
+
|
|
969
|
+
function toggleReplayPlay(forceState){
|
|
970
|
+
REPLAY.playing=typeof forceState==='boolean'?forceState:!REPLAY.playing;
|
|
971
|
+
// If we're at the last frame and user hits play, restart from 0
|
|
972
|
+
if(REPLAY.playing&&REPLAY.idx>=REPLAY.frames.length-1){REPLAY.idx=0;renderReplayFrame()}
|
|
973
|
+
var btn=$('#replayPlay');if(btn)btn.innerHTML=REPLAY.playing?'❙❙':'▶';
|
|
974
|
+
if(REPLAY.playing)scheduleNextReplayFrame();else stopReplayTimer();
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function stepReplay(delta){
|
|
978
|
+
stopReplayTimer();
|
|
979
|
+
REPLAY.idx=Math.max(0,Math.min(REPLAY.frames.length-1,REPLAY.idx+delta));
|
|
980
|
+
renderReplayFrame();
|
|
981
|
+
if(REPLAY.playing)scheduleNextReplayFrame();
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function setReplaySpeed(s){
|
|
985
|
+
REPLAY.speed=s;
|
|
986
|
+
document.querySelectorAll('.replay-speed-btn').forEach(function(b){
|
|
987
|
+
b.classList.toggle('active',parseFloat(b.dataset.speed)===s);
|
|
988
|
+
});
|
|
989
|
+
if(REPLAY.playing){stopReplayTimer();scheduleNextReplayFrame()}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Wire up controls
|
|
993
|
+
$('#replayPlay').addEventListener('click',function(){toggleReplayPlay()});
|
|
994
|
+
$('#replayPrev').addEventListener('click',function(){stepReplay(-1)});
|
|
995
|
+
$('#replayNext').addEventListener('click',function(){stepReplay(1)});
|
|
996
|
+
$('#replayClose').addEventListener('click',closeReplay);
|
|
997
|
+
document.querySelectorAll('.replay-speed-btn').forEach(function(b){
|
|
998
|
+
b.addEventListener('click',function(){setReplaySpeed(parseFloat(b.dataset.speed))});
|
|
999
|
+
});
|
|
1000
|
+
document.addEventListener('keydown',function(e){
|
|
1001
|
+
if(!$('#replayModal').classList.contains('open'))return;
|
|
1002
|
+
if(e.key==='Escape'){closeReplay()}
|
|
1003
|
+
else if(e.key===' '){e.preventDefault();toggleReplayPlay()}
|
|
1004
|
+
else if(e.key==='ArrowLeft'){stepReplay(-1)}
|
|
1005
|
+
else if(e.key==='ArrowRight'){stepReplay(1)}
|
|
1006
|
+
});
|
|
1007
|
+
// Click on stage advances; click outside the image closes
|
|
1008
|
+
$('#replayModal').addEventListener('click',function(e){
|
|
1009
|
+
if(e.target&&e.target.id==='replayModal')closeReplay();
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
// Expose to the renderer below so the storyline header can wire its button
|
|
1013
|
+
window.openReplay=openReplay;
|
|
1014
|
+
|
|
1015
|
+
/* ══════════════════════════════════════════════════════════════════
|
|
1016
|
+
Network tab — cross-run network query (Investigate › Network)
|
|
1017
|
+
══════════════════════════════════════════════════════════════════ */
|
|
1018
|
+
function refreshNetwork(){
|
|
1019
|
+
var box=$('#netResults');var empty=$('#networkEmpty');
|
|
1020
|
+
if(!box)return;
|
|
1021
|
+
box.textContent='';
|
|
1022
|
+
box.appendChild(el('div',{style:'padding:20px;text-align:center;color:var(--text3);font-size:11px'},'Loading...'));
|
|
1023
|
+
var runsUrl=S.project?'/api/db/projects/'+S.project+'/runs':'/api/db/runs';
|
|
1024
|
+
api(runsUrl).then(function(runs){
|
|
1025
|
+
if(!Array.isArray(runs)||runs.length===0){
|
|
1026
|
+
box.textContent='';if(empty)empty.style.display='block';return;
|
|
1027
|
+
}
|
|
1028
|
+
if(empty)empty.style.display='none';
|
|
1029
|
+
var top=runs.slice(0,30);
|
|
1030
|
+
var promises=top.map(function(r){
|
|
1031
|
+
return api('/api/db/runs/'+r.id+'/network-logs').catch(function(){return []}).then(function(logs){
|
|
1032
|
+
return {run:r,logs:Array.isArray(logs)?logs:[]};
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
Promise.all(promises).then(function(results){renderNetworkResults(box,results)});
|
|
1036
|
+
}).catch(function(){box.textContent='';if(empty)empty.style.display='block'});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function renderNetworkResults(box,results){
|
|
1040
|
+
var statusFilter=$('#netStatusFilter')?$('#netStatusFilter').value:'errors';
|
|
1041
|
+
var urlFilter=($('#netUrlFilter')?$('#netUrlFilter').value:'').toLowerCase().trim();
|
|
1042
|
+
var rows=[];
|
|
1043
|
+
results.forEach(function(r){
|
|
1044
|
+
r.logs.forEach(function(n){
|
|
1045
|
+
var s=n.status||0;
|
|
1046
|
+
var keep=true;
|
|
1047
|
+
if(statusFilter==='errors')keep=s>=400||s===0;
|
|
1048
|
+
else if(statusFilter==='slow')keep=(n.duration||0)>=1000;
|
|
1049
|
+
if(keep&&urlFilter&&(n.url||'').toLowerCase().indexOf(urlFilter)<0)keep=false;
|
|
1050
|
+
if(keep)rows.push({run:r.run,n:n});
|
|
1051
|
+
});
|
|
1052
|
+
});
|
|
1053
|
+
box.textContent='';
|
|
1054
|
+
if(rows.length===0){
|
|
1055
|
+
box.appendChild(el('div',{style:'padding:30px;text-align:center;color:var(--text3);font-size:11px'},'No matching network records.'));
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
rows.sort(function(a,b){return (b.n.duration||0)-(a.n.duration||0)});
|
|
1059
|
+
var head=el('div',{className:'net-row net-head'},[
|
|
1060
|
+
el('span',{className:'net-col-run'},'Run'),
|
|
1061
|
+
el('span',{className:'net-col-method'},'Method'),
|
|
1062
|
+
el('span',{className:'net-col-status'},'Status'),
|
|
1063
|
+
el('span',{className:'net-col-url'},'URL'),
|
|
1064
|
+
el('span',{className:'net-col-dur'},'Duration')
|
|
1065
|
+
]);
|
|
1066
|
+
box.appendChild(head);
|
|
1067
|
+
rows.slice(0,200).forEach(function(rr){
|
|
1068
|
+
var n=rr.n;var s=n.status||0;
|
|
1069
|
+
var sCls=s===0?'s5xx':s<300?'s2xx':s<400?'s3xx':s<500?'s4xx':'s5xx';
|
|
1070
|
+
var mCls=(n.method||'GET').toLowerCase();
|
|
1071
|
+
var row=el('div',{className:'net-row clickable',onclick:function(){
|
|
1072
|
+
showView('investigate');
|
|
1073
|
+
var btn=document.querySelector('.tab-btn[data-tab="runsTabHistory"]');if(btn)btn.click();
|
|
1074
|
+
}},[
|
|
1075
|
+
el('span',{className:'net-col-run'},'#'+rr.run.id),
|
|
1076
|
+
el('span',{className:'net-col-method m-'+mCls},n.method||'GET'),
|
|
1077
|
+
el('span',{className:'net-col-status st-'+sCls},String(s||'ERR')),
|
|
1078
|
+
el('span',{className:'net-col-url',title:n.url||''},n.url||''),
|
|
1079
|
+
el('span',{className:'net-col-dur'},dur(n.duration||0))
|
|
1080
|
+
]);
|
|
1081
|
+
box.appendChild(row);
|
|
1082
|
+
});
|
|
1083
|
+
if(rows.length>200){
|
|
1084
|
+
box.appendChild(el('div',{style:'padding:10px;text-align:center;color:var(--text3);font-size:11px'},'Showing 200 of '+rows.length+' results'));
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
(function(){
|
|
1089
|
+
var btn=$('#btnRefreshNetwork');if(btn)btn.addEventListener('click',refreshNetwork);
|
|
1090
|
+
var sel=$('#netStatusFilter');if(sel)sel.addEventListener('change',refreshNetwork);
|
|
1091
|
+
var inp=$('#netUrlFilter');
|
|
1092
|
+
if(inp){
|
|
1093
|
+
var deb;
|
|
1094
|
+
inp.addEventListener('input',function(){clearTimeout(deb);deb=setTimeout(refreshNetwork,200)});
|
|
1095
|
+
}
|
|
1096
|
+
// Lazy: only fetch first time the Network tab is clicked
|
|
1097
|
+
var netTabBtn=document.querySelector('.tab-btn[data-tab="investigateTabNetwork"]');
|
|
1098
|
+
if(netTabBtn){
|
|
1099
|
+
netTabBtn.addEventListener('click',function(){
|
|
1100
|
+
if(!netTabBtn.dataset.loaded){netTabBtn.dataset.loaded='1';refreshNetwork()}
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
})();
|