@matware/e2e-runner 1.2.1 → 1.3.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.
Files changed (82) hide show
  1. package/.claude-plugin/marketplace.json +21 -0
  2. package/.mcp.json +2 -2
  3. package/.opencode/commands/create-test.md +63 -0
  4. package/.opencode/commands/run.md +50 -0
  5. package/.opencode/commands/verify-issue.md +62 -0
  6. package/.opencode/skills/e2e-testing/SKILL.md +181 -0
  7. package/.opencode/skills/e2e-testing/references/action-types.md +143 -0
  8. package/.opencode/skills/e2e-testing/references/auth-strategies.md +91 -0
  9. package/.opencode/skills/e2e-testing/references/graphql.md +59 -0
  10. package/.opencode/skills/e2e-testing/references/issue-verification.md +59 -0
  11. package/.opencode/skills/e2e-testing/references/multi-pool.md +60 -0
  12. package/.opencode/skills/e2e-testing/references/network-debugging.md +62 -0
  13. package/.opencode/skills/e2e-testing/references/test-json-format.md +163 -0
  14. package/.opencode/skills/e2e-testing/references/troubleshooting.md +224 -0
  15. package/.opencode/skills/e2e-testing/references/variables.md +41 -0
  16. package/.opencode/skills/e2e-testing/references/visual-verification.md +89 -0
  17. package/OPENCODE.md +166 -0
  18. package/README.md +581 -55
  19. package/agents/test-creator.md +54 -1
  20. package/agents/test-improver.md +37 -0
  21. package/bin/cli.js +408 -16
  22. package/commands/create-test.md +16 -1
  23. package/opencode.json +11 -0
  24. package/package.json +7 -2
  25. package/scripts/setup-opencode.sh +113 -0
  26. package/skills/e2e-testing/SKILL.md +10 -3
  27. package/skills/e2e-testing/references/action-types.md +48 -5
  28. package/skills/e2e-testing/references/auth-strategies.md +91 -0
  29. package/skills/e2e-testing/references/graphql.md +59 -0
  30. package/skills/e2e-testing/references/issue-verification.md +59 -0
  31. package/skills/e2e-testing/references/multi-pool.md +60 -0
  32. package/skills/e2e-testing/references/network-debugging.md +62 -0
  33. package/skills/e2e-testing/references/test-json-format.md +4 -0
  34. package/skills/e2e-testing/references/troubleshooting.md +44 -2
  35. package/skills/e2e-testing/references/variables.md +41 -0
  36. package/skills/e2e-testing/references/visual-verification.md +89 -0
  37. package/src/actions.js +324 -2
  38. package/src/ai-generate.js +58 -8
  39. package/src/config.js +143 -0
  40. package/src/dashboard.js +145 -13
  41. package/src/db.js +130 -2
  42. package/src/index.js +7 -6
  43. package/src/learner-sqlite.js +304 -0
  44. package/src/learner.js +8 -3
  45. package/src/mcp-tools.js +1121 -43
  46. package/src/module-resolver.js +37 -0
  47. package/src/narrate.js +37 -0
  48. package/src/pool-manager.js +223 -0
  49. package/src/reporter.js +82 -1
  50. package/src/runner.js +157 -28
  51. package/src/sync/auth.js +354 -0
  52. package/src/sync/client.js +572 -0
  53. package/src/sync/hub-routes.js +816 -0
  54. package/src/sync/index.js +68 -0
  55. package/src/sync/middleware.js +347 -0
  56. package/src/sync/queue.js +209 -0
  57. package/src/sync/schema.js +540 -0
  58. package/src/verify.js +10 -7
  59. package/src/watch.js +384 -0
  60. package/templates/build-dashboard.js +47 -6
  61. package/templates/dashboard/js/api.js +60 -0
  62. package/templates/dashboard/js/init.js +13 -0
  63. package/templates/dashboard/js/keyboard.js +46 -0
  64. package/templates/dashboard/js/state.js +40 -0
  65. package/templates/dashboard/js/toast.js +41 -0
  66. package/templates/dashboard/js/utils.js +196 -0
  67. package/templates/dashboard/js/view-live.js +143 -0
  68. package/templates/dashboard/js/view-runs.js +572 -0
  69. package/templates/dashboard/js/view-tests.js +294 -0
  70. package/templates/dashboard/js/view-watch.js +242 -0
  71. package/templates/dashboard/js/websocket.js +110 -0
  72. package/templates/dashboard/styles/base.css +69 -0
  73. package/templates/dashboard/styles/components.css +110 -0
  74. package/templates/dashboard/styles/view-live.css +74 -0
  75. package/templates/dashboard/styles/view-runs.css +207 -0
  76. package/templates/dashboard/styles/view-tests.css +96 -0
  77. package/templates/dashboard/styles/view-watch.css +53 -0
  78. package/templates/dashboard/template.html +165 -99
  79. package/templates/dashboard.html +1596 -541
  80. package/templates/sample-test.json +0 -8
  81. package/templates/dashboard/app.js +0 -1152
  82. package/templates/dashboard/styles.css +0 -413
