@matware/e2e-runner 1.3.0 → 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 +37 -6
- package/.claude-plugin/plugin.json +17 -3
- package/LICENSE +190 -0
- package/README.md +151 -527
- package/agents/test-creator.md +4 -2
- package/agents/test-improver.md +5 -3
- package/bin/cli.js +84 -20
- package/commands/capture.md +45 -0
- 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 +321 -14
- package/src/ai-generate.js +81 -0
- package/src/app-pool.js +339 -0
- package/src/config.js +131 -7
- package/src/dashboard.js +209 -11
- package/src/db.js +74 -7
- package/src/index.js +6 -4
- package/src/learner-sqlite.js +154 -0
- package/src/learner.js +70 -3
- package/src/mcp-tools.js +259 -34
- package/src/module-analysis.js +247 -0
- package/src/module-resolver.js +35 -2
- package/src/narrate.js +42 -1
- package/src/pool-manager.js +68 -17
- package/src/pool.js +464 -37
- package/src/reporter.js +4 -1
- package/src/runner.js +410 -63
- package/src/visual-diff.js +515 -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 +62 -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 +20 -0
- package/templates/dashboard/js/view-live.js +240 -9
- package/templates/dashboard/js/view-runs.js +540 -94
- 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 +36 -0
- package/templates/dashboard/styles/base.css +489 -53
- package/templates/dashboard/styles/components.css +719 -77
- package/templates/dashboard/styles/view-live.css +463 -59
- package/templates/dashboard/styles/view-runs.css +793 -155
- 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 +369 -56
- package/templates/dashboard.html +5375 -901
- 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
|
}
|
|
@@ -88,7 +88,7 @@ function refreshRuns(){
|
|
|
88
88
|
var htr=document.createElement('tr');
|
|
89
89
|
var cols=[];
|
|
90
90
|
if(!S.project)cols.push('Project');
|
|
91
|
-
cols=cols.concat(['Suite','Source','Date','Total','Pass','Fail','Rate','Time']);
|
|
91
|
+
cols=cols.concat(['Suite','Driver','Source','Date','Total','Pass','Fail','Rate','Time']);
|
|
92
92
|
cols.forEach(function(c){htr.appendChild(el('th',null,c))});
|
|
93
93
|
head.textContent='';head.appendChild(htr);
|
|
94
94
|
var colSpan=cols.length;
|
|
@@ -107,6 +107,7 @@ function refreshRuns(){
|
|
|
107
107
|
if(r.id===S.selectedRun)tr.classList.add('expanded');
|
|
108
108
|
if(!S.project)tr.appendChild(el('td',{style:'font-weight:600'},r.project_name||'-'));
|
|
109
109
|
tr.appendChild(el('td',{style:'color:var(--accent)'},r.suite_name||'all'));
|
|
110
|
+
var driverTd=document.createElement('td');driverTd.appendChild(createDriverBadge(r.pool_driver));tr.appendChild(driverTd);
|
|
110
111
|
var srcTd=document.createElement('td');srcTd.appendChild(createTriggerBadge(r.triggered_by));tr.appendChild(srcTd);
|
|
111
112
|
tr.appendChild(el('td',null,fdate(r.generated_at)));
|
|
112
113
|
tr.appendChild(el('td',null,String(r.total||0)));
|
|
@@ -203,8 +204,10 @@ function loadDetailInline(id,detailTr){
|
|
|
203
204
|
])
|
|
204
205
|
]);
|
|
205
206
|
var srcBlock=el('div',null,[el('div',{className:'rd-s-label'},'Source'),el('div',{style:'margin-top:4px'},[createTriggerBadge(d.triggeredBy)])]);
|
|
207
|
+
var drvBlock=el('div',null,[el('div',{className:'rd-s-label'},'Driver'),el('div',{style:'margin-top:4px'},[createDriverBadge(d.poolDriver)])]);
|
|
206
208
|
var summ=el('div',{className:'rd-summary'},[
|
|
207
209
|
el('div',null,[el('div',{className:'rd-s-label'},'Suite'),el('div',{className:'rd-s-val',style:'font-size:14px;color:var(--accent)'},d.suiteName||'all')]),
|
|
210
|
+
drvBlock,
|
|
208
211
|
srcBlock,
|
|
209
212
|
el('div',null,[el('div',{className:'rd-s-label'},'Total'),el('div',{className:'rd-s-val'},String(d.summary.total))]),
|
|
210
213
|
el('div',null,[el('div',{className:'rd-s-label'},'Passed'),el('div',{className:'rd-s-val',style:'color:var(--green)'},String(d.summary.passed))]),
|
|
@@ -272,53 +275,121 @@ function loadDetailInline(id,detailTr){
|
|
|
272
275
|
body.appendChild(errDiv);
|
|
273
276
|
}
|
|
274
277
|
|
|
275
|
-
//
|
|
278
|
+
// Storyline \u2014 unified per-step cards with thumbnails, narrative, duration bar
|
|
276
279
|
if(r.actions&&r.actions.length){
|
|
277
280
|
var passCount=r.actions.filter(function(a){return a.success}).length;
|
|
278
281
|
var failCount=r.actions.length-passCount;
|
|
279
|
-
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'},[
|
|
280
293
|
el('span',{className:'net-arrow'},'\u25B6'),
|
|
281
|
-
el('span',{className:'net-title'},'
|
|
294
|
+
el('span',{className:'net-title'},'Storyline'),
|
|
282
295
|
el('div',{className:'net-stats'},[
|
|
283
296
|
el('span',{className:'net-stat'},[document.createTextNode('Steps: '),el('strong',null,String(r.actions.length))]),
|
|
284
|
-
failCount?el('span',{className:'net-stat has-err'},[document.createTextNode('Failed: '),el('strong',null,String(failCount))]):null
|
|
285
|
-
|
|
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
|
|
286
301
|
]);
|
|
287
|
-
var
|
|
288
|
-
|
|
302
|
+
var slBody=el('div',{className:'storyline'});
|
|
303
|
+
|
|
304
|
+
r.actions.forEach(function(a,idx){
|
|
305
|
+
var stepNum=String(idx+1).padStart(2,'0');
|
|
289
306
|
var label=a.narrative||a.type;
|
|
290
307
|
var durText=a.duration!=null?dur(a.duration):'';
|
|
291
|
-
var
|
|
292
|
-
|
|
293
|
-
|
|
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))})})}
|
|
294
336
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
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),
|
|
298
343
|
retryBadge,
|
|
299
|
-
el('span',{className:'
|
|
300
|
-
])
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
344
|
+
el('span',{className:'sl-dur'},durText)
|
|
345
|
+
]);
|
|
346
|
+
|
|
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
|
+
]);
|
|
305
354
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if(s.hash){capEl.appendChild(createHashBadge(s.hash))}
|
|
318
|
-
else{(function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,s.path)}
|
|
319
|
-
shotsWrap.appendChild(el('div',{className:'rd-shot'+(s.type==='error'?' err-shot':''),onclick:function(e){e.stopPropagation();openModal(src)}},[img,capEl]));
|
|
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);
|
|
320
366
|
});
|
|
321
|
-
|
|
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]));
|
|
322
393
|
}
|
|
323
394
|
|
|
324
395
|
// Console logs
|
|
@@ -398,11 +469,63 @@ function refreshScreenshots(){
|
|
|
398
469
|
var img=document.createElement('img');img.src=src;img.alt=f.name;img.loading='lazy';
|
|
399
470
|
var capEl=el('div',{className:'cap'},[el('span',{className:'cap-name'},f.name)]);
|
|
400
471
|
(function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,f.path);
|
|
401
|
-
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]));
|
|
402
473
|
});
|
|
474
|
+
resetBlankBar();
|
|
403
475
|
}).catch(function(){});
|
|
404
476
|
}
|
|
405
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
|
+
|
|
406
529
|
function searchByHash(){
|
|
407
530
|
var container=$('#ssSearchResult');
|
|
408
531
|
container.textContent='';
|
|
@@ -436,10 +559,8 @@ function refreshLearnings(){
|
|
|
436
559
|
fetch(url).then(function(r){return r.json()}).then(function(data){
|
|
437
560
|
if(!data||data.totalRuns===0){
|
|
438
561
|
$('#learningsEmpty').style.display='block';
|
|
439
|
-
$('#
|
|
440
|
-
$('#
|
|
441
|
-
$('#learningsPages').textContent='';$('#learningsApis').textContent='';
|
|
442
|
-
$('#learningsErrors').textContent='';
|
|
562
|
+
$('#learnHero').textContent='';$('#learnCards').textContent='';
|
|
563
|
+
$('#learnTrend').textContent='';$('#learnBottom').textContent='';
|
|
443
564
|
$('#badgeLearnings').textContent='-';
|
|
444
565
|
return;
|
|
445
566
|
}
|
|
@@ -464,48 +585,139 @@ function refreshLearnings(){
|
|
|
464
585
|
$('#badgeLearnings').textContent='\u2714';
|
|
465
586
|
$('#badgeLearnings').style.background='var(--green-dim)';$('#badgeLearnings').style.color='var(--green)';
|
|
466
587
|
}
|
|
467
|
-
|
|
588
|
+
renderLearnHero(data);
|
|
589
|
+
renderLearnCards(data);
|
|
468
590
|
renderLearnTrend(data.recentTrend||[]);
|
|
469
|
-
|
|
470
|
-
renderLearnSelectors(data.unstableSelectors||[]);
|
|
471
|
-
renderLearnPages(data.failingPages||[]);
|
|
472
|
-
renderLearnApis(data.apiIssues||[]);
|
|
473
|
-
renderLearnErrors(data.topErrors||[]);
|
|
591
|
+
renderLearnBottomRow(data);
|
|
474
592
|
}).catch(function(){$('#learningsEmpty').style.display='block'});
|
|
475
593
|
}
|
|
476
594
|
|
|
477
|
-
function
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
595
|
+
function rateColor(v){return v>=90?'var(--green)':v>=70?'var(--amber)':'var(--red)'}
|
|
596
|
+
function rateClass(v){return v>=90?'good':v>=70?'warn':'bad'}
|
|
597
|
+
function durFmt(ms){return ms<1000?Math.round(ms)+'ms':(ms/1000).toFixed(1)+'s'}
|
|
598
|
+
|
|
599
|
+
function renderLearnHero(d){
|
|
600
|
+
var c=$('#learnHero');c.textContent='';
|
|
601
|
+
var wrap=document.createElement('div');wrap.className='learn-hero';
|
|
602
|
+
var passRate=d.overallPassRate||0;
|
|
603
|
+
var ns='http://www.w3.org/2000/svg';
|
|
604
|
+
var ringWrap=document.createElement('div');ringWrap.className='learn-hero-ring';
|
|
605
|
+
var svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 36 36');
|
|
606
|
+
var bgCircle=document.createElementNS(ns,'circle');bgCircle.setAttribute('cx','18');bgCircle.setAttribute('cy','18');bgCircle.setAttribute('r','15.9');bgCircle.className.baseVal='learn-hero-ring-bg';svg.appendChild(bgCircle);
|
|
607
|
+
var fgCircle=document.createElementNS(ns,'circle');fgCircle.setAttribute('cx','18');fgCircle.setAttribute('cy','18');fgCircle.setAttribute('r','15.9');fgCircle.className.baseVal='learn-hero-ring-fg';
|
|
608
|
+
var circ=2*Math.PI*15.9;fgCircle.setAttribute('stroke-dasharray',circ.toFixed(1));fgCircle.setAttribute('stroke-dashoffset',(circ*(1-passRate/100)).toFixed(1));fgCircle.setAttribute('stroke',rateColor(passRate));
|
|
609
|
+
svg.appendChild(fgCircle);ringWrap.appendChild(svg);
|
|
610
|
+
var pctEl=document.createElement('div');pctEl.className='learn-hero-pct';pctEl.style.color=rateColor(passRate);pctEl.textContent=passRate+'%';
|
|
611
|
+
ringWrap.appendChild(pctEl);wrap.appendChild(ringWrap);
|
|
612
|
+
|
|
613
|
+
var stats=document.createElement('div');stats.className='learn-hero-stats';
|
|
614
|
+
var badSels=d.unstableSelectors?d.unstableSelectors.length:0;
|
|
615
|
+
var slowTests=d.failingPages?d.failingPages.length:0;
|
|
616
|
+
var apiIssues=d.apiIssues?d.apiIssues.length:0;
|
|
617
|
+
var topErr=d.topErrors&&d.topErrors.length>0?d.topErrors[0].occurrence_count:0;
|
|
618
|
+
var flakyCount=d.flakyTests?d.flakyTests.length:0;
|
|
619
|
+
var items=[
|
|
620
|
+
{val:String(d.totalRuns),lbl:'Runs',color:'var(--accent)'},
|
|
621
|
+
{val:String(d.totalTests),lbl:'Tests',color:'var(--accent)'},
|
|
622
|
+
{val:durFmt(d.avgDurationMs||0),lbl:'Avg Duration',color:'var(--purple)'},
|
|
623
|
+
{val:String(flakyCount),lbl:'Flaky',color:flakyCount>0?'var(--amber)':'var(--green)'},
|
|
624
|
+
{val:String(badSels),lbl:'Bad Selectors',color:badSels>0?'var(--red)':'var(--green)'},
|
|
625
|
+
{val:String(slowTests),lbl:'Slow Pages',color:slowTests>0?'var(--amber)':'var(--green)'},
|
|
626
|
+
{val:String(apiIssues),lbl:'API Issues',color:apiIssues>0?'var(--red)':'var(--green)'},
|
|
627
|
+
{val:String(topErr),lbl:'Top Error Hits',color:topErr>0?'var(--red)':'var(--green)'}
|
|
628
|
+
];
|
|
629
|
+
items.forEach(function(it){
|
|
630
|
+
var statEl=document.createElement('div');statEl.className='learn-hero-stat';
|
|
631
|
+
var valEl=document.createElement('div');valEl.className='learn-hero-stat-val';valEl.style.color=it.color;valEl.textContent=it.val;
|
|
632
|
+
var lblEl=document.createElement('div');lblEl.className='learn-hero-stat-lbl';lblEl.textContent=it.lbl;
|
|
633
|
+
statEl.appendChild(valEl);statEl.appendChild(lblEl);stats.appendChild(statEl);
|
|
490
634
|
});
|
|
491
|
-
|
|
635
|
+
wrap.appendChild(stats);c.appendChild(wrap);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function makeLearnItem(label,sub,pct,valText,color){
|
|
639
|
+
var item=document.createElement('div');item.className='learn-item';
|
|
640
|
+
var barWrap=document.createElement('div');barWrap.className='learn-item-bar';
|
|
641
|
+
var lblEl=document.createElement('div');lblEl.className='learn-item-label';
|
|
642
|
+
var codeEl=document.createElement('code');codeEl.textContent=label;lblEl.appendChild(codeEl);
|
|
643
|
+
barWrap.appendChild(lblEl);
|
|
644
|
+
if(sub){var subEl=document.createElement('div');subEl.className='learn-item-sub';subEl.textContent=sub;barWrap.appendChild(subEl)}
|
|
645
|
+
var bar=document.createElement('div');bar.className='learn-bar';
|
|
646
|
+
var fill=document.createElement('div');fill.className='learn-bar-fill';fill.style.width=Math.min(pct,100)+'%';fill.style.background=color;
|
|
647
|
+
bar.appendChild(fill);barWrap.appendChild(bar);
|
|
648
|
+
item.appendChild(barWrap);
|
|
649
|
+
var valEl=document.createElement('div');valEl.className='learn-item-val';valEl.style.color=color;valEl.textContent=valText;
|
|
650
|
+
item.appendChild(valEl);
|
|
651
|
+
return item;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function makeLearnCard(icon,title,emptyMsg){
|
|
655
|
+
var card=document.createElement('div');card.className='learn-card';
|
|
656
|
+
var titleEl=document.createElement('div');titleEl.className='learn-card-title';
|
|
657
|
+
var iconEl=document.createElement('span');iconEl.className='lc-icon';iconEl.textContent=icon;
|
|
658
|
+
titleEl.appendChild(iconEl);titleEl.appendChild(document.createTextNode(title));
|
|
659
|
+
card.appendChild(titleEl);
|
|
660
|
+
card._empty=emptyMsg;
|
|
661
|
+
return card;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function renderLearnCards(d){
|
|
665
|
+
var c=$('#learnCards');c.textContent='';
|
|
666
|
+
|
|
667
|
+
var selCard=makeLearnCard('\u26A0','Risky Selectors','No unstable selectors');
|
|
668
|
+
var sels=d.unstableSelectors||[];
|
|
669
|
+
if(!sels.length){var e1=document.createElement('div');e1.className='learn-card-empty';e1.textContent=selCard._empty;selCard.appendChild(e1)}
|
|
670
|
+
else{sels.slice(0,5).forEach(function(s){
|
|
671
|
+
var sel=s.selector.length>40?s.selector.slice(0,37)+'...':s.selector;
|
|
672
|
+
selCard.appendChild(makeLearnItem(sel,s.action_type+' \u00B7 '+s.total_uses+' uses',parseFloat(s.fail_rate),s.fail_rate+'%',parseFloat(s.fail_rate)>30?'var(--red)':'var(--amber)'));
|
|
673
|
+
})}
|
|
674
|
+
c.appendChild(selCard);
|
|
675
|
+
|
|
676
|
+
var pageCard=makeLearnCard('\u23F1','Problem Pages','No failing pages');
|
|
677
|
+
var pages=d.failingPages||[];
|
|
678
|
+
if(!pages.length){var e2=document.createElement('div');e2.className='learn-card-empty';e2.textContent=pageCard._empty;pageCard.appendChild(e2)}
|
|
679
|
+
else{pages.slice(0,5).forEach(function(p){
|
|
680
|
+
pageCard.appendChild(makeLearnItem(p.url_path,p.total_visits+' visits \u00B7 '+p.console_errors+' console errs',parseFloat(p.fail_rate),p.fail_rate+'%',parseFloat(p.fail_rate)>30?'var(--red)':'var(--amber)'));
|
|
681
|
+
})}
|
|
682
|
+
c.appendChild(pageCard);
|
|
683
|
+
|
|
684
|
+
var flakyCard=makeLearnCard('\u223C','Flaky Tests','No flaky tests detected');
|
|
685
|
+
var flaky=d.flakyTests||[];
|
|
686
|
+
if(!flaky.length){var e3=document.createElement('div');e3.className='learn-card-empty';e3.textContent=flakyCard._empty;flakyCard.appendChild(e3)}
|
|
687
|
+
else{flaky.slice(0,5).forEach(function(f){
|
|
688
|
+
flakyCard.appendChild(makeLearnItem(f.test_name,'Attempt avg '+f.avg_attempts+' \u00B7 '+f.total_runs+' runs',parseFloat(f.flaky_rate),f.flaky_rate+'%',parseFloat(f.flaky_rate)>30?'var(--red)':'var(--amber)'));
|
|
689
|
+
})}
|
|
690
|
+
c.appendChild(flakyCard);
|
|
691
|
+
|
|
692
|
+
var apiCard=makeLearnCard('\u21C4','API Issues','No API issues');
|
|
693
|
+
var apis=d.apiIssues||[];
|
|
694
|
+
if(!apis.length){var e4=document.createElement('div');e4.className='learn-card-empty';e4.textContent=apiCard._empty;apiCard.appendChild(e4)}
|
|
695
|
+
else{apis.slice(0,5).forEach(function(a){
|
|
696
|
+
var ep=a.endpoint.length>40?a.endpoint.slice(0,37)+'...':a.endpoint;
|
|
697
|
+
apiCard.appendChild(makeLearnItem(ep,a.total_calls+' calls \u00B7 '+durFmt(a.avg_duration_ms),parseFloat(a.error_rate),a.error_rate+'%',parseFloat(a.error_rate)>20?'var(--red)':'var(--amber)'));
|
|
698
|
+
})}
|
|
699
|
+
c.appendChild(apiCard);
|
|
492
700
|
}
|
|
493
701
|
|
|
494
702
|
function renderLearnTrend(trend){
|
|
495
|
-
var container=$('#
|
|
703
|
+
var container=$('#learnTrend');container.textContent='';
|
|
496
704
|
if(!trend.length)return;
|
|
497
|
-
var card=document.createElement('div');card.className='card';
|
|
498
|
-
var
|
|
499
|
-
var
|
|
705
|
+
var card=document.createElement('div');card.className='learn-card';
|
|
706
|
+
var titleEl=document.createElement('div');titleEl.className='learn-card-title';
|
|
707
|
+
var iconEl=document.createElement('span');iconEl.className='lc-icon';iconEl.textContent='\u2197';
|
|
708
|
+
titleEl.appendChild(iconEl);titleEl.appendChild(document.createTextNode('Pass Rate Trend'));
|
|
709
|
+
card.appendChild(titleEl);
|
|
710
|
+
var chartDiv=document.createElement('div');chartDiv.style.cssText='height:80px;width:100%';
|
|
500
711
|
var w=100/trend.length;var ns='http://www.w3.org/2000/svg';
|
|
501
|
-
var svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 100 100');svg.setAttribute('preserveAspectRatio','none');
|
|
712
|
+
var svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 100 100');svg.setAttribute('preserveAspectRatio','none');svg.style.cssText='width:100%;height:100%';
|
|
502
713
|
var bg=document.createElementNS(ns,'rect');bg.setAttribute('x','0');bg.setAttribute('y','0');bg.setAttribute('width','100');bg.setAttribute('height','100');bg.setAttribute('fill','var(--surface2)');bg.setAttribute('rx','2');svg.appendChild(bg);
|
|
503
714
|
var gridLine=document.createElementNS(ns,'line');gridLine.setAttribute('x1','0');gridLine.setAttribute('y1','50');gridLine.setAttribute('x2','100');gridLine.setAttribute('y2','50');gridLine.setAttribute('stroke','var(--border)');gridLine.setAttribute('stroke-width','0.3');gridLine.setAttribute('stroke-dasharray','2,2');svg.appendChild(gridLine);
|
|
504
715
|
var pts=trend.map(function(t,i){return(i*w+w/2)+','+(100-t.pass_rate)}).join(' ');
|
|
505
716
|
var poly=document.createElementNS(ns,'polygon');poly.setAttribute('points',(0*w+w/2)+',100 '+pts+' '+((trend.length-1)*w+w/2)+',100');poly.setAttribute('fill','var(--accent-dim)');svg.appendChild(poly);
|
|
506
717
|
var pl=document.createElementNS(ns,'polyline');pl.setAttribute('points',pts);pl.setAttribute('fill','none');pl.setAttribute('stroke','var(--accent)');pl.setAttribute('stroke-width','1.5');svg.appendChild(pl);
|
|
507
718
|
trend.forEach(function(t,i){
|
|
508
|
-
var
|
|
719
|
+
var color=rateColor(t.pass_rate);
|
|
720
|
+
var circle=document.createElementNS(ns,'circle');circle.setAttribute('cx',''+(i*w+w/2));circle.setAttribute('cy',''+(100-t.pass_rate));circle.setAttribute('r','2.5');circle.setAttribute('fill',color);
|
|
509
721
|
var title=document.createElementNS(ns,'title');title.textContent=t.date+': '+t.pass_rate+'% ('+t.total_tests+' tests)';circle.appendChild(title);svg.appendChild(circle);
|
|
510
722
|
});
|
|
511
723
|
chartDiv.appendChild(svg);card.appendChild(chartDiv);
|
|
@@ -514,35 +726,47 @@ function renderLearnTrend(trend){
|
|
|
514
726
|
card.appendChild(dates);container.appendChild(card);
|
|
515
727
|
}
|
|
516
728
|
|
|
517
|
-
function
|
|
518
|
-
var
|
|
519
|
-
|
|
520
|
-
var
|
|
521
|
-
var
|
|
522
|
-
var
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
var
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
729
|
+
function renderLearnBottomRow(d){
|
|
730
|
+
var c=$('#learnBottom');c.textContent='';
|
|
731
|
+
|
|
732
|
+
var errCard=makeLearnCard('\u2718','Most Common Errors','No errors recorded');
|
|
733
|
+
var errors=d.topErrors||[];
|
|
734
|
+
if(!errors.length){var e1=document.createElement('div');e1.className='learn-card-empty';e1.textContent=errCard._empty;errCard.appendChild(e1)}
|
|
735
|
+
else{errors.slice(0,5).forEach(function(e){
|
|
736
|
+
var pat=e.pattern.length>45?e.pattern.slice(0,42)+'...':e.pattern;
|
|
737
|
+
var maxCount=errors[0].occurrence_count||1;
|
|
738
|
+
var pct=(e.occurrence_count/maxCount)*100;
|
|
739
|
+
var verdictEl=document.createElement('div');verdictEl.className='learn-verdict '+rateClass(100-(pct));verdictEl.textContent=e.category.replace(/-/g,' ');
|
|
740
|
+
var item=makeLearnItem(pat,(e.last_seen||'').split('T')[0]+' \u00B7 '+e.occurrence_count+'x',pct,e.occurrence_count+'x','var(--red)');
|
|
741
|
+
item.insertBefore(verdictEl,item.lastChild);
|
|
742
|
+
errCard.appendChild(item);
|
|
743
|
+
})}
|
|
744
|
+
c.appendChild(errCard);
|
|
745
|
+
|
|
746
|
+
var slowCard=makeLearnCard('\u23F3','Slowest Tests','No slow test data');
|
|
747
|
+
var trend=d.recentTrend||[];
|
|
748
|
+
var slowTests=[];
|
|
749
|
+
if(d.flakyTests){
|
|
750
|
+
d.flakyTests.forEach(function(f){
|
|
751
|
+
if(f.avg_duration_ms&&f.avg_duration_ms>2000){slowTests.push({name:f.test_name,dur:f.avg_duration_ms})}
|
|
534
752
|
});
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
753
|
+
}
|
|
754
|
+
if(d.failingPages){
|
|
755
|
+
d.failingPages.forEach(function(p){
|
|
756
|
+
if(p.avg_load_time_ms&&p.avg_load_time_ms>3000){slowTests.push({name:p.url_path,dur:p.avg_load_time_ms})}
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
slowTests.sort(function(a,b){return b.dur-a.dur});
|
|
760
|
+
if(!slowTests.length){var e2=document.createElement('div');e2.className='learn-card-empty';e2.textContent=slowCard._empty;slowCard.appendChild(e2)}
|
|
761
|
+
else{
|
|
762
|
+
var maxDur=slowTests[0].dur;
|
|
763
|
+
slowTests.slice(0,5).forEach(function(t){
|
|
764
|
+
slowCard.appendChild(makeLearnItem(t.name,'','',durFmt(t.dur),(t.dur/maxDur)*100,t.dur>5000?'var(--red)':'var(--amber)'));
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
c.appendChild(slowCard);
|
|
538
768
|
}
|
|
539
769
|
|
|
540
|
-
function renderLearnFlaky(flaky){var c=$('#learningsFlaky');c.textContent='';if(!flaky.length)return;c.appendChild(buildLearnTable('Flaky Tests',['Test','Flaky Rate','Occurrences','Total Runs','Last Flaky','Avg Attempts'],flaky.map(function(f){return[{code:f.test_name},{badge:f.flaky_rate+'%',cls:f.flaky_rate>30?'fail':'flaky'},{text:f.flaky_count},{text:f.total_runs},{text:(f.last_flaky||'-').split('T')[0]},{text:f.avg_attempts}]})))}
|
|
541
|
-
function renderLearnSelectors(sels){var c=$('#learningsSelectors');c.textContent='';if(!sels.length)return;c.appendChild(buildLearnTable('Unstable Selectors',['Selector','Action','Fail Rate','Uses','Tests','Page'],sels.map(function(s){var sel=s.selector.length>45?s.selector.slice(0,42)+'...':s.selector;return[{code:sel},{text:s.action_type},{badge:s.fail_rate+'%',cls:s.fail_rate>30?'fail':'flaky'},{text:s.total_uses},{text:s.used_by_tests},{text:s.page_url||'-'}]})))}
|
|
542
|
-
function renderLearnPages(pages){var c=$('#learningsPages');c.textContent='';if(!pages.length)return;c.appendChild(buildLearnTable('Failing Pages',['Page','Fail Rate','Visits','Console Errors','Network Errors'],pages.map(function(p){return[{code:p.url_path},{badge:p.fail_rate+'%',cls:p.fail_rate>30?'fail':'flaky'},{text:p.total_visits},{text:p.console_errors},{text:p.network_errors}]})))}
|
|
543
|
-
function renderLearnApis(apis){var c=$('#learningsApis');c.textContent='';if(!apis.length)return;c.appendChild(buildLearnTable('API Issues',['Endpoint','Error Rate','Calls','Avg Duration','Status Codes'],apis.map(function(a){var ep=a.endpoint.length>45?a.endpoint.slice(0,42)+'...':a.endpoint;var d=a.avg_duration_ms<1000?Math.round(a.avg_duration_ms)+'ms':(a.avg_duration_ms/1000).toFixed(1)+'s';return[{code:ep},{badge:a.error_rate+'%',cls:a.error_rate>20?'fail':'flaky'},{text:a.total_calls},{text:d},{text:a.status_codes||'-'}]})))}
|
|
544
|
-
function renderLearnErrors(errors){var c=$('#learningsErrors');c.textContent='';if(!errors.length)return;c.appendChild(buildLearnTable('Error Patterns',['Pattern','Category','Count','First Seen','Last Seen','Example Test'],errors.map(function(e){var pat=e.pattern.length>50?e.pattern.slice(0,47)+'...':e.pattern;return[{text:pat},{badge:e.category,cls:'run'},{text:e.occurrence_count},{text:(e.first_seen||'-').split('T')[0]},{text:(e.last_seen||'-').split('T')[0]},{code:e.example_test||'-'}]})))}
|
|
545
|
-
|
|
546
770
|
$('#btnRefreshLearnings').addEventListener('click',refreshLearnings);
|
|
547
771
|
$('#learningsDays').addEventListener('change',refreshLearnings);
|
|
548
772
|
|
|
@@ -570,3 +794,225 @@ $('#btnExportLearnings').addEventListener('click',function(){
|
|
|
570
794
|
/* ── Modal ── */
|
|
571
795
|
function openModal(src){$('#modalImg').src=src;$('#modal').classList.add('open')}
|
|
572
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
|
+
})();
|