@matware/e2e-runner 1.2.1 → 1.3.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.
Files changed (88) hide show
  1. package/.claude-plugin/marketplace.json +52 -0
  2. package/.claude-plugin/plugin.json +17 -3
  3. package/.mcp.json +2 -2
  4. package/.opencode/commands/create-test.md +63 -0
  5. package/.opencode/commands/run.md +50 -0
  6. package/.opencode/commands/verify-issue.md +62 -0
  7. package/.opencode/skills/e2e-testing/SKILL.md +181 -0
  8. package/.opencode/skills/e2e-testing/references/action-types.md +143 -0
  9. package/.opencode/skills/e2e-testing/references/auth-strategies.md +91 -0
  10. package/.opencode/skills/e2e-testing/references/graphql.md +59 -0
  11. package/.opencode/skills/e2e-testing/references/issue-verification.md +59 -0
  12. package/.opencode/skills/e2e-testing/references/multi-pool.md +60 -0
  13. package/.opencode/skills/e2e-testing/references/network-debugging.md +62 -0
  14. package/.opencode/skills/e2e-testing/references/test-json-format.md +163 -0
  15. package/.opencode/skills/e2e-testing/references/troubleshooting.md +224 -0
  16. package/.opencode/skills/e2e-testing/references/variables.md +41 -0
  17. package/.opencode/skills/e2e-testing/references/visual-verification.md +89 -0
  18. package/LICENSE +190 -0
  19. package/OPENCODE.md +166 -0
  20. package/README.md +165 -104
  21. package/agents/test-creator.md +54 -1
  22. package/agents/test-improver.md +37 -0
  23. package/bin/cli.js +409 -16
  24. package/commands/capture.md +45 -0
  25. package/commands/create-test.md +16 -1
  26. package/opencode.json +11 -0
  27. package/package.json +7 -2
  28. package/scripts/setup-opencode.sh +113 -0
  29. package/skills/e2e-testing/SKILL.md +10 -3
  30. package/skills/e2e-testing/references/action-types.md +48 -5
  31. package/skills/e2e-testing/references/auth-strategies.md +91 -0
  32. package/skills/e2e-testing/references/graphql.md +59 -0
  33. package/skills/e2e-testing/references/issue-verification.md +59 -0
  34. package/skills/e2e-testing/references/multi-pool.md +60 -0
  35. package/skills/e2e-testing/references/network-debugging.md +62 -0
  36. package/skills/e2e-testing/references/test-json-format.md +4 -0
  37. package/skills/e2e-testing/references/troubleshooting.md +44 -2
  38. package/skills/e2e-testing/references/variables.md +41 -0
  39. package/skills/e2e-testing/references/visual-verification.md +89 -0
  40. package/src/actions.js +475 -2
  41. package/src/ai-generate.js +139 -8
  42. package/src/app-pool.js +339 -0
  43. package/src/config.js +266 -5
  44. package/src/dashboard.js +216 -17
  45. package/src/db.js +191 -7
  46. package/src/index.js +12 -9
  47. package/src/learner-sqlite.js +458 -0
  48. package/src/learner.js +78 -6
  49. package/src/mcp-tools.js +1348 -51
  50. package/src/module-resolver.js +37 -0
  51. package/src/narrate.js +65 -0
  52. package/src/pool-manager.js +229 -0
  53. package/src/pool.js +301 -31
  54. package/src/reporter.js +86 -2
  55. package/src/runner.js +480 -71
  56. package/src/sync/auth.js +354 -0
  57. package/src/sync/client.js +572 -0
  58. package/src/sync/hub-routes.js +816 -0
  59. package/src/sync/index.js +68 -0
  60. package/src/sync/middleware.js +347 -0
  61. package/src/sync/queue.js +209 -0
  62. package/src/sync/schema.js +540 -0
  63. package/src/verify.js +10 -7
  64. package/src/visual-diff.js +446 -0
  65. package/src/watch.js +384 -0
  66. package/templates/build-dashboard.js +47 -6
  67. package/templates/dashboard/js/api.js +62 -0
  68. package/templates/dashboard/js/init.js +13 -0
  69. package/templates/dashboard/js/keyboard.js +46 -0
  70. package/templates/dashboard/js/state.js +40 -0
  71. package/templates/dashboard/js/toast.js +41 -0
  72. package/templates/dashboard/js/utils.js +216 -0
  73. package/templates/dashboard/js/view-live.js +181 -0
  74. package/templates/dashboard/js/view-runs.js +676 -0
  75. package/templates/dashboard/js/view-tests.js +294 -0
  76. package/templates/dashboard/js/view-watch.js +242 -0
  77. package/templates/dashboard/js/websocket.js +116 -0
  78. package/templates/dashboard/styles/base.css +69 -0
  79. package/templates/dashboard/styles/components.css +117 -0
  80. package/templates/dashboard/styles/view-live.css +97 -0
  81. package/templates/dashboard/styles/view-runs.css +243 -0
  82. package/templates/dashboard/styles/view-tests.css +96 -0
  83. package/templates/dashboard/styles/view-watch.css +53 -0
  84. package/templates/dashboard/template.html +181 -100
  85. package/templates/dashboard.html +1614 -547
  86. package/templates/sample-test.json +0 -8
  87. package/templates/dashboard/app.js +0 -1152
  88. package/templates/dashboard/styles.css +0 -413