@@ -0,0 +1,572 @@
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','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 srcTd=document.createElement('td');srcTd.appendChild(createTriggerBadge(r.triggered_by));tr.appendChild(srcTd);
111
+ tr.appendChild(el('td',null,fdate(r.generated_at)));
112
+ tr.appendChild(el('td',null,String(r.total||0)));
113
+ tr.appendChild(el('td',{style:'color:var(--green)'},String(r.passed||0)));
114
+ tr.appendChild(el('td',{style:'color:var(--red)'},String(r.failed||0)));
115
+ var rv=parseFloat(r.pass_rate)||0;
116
+ tr.appendChild(el('td',{style:'font-weight:600;color:'+(rv>=90?'var(--green)':rv>=70?'var(--amber)':'var(--red)')},r.pass_rate||'-'));
117
+ tr.appendChild(el('td',{style:'color:var(--text2)'},r.duration||'-'));
118
+ tr.addEventListener('click',function(){toggleDetail(r.id,tr,colSpan)});
119
+ body.appendChild(tr);
120
+
121
+ var item={tr:tr,data:r,detailTr:null};
122
+ if(r.id===S.selectedRun){
123
+ var detailTr=createDetailRow(colSpan);
124
+ body.appendChild(detailTr);
125
+ loadDetailInline(r.id,detailTr);
126
+ item.detailTr=detailTr;
127
+ }
128
+ _allRunRows.push(item);
129
+ });
130
+ }).catch(function(){});
131
+ }
132
+
133
+ function createDetailRow(colSpan){
134
+ var detailTr=document.createElement('tr');
135
+ detailTr.className='run-detail-row';
136
+ var td=document.createElement('td');
137
+ td.setAttribute('colspan',colSpan);
138
+ var wrap=el('div',{className:'rd-wrap'});
139
+ var inner=el('div',{className:'rd-inner'},[
140
+ el('div',{style:'color:var(--text3);font-size:11px'},[
141
+ el('span',{className:'spinner-small'}),
142
+ document.createTextNode(' Loading...')
143
+ ])
144
+ ]);
145
+ wrap.appendChild(inner);
146
+ td.appendChild(wrap);
147
+ detailTr.appendChild(td);
148
+ return detailTr;
149
+ }
150
+
151
+ function toggleDetail(id,clickedTr,colSpan){
152
+ if(S.selectedRun===id){
153
+ var existing=clickedTr.nextElementSibling;
154
+ if(existing&&existing.classList.contains('run-detail-row')){
155
+ var w=existing.querySelector('.rd-wrap');
156
+ if(w)w.classList.remove('open');
157
+ clickedTr.classList.remove('expanded');
158
+ setTimeout(function(){if(existing.parentNode)existing.parentNode.removeChild(existing)},350);
159
+ }
160
+ S.selectedRun=null;
161
+ return;
162
+ }
163
+
164
+ var prevTr=document.querySelector('#runsBody tr.expanded');
165
+ if(prevTr){
166
+ prevTr.classList.remove('expanded');
167
+ var prevDetail=prevTr.nextElementSibling;
168
+ if(prevDetail&&prevDetail.classList.contains('run-detail-row')){
169
+ var pw=prevDetail.querySelector('.rd-wrap');
170
+ if(pw)pw.classList.remove('open');
171
+ setTimeout(function(){if(prevDetail.parentNode)prevDetail.parentNode.removeChild(prevDetail)},350);
172
+ }
173
+ }
174
+
175
+ S.selectedRun=id;
176
+ clickedTr.classList.add('expanded');
177
+ var detailTr=createDetailRow(colSpan);
178
+ clickedTr.parentNode.insertBefore(detailTr,clickedTr.nextSibling);
179
+ requestAnimationFrame(function(){
180
+ requestAnimationFrame(function(){
181
+ var w2=detailTr.querySelector('.rd-wrap');
182
+ if(w2)w2.classList.add('open');
183
+ });
184
+ });
185
+ loadDetailInline(id,detailTr);
186
+ }
187
+
188
+ /* ── Run Detail ── */
189
+ function loadDetailInline(id,detailTr){
190
+ api('/api/db/runs/'+id).then(function(d){
191
+ if(d.error)return;
192
+ var inner=detailTr.querySelector('.rd-inner');
193
+ inner.textContent='';
194
+ var results=d.results||[];
195
+
196
+ var exportBtn=el('div',null,[
197
+ el('div',{className:'rd-s-label'},'Export'),
198
+ el('div',{style:'margin-top:4px'},[
199
+ el('button',{className:'btn sm',onclick:function(e){
200
+ e.stopPropagation();
201
+ downloadFile('run-'+id+'.json',JSON.stringify(d,null,2),'application/json');
202
+ }},'JSON')
203
+ ])
204
+ ]);
205
+ var srcBlock=el('div',null,[el('div',{className:'rd-s-label'},'Source'),el('div',{style:'margin-top:4px'},[createTriggerBadge(d.triggeredBy)])]);
206
+ var summ=el('div',{className:'rd-summary'},[
207
+ 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')]),
208
+ srcBlock,
209
+ el('div',null,[el('div',{className:'rd-s-label'},'Total'),el('div',{className:'rd-s-val'},String(d.summary.total))]),
210
+ el('div',null,[el('div',{className:'rd-s-label'},'Passed'),el('div',{className:'rd-s-val',style:'color:var(--green)'},String(d.summary.passed))]),
211
+ 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))]),
212
+ 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||'-')]),
213
+ exportBtn
214
+ ]);
215
+ inner.appendChild(summ);
216
+
217
+ // Insights
218
+ var insightsContainer=el('div',{className:'rd-insights'});
219
+ inner.appendChild(insightsContainer);
220
+ fetch('/api/db/runs/'+id+'/insights').then(function(r){return r.json()}).then(function(ins){
221
+ if(!ins||ins.error)return;
222
+ var items=[];
223
+ var h=ins.health;
224
+ if(h){
225
+ var rateColor=h.passRate>=90?'green':h.passRate>=70?'amber':'red';
226
+ var trendIcon=h.passRateTrend==='improving'?'\u25B2':h.passRateTrend==='declining'?'\u25BC':'=';
227
+ var trendCls=h.passRateTrend==='improving'?'green':h.passRateTrend==='declining'?'red':'';
228
+ items.push(el('div',{className:'rd-ins-health'},[
229
+ el('span',{className:'rd-ins-rate '+rateColor},h.passRate+'%'),
230
+ el('span',{className:'rd-ins-trend '+trendCls},trendIcon+' '+h.passRateTrend),
231
+ h.flakyCount>0?el('span',{className:'rd-ins-tag amber'},h.flakyCount+' flaky'):null,
232
+ h.unstableSelectorCount>0?el('span',{className:'rd-ins-tag red'},h.unstableSelectorCount+' unstable sel.'):null
233
+ ]));
234
+ }
235
+ var insights=ins.insights||[];
236
+ insights.forEach(function(i){
237
+ var icon=i.type==='new-failure'?'\u2718':i.type==='recovered'?'\u2714':i.type==='flaky'?'\u223C':'!';
238
+ var cls=i.type==='new-failure'?'red':i.type==='recovered'?'green':i.type==='flaky'?'amber':'';
239
+ items.push(el('div',{className:'rd-ins-item '+cls},[
240
+ el('span',{className:'rd-ins-icon'},icon),
241
+ el('span',null,i.message)
242
+ ]));
243
+ });
244
+ if(items.length>0){items.forEach(function(it){insightsContainer.appendChild(it)})}
245
+ else{insightsContainer.style.display='none'}
246
+ }).catch(function(){insightsContainer.style.display='none'});
247
+
248
+ // Pool distribution bar
249
+ var histPoolTests={};
250
+ results.forEach(function(r){if(!r.poolUrl)return;histPoolTests[r.name]={poolUrl:r.poolUrl,success:r.success}});
251
+ var histPoolDist=buildPoolDistribution(histPoolTests);
252
+ if(histPoolDist)inner.appendChild(histPoolDist);
253
+
254
+ results.forEach(function(r){
255
+ var d2=r.durationMs?dur(r.durationMs):r.endTime&&r.startTime?dur(new Date(r.endTime)-new Date(r.startTime)):'-';
256
+ var flaky=r.success&&r.attempt>1;
257
+ var state=flaky?'flaky':(r.success?'pass':'fail');
258
+
259
+ var badges=el('div',{style:'display:flex;gap:6px;align-items:center;flex-shrink:0'});
260
+ badges.appendChild(el('span',{className:'badge '+(r.success?'pass':'fail')},r.success?'PASS':'FAIL'));
261
+ if(flaky)badges.appendChild(el('span',{className:'badge flaky'},'FLAKY'));
262
+
263
+ var poolEl=r.poolUrl?el('span',{className:'pool-badge'},r.poolUrl.replace('ws://','').replace('wss://','')) :null;
264
+ 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)]);
265
+ var body=el('div',{className:'rd-test-body'});
266
+
267
+ if(r.maxAttempts>1){body.appendChild(el('div',{className:'rd-retries'},'Attempt '+r.attempt+' of '+r.maxAttempts))}
268
+ if(r.error){
269
+ var errDiv=el('div',{className:'rd-error-msg'});
270
+ errDiv.appendChild(document.createTextNode(r.error));
271
+ errDiv.appendChild(makeCopyBtn(r.error));
272
+ body.appendChild(errDiv);
273
+ }
274
+
275
+ // Actions panel
276
+ if(r.actions&&r.actions.length){
277
+ var passCount=r.actions.filter(function(a){return a.success}).length;
278
+ var failCount=r.actions.length-passCount;
279
+ var actHead=el('div',{className:'rd-net-head'},[
280
+ el('span',{className:'net-arrow'},'\u25B6'),
281
+ el('span',{className:'net-title'},'Actions'),
282
+ el('div',{className:'net-stats'},[
283
+ 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
+ ])
286
+ ]);
287
+ var actBody=el('div',{className:'rd-net-body',style:'padding:8px 14px'});
288
+ r.actions.forEach(function(a){
289
+ var label=a.narrative||a.type;
290
+ var durText=a.duration!=null?dur(a.duration):'';
291
+ var retryBadge=null;
292
+ if(a.actionRetries&&a.actionRetries>0){
293
+ retryBadge=el('span',{className:'badge flaky',style:'font-size:9px;padding:1px 5px'},'\u21BB x'+a.actionRetries);
294
+ }
295
+ actBody.appendChild(el('div',{className:'lt-step'},[
296
+ el('span',{className:'step-icon '+(a.success?'ok':'fail')},a.success?'\u2714':'\u2718'),
297
+ el('span',{className:'step-detail',style:'flex:1'},label),
298
+ retryBadge,
299
+ el('span',{className:'step-dur'},durText)
300
+ ]));
301
+ });
302
+ actHead.addEventListener('click',function(){actHead.classList.toggle('open')});
303
+ body.appendChild(el('div',{className:'rd-net-panel'},[actHead,actBody]));
304
+ }
305
+
306
+ // Screenshots
307
+ var shots=[];
308
+ var hashes=r.screenshotHashes||{};
309
+ (r.screenshots||[]).forEach(function(p){shots.push({path:p,label:p.split('/').pop(),type:'screenshot',hash:hashes[p]||null})});
310
+ if(r.errorScreenshot){shots.push({path:r.errorScreenshot,label:r.errorScreenshot.split('/').pop(),type:'error',hash:hashes[r.errorScreenshot]||null})}
311
+ if(shots.length){
312
+ var shotsWrap=el('div',{className:'rd-shots'});
313
+ shots.forEach(function(s){
314
+ var src='/api/image?path='+encodeURIComponent(s.path);
315
+ var img=document.createElement('img');img.src=src;img.alt=s.label;img.loading='lazy';
316
+ var capEl=el('div',{className:'rd-shot-cap'},[el('span',{className:'cap-name'},s.label)]);
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]));
320
+ });
321
+ body.appendChild(shotsWrap);
322
+ }
323
+
324
+ // Console logs
325
+ var cIssues=(r.consoleLogs||[]).filter(function(l){return l.type==='error'||l.type==='warn'||l.type==='warning'});
326
+ if(cIssues.length){
327
+ var cErrors=cIssues.filter(function(l){return l.type==='error'}).length;
328
+ var cWarns=cIssues.length-cErrors;
329
+ var conHead=el('div',{className:'rd-net-head'},[
330
+ el('span',{className:'net-arrow'},'\u25B6'),
331
+ el('span',{className:'net-title'},'Console'),
332
+ el('div',{className:'net-stats'},[
333
+ cErrors?el('span',{className:'net-stat has-err'},[document.createTextNode('Errors: '),el('strong',null,String(cErrors))]):null,
334
+ cWarns?el('span',{className:'net-stat'},[document.createTextNode('Warnings: '),el('strong',null,String(cWarns))]):null
335
+ ]),
336
+ makeCopyBtn(function(){return cIssues.map(function(l){return '['+l.type+'] '+l.text}).join('\n')})
337
+ ]);
338
+ var conBody=el('div',{className:'rd-net-body'});
339
+ cIssues.forEach(function(l){conBody.appendChild(el('div',{className:'rd-log-item '+l.type},'['+l.type+'] '+l.text))});
340
+ conHead.addEventListener('click',function(){conHead.classList.toggle('open')});
341
+ body.appendChild(el('div',{className:'rd-net-panel'},[conHead,conBody]));
342
+ }
343
+
344
+ // Network errors
345
+ if(r.networkErrors&&r.networkErrors.length){
346
+ var neHead=el('div',{className:'rd-net-head'},[
347
+ el('span',{className:'net-arrow'},'\u25B6'),
348
+ el('span',{className:'net-title'},'Network Errors'),
349
+ el('div',{className:'net-stats'},[el('span',{className:'net-stat has-err'},[document.createTextNode('Errors: '),el('strong',null,String(r.networkErrors.length))])]),
350
+ makeCopyBtn(function(){return r.networkErrors.map(function(ne){return '['+ne.error+'] '+ne.url}).join('\n')})
351
+ ]);
352
+ var neBody=el('div',{className:'rd-net-body'});
353
+ r.networkErrors.forEach(function(ne){neBody.appendChild(el('div',{className:'rd-log-item error'},'['+ne.error+'] '+ne.url))});
354
+ neHead.addEventListener('click',function(){neHead.classList.toggle('open')});
355
+ body.appendChild(el('div',{className:'rd-net-panel'},[neHead,neBody]));
356
+ }
357
+
358
+ // Network panel
359
+ if(r.networkLogs&&r.networkLogs.length){
360
+ var errCount=r.networkLogs.filter(function(n){return n.status>=400}).length;
361
+ var netHead=el('div',{className:'rd-net-head'},[
362
+ el('span',{className:'net-arrow'},'\u25B6'),
363
+ el('span',{className:'net-title'},'Network Requests'),
364
+ el('div',{className:'net-stats'},[
365
+ el('span',{className:'net-stat'},[document.createTextNode('Total: '),el('strong',null,String(r.networkLogs.length))]),
366
+ errCount?el('span',{className:'net-stat has-err'},[document.createTextNode('Errors: '),el('strong',null,String(errCount))]):null
367
+ ])
368
+ ]);
369
+ 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')]);
370
+ var netBody=el('div',{className:'rd-net-body'},[netCols]);
371
+ r.networkLogs.forEach(function(n){var built=buildNetRow(n);netBody.appendChild(built.row);if(built.detail)netBody.appendChild(built.detail)});
372
+ netHead.addEventListener('click',function(){netHead.classList.toggle('open')});
373
+ body.appendChild(el('div',{className:'rd-net-panel'},[netHead,netBody]));
374
+ }
375
+
376
+ inner.appendChild(el('div',{className:'rd-test '+state},[head,body]));
377
+ });
378
+
379
+ var w=detailTr.querySelector('.rd-wrap');
380
+ if(w&&!w.classList.contains('open')){requestAnimationFrame(function(){w.classList.add('open')})}
381
+ }).catch(function(){
382
+ var inner=detailTr.querySelector('.rd-inner');
383
+ if(inner)inner.textContent='Failed to load run detail';
384
+ });
385
+ }
386
+
387
+ /* ── Screenshots ── */
388
+ function refreshScreenshots(){
389
+ var gal=$('#screenshotGallery'),empty=$('#screenshotsEmpty');
390
+ gal.textContent='';
391
+ if(!S.project){empty.style.display='block';empty.querySelector('p').textContent='Select a project to view screenshots.';$('#badgeScreenshots').textContent='-';return}
392
+ api('/api/db/projects/'+S.project+'/screenshots').then(function(files){
393
+ if(!Array.isArray(files)||!files.length){empty.style.display='block';empty.querySelector('p').textContent='No screenshots for this project.';$('#badgeScreenshots').textContent='0';return}
394
+ empty.style.display='none';
395
+ $('#badgeScreenshots').textContent=files.length;
396
+ files.forEach(function(f){
397
+ var src='/api/image?path='+encodeURIComponent(f.path);
398
+ var img=document.createElement('img');img.src=src;img.alt=f.name;img.loading='lazy';
399
+ var capEl=el('div',{className:'cap'},[el('span',{className:'cap-name'},f.name)]);
400
+ (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]));
402
+ });
403
+ }).catch(function(){});
404
+ }
405
+
406
+ function searchByHash(){
407
+ var container=$('#ssSearchResult');
408
+ container.textContent='';
409
+ var raw=$('#ssHashInput').value.trim();
410
+ if(!raw)return;
411
+ var hash=raw.replace(/^ss:/,'');
412
+ if(!/^[a-f0-9]{1,8}$/i.test(hash)){
413
+ container.appendChild(el('div',{className:'ss-search-error'},'Invalid hash format. Expected 8 hex characters (e.g. ss:a3f2b1c9).'));
414
+ return;
415
+ }
416
+ fetch('/api/screenshot-hash/'+hash).then(function(res){
417
+ if(!res.ok){container.appendChild(el('div',{className:'ss-search-error'},'Screenshot not found for hash: ss:'+hash));return}
418
+ return res.blob();
419
+ }).then(function(blob){
420
+ if(!blob)return;
421
+ var url=URL.createObjectURL(blob);
422
+ var wrap=el('div',{className:'ss-search-result'},[el('div',{className:'ss-result-label'},[createHashBadge(hash),el('span',{},'Found')])]);
423
+ var img=document.createElement('img');img.src=url;img.alt='ss:'+hash;
424
+ img.addEventListener('click',function(){openModal(url)});
425
+ wrap.appendChild(img);
426
+ container.appendChild(wrap);
427
+ }).catch(function(){container.appendChild(el('div',{className:'ss-search-error'},'Error searching for screenshot.'))});
428
+ }
429
+ $('#ssHashBtn').addEventListener('click',searchByHash);
430
+ $('#ssHashInput').addEventListener('keydown',function(e){if(e.key==='Enter')searchByHash()});
431
+
432
+ /* ── Learnings ── */
433
+ function refreshLearnings(){
434
+ var days=$('#learningsDays').value||30;
435
+ var url=S.project?'/api/db/projects/'+S.project+'/learnings?days='+days:'/api/db/learnings?days='+days;
436
+ fetch(url).then(function(r){return r.json()}).then(function(data){
437
+ if(!data||data.totalRuns===0){
438
+ $('#learningsEmpty').style.display='block';
439
+ $('#learningsOverview').textContent='';$('#learningsTrend').textContent='';
440
+ $('#learningsFlaky').textContent='';$('#learningsSelectors').textContent='';
441
+ $('#learningsPages').textContent='';$('#learningsApis').textContent='';
442
+ $('#learningsErrors').textContent='';
443
+ $('#badgeLearnings').textContent='-';
444
+ return;
445
+ }
446
+ $('#learningsEmpty').style.display='none';
447
+ S.lastLearningsData=data;
448
+ var flakyCount=data.flakyTests?data.flakyTests.length:0;
449
+ var passRate=data.overallPassRate||0;
450
+ var declining=data.recentTrend&&Array.isArray(data.recentTrend.data||data.recentTrend)&&(function(){
451
+ var td=data.recentTrend.data||data.recentTrend;
452
+ if(td.length<2)return false;
453
+ var last=td[td.length-1].pass_rate;
454
+ var prior=td.slice(0,-1).reduce(function(s,t){return s+t.pass_rate},0)/(td.length-1);
455
+ return last-prior<-2;
456
+ })();
457
+ if(passRate<70){
458
+ $('#badgeLearnings').textContent='\u26A0';
459
+ $('#badgeLearnings').style.background='var(--red-dim)';$('#badgeLearnings').style.color='var(--red)';
460
+ } else if(flakyCount>0||declining){
461
+ $('#badgeLearnings').textContent=flakyCount>0?flakyCount:(declining?'\u25BC':'\u2714');
462
+ $('#badgeLearnings').style.background='var(--amber-dim)';$('#badgeLearnings').style.color='var(--amber)';
463
+ } else {
464
+ $('#badgeLearnings').textContent='\u2714';
465
+ $('#badgeLearnings').style.background='var(--green-dim)';$('#badgeLearnings').style.color='var(--green)';
466
+ }
467
+ renderLearnOverview(data);
468
+ renderLearnTrend(data.recentTrend||[]);
469
+ renderLearnFlaky(data.flakyTests||[]);
470
+ renderLearnSelectors(data.unstableSelectors||[]);
471
+ renderLearnPages(data.failingPages||[]);
472
+ renderLearnApis(data.apiIssues||[]);
473
+ renderLearnErrors(data.topErrors||[]);
474
+ }).catch(function(){$('#learningsEmpty').style.display='block'});
475
+ }
476
+
477
+ function renderLearnOverview(d){
478
+ var container=$('#learningsOverview');container.textContent='';
479
+ var grid=document.createElement('div');grid.className='learn-grid';
480
+ [{val:d.totalRuns,lbl:'Runs',cls:'accent'},{val:d.totalTests,lbl:'Tests',cls:'accent'},
481
+ {val:d.overallPassRate+'%',lbl:'Pass Rate',cls:d.overallPassRate>=90?'green':d.overallPassRate>=70?'':'red'},
482
+ {val:d.avgDurationMs<1000?d.avgDurationMs+'ms':(d.avgDurationMs/1000).toFixed(1)+'s',lbl:'Avg Duration',cls:'purple'},
483
+ {val:(d.flakyTests?d.flakyTests.length:0),lbl:'Flaky Tests',cls:d.flakyTests&&d.flakyTests.length>0?'red':'green'},
484
+ {val:(d.unstableSelectors?d.unstableSelectors.length:0),lbl:'Unstable Selectors',cls:d.unstableSelectors&&d.unstableSelectors.length>0?'red':'green'}
485
+ ].forEach(function(item){
486
+ var stat=document.createElement('div');stat.className='learn-stat';
487
+ var valEl=document.createElement('div');valEl.className='learn-stat-val '+item.cls;valEl.textContent=item.val;
488
+ var lblEl=document.createElement('div');lblEl.className='learn-stat-lbl';lblEl.textContent=item.lbl;
489
+ stat.appendChild(valEl);stat.appendChild(lblEl);grid.appendChild(stat);
490
+ });
491
+ container.appendChild(grid);
492
+ }
493
+
494
+ function renderLearnTrend(trend){
495
+ var container=$('#learningsTrend');container.textContent='';
496
+ if(!trend.length)return;
497
+ var card=document.createElement('div');card.className='card';
498
+ var label=document.createElement('div');label.className='card-label';label.textContent='Pass Rate Trend (7 days)';card.appendChild(label);
499
+ var chartDiv=document.createElement('div');chartDiv.className='learn-trend-chart';
500
+ 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');
502
+ 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
+ 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
+ var pts=trend.map(function(t,i){return(i*w+w/2)+','+(100-t.pass_rate)}).join(' ');
505
+ 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
+ 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
+ trend.forEach(function(t,i){
508
+ var circle=document.createElementNS(ns,'circle');circle.setAttribute('cx',''+(i*w+w/2));circle.setAttribute('cy',''+(100-t.pass_rate));circle.setAttribute('r','2');circle.setAttribute('fill','var(--accent)');
509
+ var title=document.createElementNS(ns,'title');title.textContent=t.date+': '+t.pass_rate+'% ('+t.total_tests+' tests)';circle.appendChild(title);svg.appendChild(circle);
510
+ });
511
+ chartDiv.appendChild(svg);card.appendChild(chartDiv);
512
+ var dates=document.createElement('div');dates.style.cssText='display:flex;justify-content:space-between;font-size:10px;color:var(--text3);margin-top:4px';
513
+ dates.appendChild(el('span',null,trend[0].date));dates.appendChild(el('span',null,trend[trend.length-1].date));
514
+ card.appendChild(dates);container.appendChild(card);
515
+ }
516
+
517
+ function buildLearnTable(title,headers,rows){
518
+ var card=document.createElement('div');card.className='card learn-section';
519
+ var h=document.createElement('div');h.className='learn-section-title';h.textContent=title;card.appendChild(h);
520
+ var wrap=document.createElement('div');wrap.className='tbl-wrap';
521
+ var tbl=document.createElement('table');tbl.className='learn-table';
522
+ var thead=document.createElement('thead');var hr=document.createElement('tr');
523
+ headers.forEach(function(hdr){var th=document.createElement('th');th.textContent=hdr;hr.appendChild(th)});
524
+ thead.appendChild(hr);tbl.appendChild(thead);
525
+ var tbody=document.createElement('tbody');
526
+ rows.forEach(function(cells){
527
+ var tr=document.createElement('tr');
528
+ cells.forEach(function(cell){
529
+ var td=document.createElement('td');
530
+ if(cell.code){var code=document.createElement('code');code.textContent=cell.code;td.appendChild(code)}
531
+ else if(cell.badge){var span=document.createElement('span');span.className='badge '+cell.cls;span.textContent=cell.badge;td.appendChild(span)}
532
+ else{td.textContent=cell.text!==undefined&&cell.text!==null?cell.text:(typeof cell==='object'?'-':cell)}
533
+ tr.appendChild(td);
534
+ });
535
+ tbody.appendChild(tr);
536
+ });
537
+ tbl.appendChild(tbody);wrap.appendChild(tbl);card.appendChild(wrap);return card;
538
+ }
539
+
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
+ $('#btnRefreshLearnings').addEventListener('click',refreshLearnings);
547
+ $('#learningsDays').addEventListener('change',refreshLearnings);
548
+
549
+ $('#btnExportLearnings').addEventListener('click',function(){
550
+ var data=S.lastLearningsData;
551
+ if(!data){showToast('No learnings data to export','error');return}
552
+ var md='# E2E Learnings Report\n\n';
553
+ md+='| Metric | Value |\n|--------|-------|\n';
554
+ md+='| Total Runs | '+data.totalRuns+' |\n';
555
+ md+='| Total Tests | '+data.totalTests+' |\n';
556
+ md+='| Pass Rate | '+data.overallPassRate+'% |\n';
557
+ md+='| Avg Duration | '+dur(data.avgDurationMs)+' |\n\n';
558
+ if(data.flakyTests&&data.flakyTests.length){
559
+ md+='## Flaky Tests\n\n| Test | Flaky Rate | Occurrences |\n|------|-----------|-------------|\n';
560
+ data.flakyTests.forEach(function(f){md+='| '+f.test_name+' | '+f.flaky_rate+'% | '+f.flaky_count+' |\n'});md+='\n';
561
+ }
562
+ if(data.unstableSelectors&&data.unstableSelectors.length){
563
+ md+='## Unstable Selectors\n\n| Selector | Action | Fail Rate |\n|----------|--------|-----------|\n';
564
+ data.unstableSelectors.forEach(function(s){md+='| `'+s.selector+'` | '+s.action_type+' | '+s.fail_rate+'% |\n'});md+='\n';
565
+ }
566
+ downloadFile('learnings-report.md',md,'text/markdown');
567
+ showToast('Learnings exported','success');
568
+ });
569
+
570
+ /* ── Modal ── */
571
+ function openModal(src){$('#modalImg').src=src;$('#modal').classList.add('open')}
572
+ $('#modal').addEventListener('click',function(){$('#modal').classList.remove('open')});