@@ -0,0 +1,676 @@
1
+ /* ══════════════════════════════════════════════════════════════════
2
+ Runs View — History + Screenshots + Learnings (inner tabs)
3
+ ══════════════════════════════════════════════════════════════════ */
4
+
5
+ /* ── Filters ── */
6
+ $$('.filter-btn').forEach(function(btn){
7
+ btn.addEventListener('click',function(){
8
+ $$('.filter-btn').forEach(function(b){b.classList.remove('active')});
9
+ btn.classList.add('active');
10
+ S.runFilter.status=btn.dataset.filter;
11
+ applyRunFilters();
12
+ });
13
+ });
14
+ $('#runSearchInput').addEventListener('input',function(){
15
+ S.runFilter.search=this.value.trim().toLowerCase();
16
+ applyRunFilters();
17
+ });
18
+
19
+ var _allRunRows=[];
20
+ function applyRunFilters(){
21
+ _allRunRows.forEach(function(item){
22
+ var show=true;
23
+ var r=item.data;
24
+ if(S.runFilter.status!=='all'){
25
+ var total=r.total||0;var passed=r.passed||0;var failed=r.failed||0;
26
+ if(S.runFilter.status==='pass'&&(failed>0||total===0))show=false;
27
+ if(S.runFilter.status==='fail'&&failed===0)show=false;
28
+ if(S.runFilter.status==='mixed'&&(failed===0||passed===0))show=false;
29
+ }
30
+ if(show&&S.runFilter.search){
31
+ var suite=(r.suite_name||'all').toLowerCase();
32
+ var proj=(r.project_name||'').toLowerCase();
33
+ if(suite.indexOf(S.runFilter.search)===-1&&proj.indexOf(S.runFilter.search)===-1)show=false;
34
+ }
35
+ item.tr.style.display=show?'':'none';
36
+ if(item.detailTr)item.detailTr.style.display=show?'':'none';
37
+ });
38
+ }
39
+
40
+ function renderRunsHealthBanner(){
41
+ var banner=$('#runsHealthBanner');
42
+ banner.textContent='';
43
+ var url=S.project?'/api/db/projects/'+S.project+'/health':'/api/db/health';
44
+ fetch(url).then(function(r){return r.json()}).then(function(h){
45
+ if(!h||!h.passRate)return;
46
+ var rateColor=h.passRate>=90?'green':h.passRate>=70?'amber':'red';
47
+ var trendIcon=h.passRateTrend==='improving'?'\u25B2':h.passRateTrend==='declining'?'\u25BC':'=';
48
+ var trendCls=h.passRateTrend==='improving'?'green':h.passRateTrend==='declining'?'red':'dim';
49
+ var deltaStr=h.trendDelta!==0?(h.trendDelta>0?'+':'')+h.trendDelta+'%':'';
50
+
51
+ banner.appendChild(el('div',{className:'hb-item'},[
52
+ el('div',{className:'hb-val '+rateColor},h.passRate+'%'),
53
+ el('div',{className:'hb-lbl'},'Pass Rate'),
54
+ el('div',{className:'hb-trend '+trendCls},trendIcon+' '+h.passRateTrend+(deltaStr?' ('+deltaStr+')':''))
55
+ ]));
56
+ if(h.flakyCount>0){
57
+ banner.appendChild(el('div',{className:'hb-item'},[
58
+ el('div',{className:'hb-val amber'},String(h.flakyCount)),
59
+ el('div',{className:'hb-lbl'},'Flaky Tests')
60
+ ]));
61
+ }
62
+ if(h.topErrorPattern){
63
+ var cat=h.topErrorPattern.category||h.topErrorPattern.pattern||'unknown';
64
+ var pat=cat.replace(/-/g,' ').replace(/\b\w/g,function(c){return c.toUpperCase()});
65
+ banner.appendChild(el('div',{className:'hb-item'},[
66
+ el('div',{className:'hb-val red',style:'font-size:13px'},pat),
67
+ el('div',{className:'hb-lbl'},'Top Error ('+h.topErrorPattern.count+'x)')
68
+ ]));
69
+ }
70
+ banner.appendChild(el('div',{className:'hb-link',onclick:function(){var lb=$('#runsTabLearnings');if(lb)lb.click()}},[
71
+ el('span',null,'\u2192 View Learnings')
72
+ ]));
73
+ }).catch(function(){});
74
+ }
75
+
76
+ function refreshRuns(){
77
+ renderRunsHealthBanner();
78
+ var url=S.project?'/api/db/projects/'+S.project+'/runs':'/api/db/runs';
79
+ api(url).then(function(rows){
80
+ var chart=$('#trendChart'),body=$('#runsBody'),empty=$('#runsEmpty'),head=$('#runsHead');
81
+ chart.textContent='';body.textContent='';
82
+ _allRunRows=[];
83
+ S.highlightedRunIdx=-1;
84
+ if(!Array.isArray(rows)||rows.length===0){empty.style.display='block';head.parentNode.parentNode.style.display='none';$('#badgeRuns').textContent='0';return}
85
+ empty.style.display='none';head.parentNode.parentNode.style.display='';
86
+ $('#badgeRuns').textContent=rows.length;
87
+
88
+ var htr=document.createElement('tr');
89
+ var cols=[];
90
+ if(!S.project)cols.push('Project');
91
+ cols=cols.concat(['Suite','Driver','Source','Date','Total','Pass','Fail','Rate','Time']);
92
+ cols.forEach(function(c){htr.appendChild(el('th',null,c))});
93
+ head.textContent='';head.appendChild(htr);
94
+ var colSpan=cols.length;
95
+
96
+ rows.slice(0,40).slice().reverse().forEach(function(r){
97
+ var rate=parseFloat(r.pass_rate)||0;
98
+ var color=rate>=90?'var(--green)':rate>=70?'var(--amber)':'var(--red)';
99
+ var bar=el('div',{className:'chart-bar',style:'height:'+Math.max(rate,4)+'%;background:'+color});
100
+ bar.appendChild(el('div',{className:'tip'},(r.project_name||'')+(r.suite_name?' / '+r.suite_name:'')+': '+r.pass_rate));
101
+ chart.appendChild(bar);
102
+ });
103
+
104
+ rows.forEach(function(r){
105
+ var tr=document.createElement('tr');
106
+ tr.dataset.runId=r.id;
107
+ if(r.id===S.selectedRun)tr.classList.add('expanded');
108
+ if(!S.project)tr.appendChild(el('td',{style:'font-weight:600'},r.project_name||'-'));
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);
111
+ var srcTd=document.createElement('td');srcTd.appendChild(createTriggerBadge(r.triggered_by));tr.appendChild(srcTd);
112
+ tr.appendChild(el('td',null,fdate(r.generated_at)));
113
+ tr.appendChild(el('td',null,String(r.total||0)));
114
+ tr.appendChild(el('td',{style:'color:var(--green)'},String(r.passed||0)));
115
+ tr.appendChild(el('td',{style:'color:var(--red)'},String(r.failed||0)));
116
+ var rv=parseFloat(r.pass_rate)||0;
117
+ tr.appendChild(el('td',{style:'font-weight:600;color:'+(rv>=90?'var(--green)':rv>=70?'var(--amber)':'var(--red)')},r.pass_rate||'-'));
118
+ tr.appendChild(el('td',{style:'color:var(--text2)'},r.duration||'-'));
119
+ tr.addEventListener('click',function(){toggleDetail(r.id,tr,colSpan)});
120
+ body.appendChild(tr);
121
+
122
+ var item={tr:tr,data:r,detailTr:null};
123
+ if(r.id===S.selectedRun){
124
+ var detailTr=createDetailRow(colSpan);
125
+ body.appendChild(detailTr);
126
+ loadDetailInline(r.id,detailTr);
127
+ item.detailTr=detailTr;
128
+ }
129
+ _allRunRows.push(item);
130
+ });
131
+ }).catch(function(){});
132
+ }
133
+
134
+ function createDetailRow(colSpan){
135
+ var detailTr=document.createElement('tr');
136
+ detailTr.className='run-detail-row';
137
+ var td=document.createElement('td');
138
+ td.setAttribute('colspan',colSpan);
139
+ var wrap=el('div',{className:'rd-wrap'});
140
+ var inner=el('div',{className:'rd-inner'},[
141
+ el('div',{style:'color:var(--text3);font-size:11px'},[
142
+ el('span',{className:'spinner-small'}),
143
+ document.createTextNode(' Loading...')
144
+ ])
145
+ ]);
146
+ wrap.appendChild(inner);
147
+ td.appendChild(wrap);
148
+ detailTr.appendChild(td);
149
+ return detailTr;
150
+ }
151
+
152
+ function toggleDetail(id,clickedTr,colSpan){
153
+ if(S.selectedRun===id){
154
+ var existing=clickedTr.nextElementSibling;
155
+ if(existing&&existing.classList.contains('run-detail-row')){
156
+ var w=existing.querySelector('.rd-wrap');
157
+ if(w)w.classList.remove('open');
158
+ clickedTr.classList.remove('expanded');
159
+ setTimeout(function(){if(existing.parentNode)existing.parentNode.removeChild(existing)},350);
160
+ }
161
+ S.selectedRun=null;
162
+ return;
163
+ }
164
+
165
+ var prevTr=document.querySelector('#runsBody tr.expanded');
166
+ if(prevTr){
167
+ prevTr.classList.remove('expanded');
168
+ var prevDetail=prevTr.nextElementSibling;
169
+ if(prevDetail&&prevDetail.classList.contains('run-detail-row')){
170
+ var pw=prevDetail.querySelector('.rd-wrap');
171
+ if(pw)pw.classList.remove('open');
172
+ setTimeout(function(){if(prevDetail.parentNode)prevDetail.parentNode.removeChild(prevDetail)},350);
173
+ }
174
+ }
175
+
176
+ S.selectedRun=id;
177
+ clickedTr.classList.add('expanded');
178
+ var detailTr=createDetailRow(colSpan);
179
+ clickedTr.parentNode.insertBefore(detailTr,clickedTr.nextSibling);
180
+ requestAnimationFrame(function(){
181
+ requestAnimationFrame(function(){
182
+ var w2=detailTr.querySelector('.rd-wrap');
183
+ if(w2)w2.classList.add('open');
184
+ });
185
+ });
186
+ loadDetailInline(id,detailTr);
187
+ }
188
+
189
+ /* ── Run Detail ── */
190
+ function loadDetailInline(id,detailTr){
191
+ api('/api/db/runs/'+id).then(function(d){
192
+ if(d.error)return;
193
+ var inner=detailTr.querySelector('.rd-inner');
194
+ inner.textContent='';
195
+ var results=d.results||[];
196
+
197
+ var exportBtn=el('div',null,[
198
+ el('div',{className:'rd-s-label'},'Export'),
199
+ el('div',{style:'margin-top:4px'},[
200
+ el('button',{className:'btn sm',onclick:function(e){
201
+ e.stopPropagation();
202
+ downloadFile('run-'+id+'.json',JSON.stringify(d,null,2),'application/json');
203
+ }},'JSON')
204
+ ])
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)])]);
208
+ var summ=el('div',{className:'rd-summary'},[
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,
211
+ srcBlock,
212
+ el('div',null,[el('div',{className:'rd-s-label'},'Total'),el('div',{className:'rd-s-val'},String(d.summary.total))]),
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))]),
214
+ el('div',null,[el('div',{className:'rd-s-label'},'Failed'),el('div',{className:'rd-s-val',style:'color:'+(d.summary.failed>0?'var(--red)':'var(--text3)')},String(d.summary.failed))]),
215
+ el('div',null,[el('div',{className:'rd-s-label'},'Duration'),el('div',{className:'rd-s-val',style:'font-size:14px;color:var(--text2)'},d.summary.duration||'-')]),
216
+ exportBtn
217
+ ]);
218
+ inner.appendChild(summ);
219
+
220
+ // Insights
221
+ var insightsContainer=el('div',{className:'rd-insights'});
222
+ inner.appendChild(insightsContainer);
223
+ fetch('/api/db/runs/'+id+'/insights').then(function(r){return r.json()}).then(function(ins){
224
+ if(!ins||ins.error)return;
225
+ var items=[];
226
+ var h=ins.health;
227
+ if(h){
228
+ var rateColor=h.passRate>=90?'green':h.passRate>=70?'amber':'red';
229
+ var trendIcon=h.passRateTrend==='improving'?'\u25B2':h.passRateTrend==='declining'?'\u25BC':'=';
230
+ var trendCls=h.passRateTrend==='improving'?'green':h.passRateTrend==='declining'?'red':'';
231
+ items.push(el('div',{className:'rd-ins-health'},[
232
+ el('span',{className:'rd-ins-rate '+rateColor},h.passRate+'%'),
233
+ el('span',{className:'rd-ins-trend '+trendCls},trendIcon+' '+h.passRateTrend),
234
+ h.flakyCount>0?el('span',{className:'rd-ins-tag amber'},h.flakyCount+' flaky'):null,
235
+ h.unstableSelectorCount>0?el('span',{className:'rd-ins-tag red'},h.unstableSelectorCount+' unstable sel.'):null
236
+ ]));
237
+ }
238
+ var insights=ins.insights||[];
239
+ insights.forEach(function(i){
240
+ var icon=i.type==='new-failure'?'\u2718':i.type==='recovered'?'\u2714':i.type==='flaky'?'\u223C':'!';
241
+ var cls=i.type==='new-failure'?'red':i.type==='recovered'?'green':i.type==='flaky'?'amber':'';
242
+ items.push(el('div',{className:'rd-ins-item '+cls},[
243
+ el('span',{className:'rd-ins-icon'},icon),
244
+ el('span',null,i.message)
245
+ ]));
246
+ });
247
+ if(items.length>0){items.forEach(function(it){insightsContainer.appendChild(it)})}
248
+ else{insightsContainer.style.display='none'}
249
+ }).catch(function(){insightsContainer.style.display='none'});
250
+
251
+ // Pool distribution bar
252
+ var histPoolTests={};
253
+ results.forEach(function(r){if(!r.poolUrl)return;histPoolTests[r.name]={poolUrl:r.poolUrl,success:r.success}});
254
+ var histPoolDist=buildPoolDistribution(histPoolTests);
255
+ if(histPoolDist)inner.appendChild(histPoolDist);
256
+
257
+ results.forEach(function(r){
258
+ var d2=r.durationMs?dur(r.durationMs):r.endTime&&r.startTime?dur(new Date(r.endTime)-new Date(r.startTime)):'-';
259
+ var flaky=r.success&&r.attempt>1;
260
+ var state=flaky?'flaky':(r.success?'pass':'fail');
261
+
262
+ var badges=el('div',{style:'display:flex;gap:6px;align-items:center;flex-shrink:0'});
263
+ badges.appendChild(el('span',{className:'badge '+(r.success?'pass':'fail')},r.success?'PASS':'FAIL'));
264
+ if(flaky)badges.appendChild(el('span',{className:'badge flaky'},'FLAKY'));
265
+
266
+ var poolEl=r.poolUrl?el('span',{className:'pool-badge'},r.poolUrl.replace('ws://','').replace('wss://','')) :null;
267
+ var head=el('div',{className:'rd-test-head'},[badges,el('div',{className:'rd-test-name'},[document.createTextNode(r.name),poolEl]),el('div',{className:'rd-test-dur'},d2)]);
268
+ var body=el('div',{className:'rd-test-body'});
269
+
270
+ if(r.maxAttempts>1){body.appendChild(el('div',{className:'rd-retries'},'Attempt '+r.attempt+' of '+r.maxAttempts))}
271
+ if(r.error){
272
+ var errDiv=el('div',{className:'rd-error-msg'});
273
+ errDiv.appendChild(document.createTextNode(r.error));
274
+ errDiv.appendChild(makeCopyBtn(r.error));
275
+ body.appendChild(errDiv);
276
+ }
277
+
278
+ // Actions panel
279
+ if(r.actions&&r.actions.length){
280
+ var passCount=r.actions.filter(function(a){return a.success}).length;
281
+ var failCount=r.actions.length-passCount;
282
+ var actHead=el('div',{className:'rd-net-head'},[
283
+ el('span',{className:'net-arrow'},'\u25B6'),
284
+ el('span',{className:'net-title'},'Actions'),
285
+ el('div',{className:'net-stats'},[
286
+ 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
+ ])
289
+ ]);
290
+ var actBody=el('div',{className:'rd-net-body',style:'padding:8px 14px'});
291
+ r.actions.forEach(function(a){
292
+ var label=a.narrative||a.type;
293
+ var durText=a.duration!=null?dur(a.duration):'';
294
+ var retryBadge=null;
295
+ if(a.actionRetries&&a.actionRetries>0){
296
+ retryBadge=el('span',{className:'badge flaky',style:'font-size:9px;padding:1px 5px'},'\u21BB x'+a.actionRetries);
297
+ }
298
+ actBody.appendChild(el('div',{className:'lt-step'},[
299
+ el('span',{className:'step-icon '+(a.success?'ok':'fail')},a.success?'\u2714':'\u2718'),
300
+ el('span',{className:'step-detail',style:'flex:1'},label),
301
+ retryBadge,
302
+ el('span',{className:'step-dur'},durText)
303
+ ]));
304
+ });
305
+ actHead.addEventListener('click',function(){actHead.classList.toggle('open')});
306
+ body.appendChild(el('div',{className:'rd-net-panel'},[actHead,actBody]));
307
+ }
308
+
309
+ // Screenshots
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]));
323
+ });
324
+ body.appendChild(shotsWrap);
325
+ }
326
+
327
+ // Console logs
328
+ var cIssues=(r.consoleLogs||[]).filter(function(l){return l.type==='error'||l.type==='warn'||l.type==='warning'});
329
+ if(cIssues.length){
330
+ var cErrors=cIssues.filter(function(l){return l.type==='error'}).length;
331
+ var cWarns=cIssues.length-cErrors;
332
+ var conHead=el('div',{className:'rd-net-head'},[
333
+ el('span',{className:'net-arrow'},'\u25B6'),
334
+ el('span',{className:'net-title'},'Console'),
335
+ el('div',{className:'net-stats'},[
336
+ cErrors?el('span',{className:'net-stat has-err'},[document.createTextNode('Errors: '),el('strong',null,String(cErrors))]):null,
337
+ cWarns?el('span',{className:'net-stat'},[document.createTextNode('Warnings: '),el('strong',null,String(cWarns))]):null
338
+ ]),
339
+ makeCopyBtn(function(){return cIssues.map(function(l){return '['+l.type+'] '+l.text}).join('\n')})
340
+ ]);
341
+ var conBody=el('div',{className:'rd-net-body'});
342
+ cIssues.forEach(function(l){conBody.appendChild(el('div',{className:'rd-log-item '+l.type},'['+l.type+'] '+l.text))});
343
+ conHead.addEventListener('click',function(){conHead.classList.toggle('open')});
344
+ body.appendChild(el('div',{className:'rd-net-panel'},[conHead,conBody]));
345
+ }
346
+
347
+ // Network errors
348
+ if(r.networkErrors&&r.networkErrors.length){
349
+ var neHead=el('div',{className:'rd-net-head'},[
350
+ el('span',{className:'net-arrow'},'\u25B6'),
351
+ el('span',{className:'net-title'},'Network Errors'),
352
+ el('div',{className:'net-stats'},[el('span',{className:'net-stat has-err'},[document.createTextNode('Errors: '),el('strong',null,String(r.networkErrors.length))])]),
353
+ makeCopyBtn(function(){return r.networkErrors.map(function(ne){return '['+ne.error+'] '+ne.url}).join('\n')})
354
+ ]);
355
+ var neBody=el('div',{className:'rd-net-body'});
356
+ r.networkErrors.forEach(function(ne){neBody.appendChild(el('div',{className:'rd-log-item error'},'['+ne.error+'] '+ne.url))});
357
+ neHead.addEventListener('click',function(){neHead.classList.toggle('open')});
358
+ body.appendChild(el('div',{className:'rd-net-panel'},[neHead,neBody]));
359
+ }
360
+
361
+ // Network panel
362
+ if(r.networkLogs&&r.networkLogs.length){
363
+ var errCount=r.networkLogs.filter(function(n){return n.status>=400}).length;
364
+ var netHead=el('div',{className:'rd-net-head'},[
365
+ el('span',{className:'net-arrow'},'\u25B6'),
366
+ el('span',{className:'net-title'},'Network Requests'),
367
+ el('div',{className:'net-stats'},[
368
+ el('span',{className:'net-stat'},[document.createTextNode('Total: '),el('strong',null,String(r.networkLogs.length))]),
369
+ errCount?el('span',{className:'net-stat has-err'},[document.createTextNode('Errors: '),el('strong',null,String(errCount))]):null
370
+ ])
371
+ ]);
372
+ var netCols=el('div',{className:'rd-net-cols'},[el('span',{className:'col-e'},''),el('span',{className:'col-m'},'Method'),el('span',{className:'col-s'},'Status'),el('span',{className:'col-u'},'URL'),el('span',{className:'col-d'},'Time')]);
373
+ var netBody=el('div',{className:'rd-net-body'},[netCols]);
374
+ r.networkLogs.forEach(function(n){var built=buildNetRow(n);netBody.appendChild(built.row);if(built.detail)netBody.appendChild(built.detail)});
375
+ netHead.addEventListener('click',function(){netHead.classList.toggle('open')});
376
+ body.appendChild(el('div',{className:'rd-net-panel'},[netHead,netBody]));
377
+ }
378
+
379
+ inner.appendChild(el('div',{className:'rd-test '+state},[head,body]));
380
+ });
381
+
382
+ var w=detailTr.querySelector('.rd-wrap');
383
+ if(w&&!w.classList.contains('open')){requestAnimationFrame(function(){w.classList.add('open')})}
384
+ }).catch(function(){
385
+ var inner=detailTr.querySelector('.rd-inner');
386
+ if(inner)inner.textContent='Failed to load run detail';
387
+ });
388
+ }
389
+
390
+ /* ── Screenshots ── */
391
+ function refreshScreenshots(){
392
+ var gal=$('#screenshotGallery'),empty=$('#screenshotsEmpty');
393
+ gal.textContent='';
394
+ if(!S.project){empty.style.display='block';empty.querySelector('p').textContent='Select a project to view screenshots.';$('#badgeScreenshots').textContent='-';return}
395
+ api('/api/db/projects/'+S.project+'/screenshots').then(function(files){
396
+ if(!Array.isArray(files)||!files.length){empty.style.display='block';empty.querySelector('p').textContent='No screenshots for this project.';$('#badgeScreenshots').textContent='0';return}
397
+ empty.style.display='none';
398
+ $('#badgeScreenshots').textContent=files.length;
399
+ files.forEach(function(f){
400
+ var src='/api/image?path='+encodeURIComponent(f.path);
401
+ var img=document.createElement('img');img.src=src;img.alt=f.name;img.loading='lazy';
402
+ var capEl=el('div',{className:'cap'},[el('span',{className:'cap-name'},f.name)]);
403
+ (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]));
405
+ });
406
+ }).catch(function(){});
407
+ }
408
+
409
+ function searchByHash(){
410
+ var container=$('#ssSearchResult');
411
+ container.textContent='';
412
+ var raw=$('#ssHashInput').value.trim();
413
+ if(!raw)return;
414
+ var hash=raw.replace(/^ss:/,'');
415
+ if(!/^[a-f0-9]{1,8}$/i.test(hash)){
416
+ container.appendChild(el('div',{className:'ss-search-error'},'Invalid hash format. Expected 8 hex characters (e.g. ss:a3f2b1c9).'));
417
+ return;
418
+ }
419
+ fetch('/api/screenshot-hash/'+hash).then(function(res){
420
+ if(!res.ok){container.appendChild(el('div',{className:'ss-search-error'},'Screenshot not found for hash: ss:'+hash));return}
421
+ return res.blob();
422
+ }).then(function(blob){
423
+ if(!blob)return;
424
+ var url=URL.createObjectURL(blob);
425
+ var wrap=el('div',{className:'ss-search-result'},[el('div',{className:'ss-result-label'},[createHashBadge(hash),el('span',{},'Found')])]);
426
+ var img=document.createElement('img');img.src=url;img.alt='ss:'+hash;
427
+ img.addEventListener('click',function(){openModal(url)});
428
+ wrap.appendChild(img);
429
+ container.appendChild(wrap);
430
+ }).catch(function(){container.appendChild(el('div',{className:'ss-search-error'},'Error searching for screenshot.'))});
431
+ }
432
+ $('#ssHashBtn').addEventListener('click',searchByHash);
433
+ $('#ssHashInput').addEventListener('keydown',function(e){if(e.key==='Enter')searchByHash()});
434
+
435
+ /* ── Learnings ── */
436
+ function refreshLearnings(){
437
+ var days=$('#learningsDays').value||30;
438
+ var url=S.project?'/api/db/projects/'+S.project+'/learnings?days='+days:'/api/db/learnings?days='+days;
439
+ fetch(url).then(function(r){return r.json()}).then(function(data){
440
+ if(!data||data.totalRuns===0){
441
+ $('#learningsEmpty').style.display='block';
442
+ $('#learnHero').textContent='';$('#learnCards').textContent='';
443
+ $('#learnTrend').textContent='';$('#learnBottom').textContent='';
444
+ $('#badgeLearnings').textContent='-';
445
+ return;
446
+ }
447
+ $('#learningsEmpty').style.display='none';
448
+ S.lastLearningsData=data;
449
+ var flakyCount=data.flakyTests?data.flakyTests.length:0;
450
+ var passRate=data.overallPassRate||0;
451
+ var declining=data.recentTrend&&Array.isArray(data.recentTrend.data||data.recentTrend)&&(function(){
452
+ var td=data.recentTrend.data||data.recentTrend;
453
+ if(td.length<2)return false;
454
+ var last=td[td.length-1].pass_rate;
455
+ var prior=td.slice(0,-1).reduce(function(s,t){return s+t.pass_rate},0)/(td.length-1);
456
+ return last-prior<-2;
457
+ })();
458
+ if(passRate<70){
459
+ $('#badgeLearnings').textContent='\u26A0';
460
+ $('#badgeLearnings').style.background='var(--red-dim)';$('#badgeLearnings').style.color='var(--red)';
461
+ } else if(flakyCount>0||declining){
462
+ $('#badgeLearnings').textContent=flakyCount>0?flakyCount:(declining?'\u25BC':'\u2714');
463
+ $('#badgeLearnings').style.background='var(--amber-dim)';$('#badgeLearnings').style.color='var(--amber)';
464
+ } else {
465
+ $('#badgeLearnings').textContent='\u2714';
466
+ $('#badgeLearnings').style.background='var(--green-dim)';$('#badgeLearnings').style.color='var(--green)';
467
+ }
468
+ renderLearnHero(data);
469
+ renderLearnCards(data);
470
+ renderLearnTrend(data.recentTrend||[]);
471
+ renderLearnBottomRow(data);
472
+ }).catch(function(){$('#learningsEmpty').style.display='block'});
473
+ }
474
+
475
+ function rateColor(v){return v>=90?'var(--green)':v>=70?'var(--amber)':'var(--red)'}
476
+ function rateClass(v){return v>=90?'good':v>=70?'warn':'bad'}
477
+ function durFmt(ms){return ms<1000?Math.round(ms)+'ms':(ms/1000).toFixed(1)+'s'}
478
+
479
+ function renderLearnHero(d){
480
+ var c=$('#learnHero');c.textContent='';
481
+ var wrap=document.createElement('div');wrap.className='learn-hero';
482
+ var passRate=d.overallPassRate||0;
483
+ var ns='http://www.w3.org/2000/svg';
484
+ var ringWrap=document.createElement('div');ringWrap.className='learn-hero-ring';
485
+ var svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 36 36');
486
+ 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);
487
+ 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';
488
+ 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));
489
+ svg.appendChild(fgCircle);ringWrap.appendChild(svg);
490
+ var pctEl=document.createElement('div');pctEl.className='learn-hero-pct';pctEl.style.color=rateColor(passRate);pctEl.textContent=passRate+'%';
491
+ ringWrap.appendChild(pctEl);wrap.appendChild(ringWrap);
492
+
493
+ var stats=document.createElement('div');stats.className='learn-hero-stats';
494
+ var badSels=d.unstableSelectors?d.unstableSelectors.length:0;
495
+ var slowTests=d.failingPages?d.failingPages.length:0;
496
+ var apiIssues=d.apiIssues?d.apiIssues.length:0;
497
+ var topErr=d.topErrors&&d.topErrors.length>0?d.topErrors[0].occurrence_count:0;
498
+ var flakyCount=d.flakyTests?d.flakyTests.length:0;
499
+ var items=[
500
+ {val:String(d.totalRuns),lbl:'Runs',color:'var(--accent)'},
501
+ {val:String(d.totalTests),lbl:'Tests',color:'var(--accent)'},
502
+ {val:durFmt(d.avgDurationMs||0),lbl:'Avg Duration',color:'var(--purple)'},
503
+ {val:String(flakyCount),lbl:'Flaky',color:flakyCount>0?'var(--amber)':'var(--green)'},
504
+ {val:String(badSels),lbl:'Bad Selectors',color:badSels>0?'var(--red)':'var(--green)'},
505
+ {val:String(slowTests),lbl:'Slow Pages',color:slowTests>0?'var(--amber)':'var(--green)'},
506
+ {val:String(apiIssues),lbl:'API Issues',color:apiIssues>0?'var(--red)':'var(--green)'},
507
+ {val:String(topErr),lbl:'Top Error Hits',color:topErr>0?'var(--red)':'var(--green)'}
508
+ ];
509
+ items.forEach(function(it){
510
+ var statEl=document.createElement('div');statEl.className='learn-hero-stat';
511
+ var valEl=document.createElement('div');valEl.className='learn-hero-stat-val';valEl.style.color=it.color;valEl.textContent=it.val;
512
+ var lblEl=document.createElement('div');lblEl.className='learn-hero-stat-lbl';lblEl.textContent=it.lbl;
513
+ statEl.appendChild(valEl);statEl.appendChild(lblEl);stats.appendChild(statEl);
514
+ });
515
+ wrap.appendChild(stats);c.appendChild(wrap);
516
+ }
517
+
518
+ function makeLearnItem(label,sub,pct,valText,color){
519
+ var item=document.createElement('div');item.className='learn-item';
520
+ var barWrap=document.createElement('div');barWrap.className='learn-item-bar';
521
+ var lblEl=document.createElement('div');lblEl.className='learn-item-label';
522
+ var codeEl=document.createElement('code');codeEl.textContent=label;lblEl.appendChild(codeEl);
523
+ barWrap.appendChild(lblEl);
524
+ if(sub){var subEl=document.createElement('div');subEl.className='learn-item-sub';subEl.textContent=sub;barWrap.appendChild(subEl)}
525
+ var bar=document.createElement('div');bar.className='learn-bar';
526
+ var fill=document.createElement('div');fill.className='learn-bar-fill';fill.style.width=Math.min(pct,100)+'%';fill.style.background=color;
527
+ bar.appendChild(fill);barWrap.appendChild(bar);
528
+ item.appendChild(barWrap);
529
+ var valEl=document.createElement('div');valEl.className='learn-item-val';valEl.style.color=color;valEl.textContent=valText;
530
+ item.appendChild(valEl);
531
+ return item;
532
+ }
533
+
534
+ function makeLearnCard(icon,title,emptyMsg){
535
+ var card=document.createElement('div');card.className='learn-card';
536
+ var titleEl=document.createElement('div');titleEl.className='learn-card-title';
537
+ var iconEl=document.createElement('span');iconEl.className='lc-icon';iconEl.textContent=icon;
538
+ titleEl.appendChild(iconEl);titleEl.appendChild(document.createTextNode(title));
539
+ card.appendChild(titleEl);
540
+ card._empty=emptyMsg;
541
+ return card;
542
+ }
543
+
544
+ function renderLearnCards(d){
545
+ var c=$('#learnCards');c.textContent='';
546
+
547
+ var selCard=makeLearnCard('\u26A0','Risky Selectors','No unstable selectors');
548
+ var sels=d.unstableSelectors||[];
549
+ if(!sels.length){var e1=document.createElement('div');e1.className='learn-card-empty';e1.textContent=selCard._empty;selCard.appendChild(e1)}
550
+ else{sels.slice(0,5).forEach(function(s){
551
+ var sel=s.selector.length>40?s.selector.slice(0,37)+'...':s.selector;
552
+ 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)'));
553
+ })}
554
+ c.appendChild(selCard);
555
+
556
+ var pageCard=makeLearnCard('\u23F1','Problem Pages','No failing pages');
557
+ var pages=d.failingPages||[];
558
+ if(!pages.length){var e2=document.createElement('div');e2.className='learn-card-empty';e2.textContent=pageCard._empty;pageCard.appendChild(e2)}
559
+ else{pages.slice(0,5).forEach(function(p){
560
+ 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)'));
561
+ })}
562
+ c.appendChild(pageCard);
563
+
564
+ var flakyCard=makeLearnCard('\u223C','Flaky Tests','No flaky tests detected');
565
+ var flaky=d.flakyTests||[];
566
+ if(!flaky.length){var e3=document.createElement('div');e3.className='learn-card-empty';e3.textContent=flakyCard._empty;flakyCard.appendChild(e3)}
567
+ else{flaky.slice(0,5).forEach(function(f){
568
+ 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)'));
569
+ })}
570
+ c.appendChild(flakyCard);
571
+
572
+ var apiCard=makeLearnCard('\u21C4','API Issues','No API issues');
573
+ var apis=d.apiIssues||[];
574
+ if(!apis.length){var e4=document.createElement('div');e4.className='learn-card-empty';e4.textContent=apiCard._empty;apiCard.appendChild(e4)}
575
+ else{apis.slice(0,5).forEach(function(a){
576
+ var ep=a.endpoint.length>40?a.endpoint.slice(0,37)+'...':a.endpoint;
577
+ 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)'));
578
+ })}
579
+ c.appendChild(apiCard);
580
+ }
581
+
582
+ function renderLearnTrend(trend){
583
+ var container=$('#learnTrend');container.textContent='';
584
+ if(!trend.length)return;
585
+ var card=document.createElement('div');card.className='learn-card';
586
+ var titleEl=document.createElement('div');titleEl.className='learn-card-title';
587
+ var iconEl=document.createElement('span');iconEl.className='lc-icon';iconEl.textContent='\u2197';
588
+ titleEl.appendChild(iconEl);titleEl.appendChild(document.createTextNode('Pass Rate Trend'));
589
+ card.appendChild(titleEl);
590
+ var chartDiv=document.createElement('div');chartDiv.style.cssText='height:80px;width:100%';
591
+ var w=100/trend.length;var ns='http://www.w3.org/2000/svg';
592
+ var svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 100 100');svg.setAttribute('preserveAspectRatio','none');svg.style.cssText='width:100%;height:100%';
593
+ 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);
594
+ 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);
595
+ var pts=trend.map(function(t,i){return(i*w+w/2)+','+(100-t.pass_rate)}).join(' ');
596
+ 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);
597
+ 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);
598
+ trend.forEach(function(t,i){
599
+ var color=rateColor(t.pass_rate);
600
+ 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);
601
+ var title=document.createElementNS(ns,'title');title.textContent=t.date+': '+t.pass_rate+'% ('+t.total_tests+' tests)';circle.appendChild(title);svg.appendChild(circle);
602
+ });
603
+ chartDiv.appendChild(svg);card.appendChild(chartDiv);
604
+ var dates=document.createElement('div');dates.style.cssText='display:flex;justify-content:space-between;font-size:10px;color:var(--text3);margin-top:4px';
605
+ dates.appendChild(el('span',null,trend[0].date));dates.appendChild(el('span',null,trend[trend.length-1].date));
606
+ card.appendChild(dates);container.appendChild(card);
607
+ }
608
+
609
+ function renderLearnBottomRow(d){
610
+ var c=$('#learnBottom');c.textContent='';
611
+
612
+ var errCard=makeLearnCard('\u2718','Most Common Errors','No errors recorded');
613
+ var errors=d.topErrors||[];
614
+ if(!errors.length){var e1=document.createElement('div');e1.className='learn-card-empty';e1.textContent=errCard._empty;errCard.appendChild(e1)}
615
+ else{errors.slice(0,5).forEach(function(e){
616
+ var pat=e.pattern.length>45?e.pattern.slice(0,42)+'...':e.pattern;
617
+ var maxCount=errors[0].occurrence_count||1;
618
+ var pct=(e.occurrence_count/maxCount)*100;
619
+ var verdictEl=document.createElement('div');verdictEl.className='learn-verdict '+rateClass(100-(pct));verdictEl.textContent=e.category.replace(/-/g,' ');
620
+ var item=makeLearnItem(pat,(e.last_seen||'').split('T')[0]+' \u00B7 '+e.occurrence_count+'x',pct,e.occurrence_count+'x','var(--red)');
621
+ item.insertBefore(verdictEl,item.lastChild);
622
+ errCard.appendChild(item);
623
+ })}
624
+ c.appendChild(errCard);
625
+
626
+ var slowCard=makeLearnCard('\u23F3','Slowest Tests','No slow test data');
627
+ var trend=d.recentTrend||[];
628
+ var slowTests=[];
629
+ if(d.flakyTests){
630
+ d.flakyTests.forEach(function(f){
631
+ if(f.avg_duration_ms&&f.avg_duration_ms>2000){slowTests.push({name:f.test_name,dur:f.avg_duration_ms})}
632
+ });
633
+ }
634
+ if(d.failingPages){
635
+ d.failingPages.forEach(function(p){
636
+ if(p.avg_load_time_ms&&p.avg_load_time_ms>3000){slowTests.push({name:p.url_path,dur:p.avg_load_time_ms})}
637
+ });
638
+ }
639
+ slowTests.sort(function(a,b){return b.dur-a.dur});
640
+ if(!slowTests.length){var e2=document.createElement('div');e2.className='learn-card-empty';e2.textContent=slowCard._empty;slowCard.appendChild(e2)}
641
+ else{
642
+ var maxDur=slowTests[0].dur;
643
+ slowTests.slice(0,5).forEach(function(t){
644
+ slowCard.appendChild(makeLearnItem(t.name,'','',durFmt(t.dur),(t.dur/maxDur)*100,t.dur>5000?'var(--red)':'var(--amber)'));
645
+ });
646
+ }
647
+ c.appendChild(slowCard);
648
+ }
649
+
650
+ $('#btnRefreshLearnings').addEventListener('click',refreshLearnings);
651
+ $('#learningsDays').addEventListener('change',refreshLearnings);
652
+
653
+ $('#btnExportLearnings').addEventListener('click',function(){
654
+ var data=S.lastLearningsData;
655
+ if(!data){showToast('No learnings data to export','error');return}
656
+ var md='# E2E Learnings Report\n\n';
657
+ md+='| Metric | Value |\n|--------|-------|\n';
658
+ md+='| Total Runs | '+data.totalRuns+' |\n';
659
+ md+='| Total Tests | '+data.totalTests+' |\n';
660
+ md+='| Pass Rate | '+data.overallPassRate+'% |\n';
661
+ md+='| Avg Duration | '+dur(data.avgDurationMs)+' |\n\n';
662
+ if(data.flakyTests&&data.flakyTests.length){
663
+ md+='## Flaky Tests\n\n| Test | Flaky Rate | Occurrences |\n|------|-----------|-------------|\n';
664
+ data.flakyTests.forEach(function(f){md+='| '+f.test_name+' | '+f.flaky_rate+'% | '+f.flaky_count+' |\n'});md+='\n';
665
+ }
666
+ if(data.unstableSelectors&&data.unstableSelectors.length){
667
+ md+='## Unstable Selectors\n\n| Selector | Action | Fail Rate |\n|----------|--------|-----------|\n';
668
+ data.unstableSelectors.forEach(function(s){md+='| `'+s.selector+'` | '+s.action_type+' | '+s.fail_rate+'% |\n'});md+='\n';
669
+ }
670
+ downloadFile('learnings-report.md',md,'text/markdown');
671
+ showToast('Learnings exported','success');
672
+ });
673
+
674
+ /* ── Modal ── */
675
+ function openModal(src){$('#modalImg').src=src;$('#modal').classList.add('open')}
676
+ $('#modal').addEventListener('click',function(){$('#modal').classList.remove('open')});