@matware/e2e-runner 1.1.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 (89) hide show
  1. package/.claude-plugin/marketplace.json +21 -0
  2. package/.claude-plugin/plugin.json +9 -0
  3. package/.mcp.json +9 -0
  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/OPENCODE.md +166 -0
  19. package/README.md +990 -296
  20. package/agents/test-analyzer.md +81 -0
  21. package/agents/test-creator.md +155 -0
  22. package/agents/test-improver.md +177 -0
  23. package/bin/cli.js +602 -22
  24. package/commands/create-test.md +65 -0
  25. package/commands/run.md +49 -0
  26. package/commands/verify-issue.md +63 -0
  27. package/opencode.json +11 -0
  28. package/package.json +15 -2
  29. package/scripts/setup-opencode.sh +113 -0
  30. package/skills/e2e-testing/SKILL.md +173 -0
  31. package/skills/e2e-testing/references/action-types.md +143 -0
  32. package/skills/e2e-testing/references/auth-strategies.md +91 -0
  33. package/skills/e2e-testing/references/graphql.md +59 -0
  34. package/skills/e2e-testing/references/issue-verification.md +59 -0
  35. package/skills/e2e-testing/references/multi-pool.md +60 -0
  36. package/skills/e2e-testing/references/network-debugging.md +62 -0
  37. package/skills/e2e-testing/references/test-json-format.md +163 -0
  38. package/skills/e2e-testing/references/troubleshooting.md +224 -0
  39. package/skills/e2e-testing/references/variables.md +41 -0
  40. package/skills/e2e-testing/references/visual-verification.md +89 -0
  41. package/src/actions.js +597 -20
  42. package/src/ai-generate.js +142 -12
  43. package/src/config.js +171 -0
  44. package/src/dashboard.js +299 -17
  45. package/src/db.js +335 -13
  46. package/src/index.js +15 -8
  47. package/src/learner-markdown.js +177 -0
  48. package/src/learner-neo4j.js +255 -0
  49. package/src/learner-sqlite.js +658 -0
  50. package/src/learner.js +418 -0
  51. package/src/mcp-tools.js +1558 -50
  52. package/src/module-resolver.js +310 -0
  53. package/src/narrate.js +262 -0
  54. package/src/neo4j-pool.js +124 -0
  55. package/src/pool-manager.js +223 -0
  56. package/src/reporter.js +117 -3
  57. package/src/runner.js +274 -71
  58. package/src/sync/auth.js +354 -0
  59. package/src/sync/client.js +572 -0
  60. package/src/sync/hub-routes.js +816 -0
  61. package/src/sync/index.js +68 -0
  62. package/src/sync/middleware.js +347 -0
  63. package/src/sync/queue.js +209 -0
  64. package/src/sync/schema.js +540 -0
  65. package/src/verify.js +14 -9
  66. package/src/watch.js +384 -0
  67. package/templates/build-dashboard.js +69 -0
  68. package/templates/dashboard/js/api.js +60 -0
  69. package/templates/dashboard/js/init.js +13 -0
  70. package/templates/dashboard/js/keyboard.js +46 -0
  71. package/templates/dashboard/js/state.js +40 -0
  72. package/templates/dashboard/js/toast.js +41 -0
  73. package/templates/dashboard/js/utils.js +196 -0
  74. package/templates/dashboard/js/view-live.js +143 -0
  75. package/templates/dashboard/js/view-runs.js +572 -0
  76. package/templates/dashboard/js/view-tests.js +294 -0
  77. package/templates/dashboard/js/view-watch.js +242 -0
  78. package/templates/dashboard/js/websocket.js +110 -0
  79. package/templates/dashboard/styles/base.css +69 -0
  80. package/templates/dashboard/styles/components.css +110 -0
  81. package/templates/dashboard/styles/view-live.css +74 -0
  82. package/templates/dashboard/styles/view-runs.css +207 -0
  83. package/templates/dashboard/styles/view-tests.css +96 -0
  84. package/templates/dashboard/styles/view-watch.css +53 -0
  85. package/templates/dashboard/template.html +267 -0
  86. package/templates/dashboard.html +2171 -530
  87. package/templates/docker-compose-neo4j.yml +19 -0
  88. package/templates/e2e.config.js +3 -0
  89. package/templates/sample-test.json +0 -8
@@ -0,0 +1,294 @@
1
+ /* ══════════════════════════════════════════════════════════════════
2
+ Tests View — Suites + Modules + Variables (inner tabs)
3
+ ══════════════════════════════════════════════════════════════════ */
4
+ function refreshSuites(){
5
+ var grid=$('#suiteGrid'),empty=$('#suitesEmpty'),accordion=$('#suiteAccordionContainer');
6
+ grid.textContent='';
7
+ var moduleSection=$('#moduleSection');
8
+ moduleSection.textContent='';
9
+
10
+ if(S.project){
11
+ api('/api/db/projects/'+S.project+'/suites').then(function(suites){
12
+ if(!Array.isArray(suites)||suites.length===0){empty.style.display='block';empty.querySelector('p').textContent='No test suites found for this project.';return}
13
+ empty.style.display='none';
14
+ $('#badgeSuites').textContent=suites.length;
15
+ renderSuiteCards(grid,suites,S.project);
16
+ }).catch(function(){});
17
+ api('/api/db/projects/'+S.project+'/modules').then(function(modules){
18
+ renderModules(moduleSection,modules);
19
+ }).catch(function(){});
20
+ } else {
21
+ api('/api/db/projects').then(function(projects){
22
+ if(!Array.isArray(projects)||projects.length===0){empty.style.display='block';empty.querySelector('p').textContent='No projects registered yet.';return}
23
+ var loaded=0,hasAny=false,totalSuites=0;
24
+ projects.forEach(function(p){
25
+ api('/api/db/projects/'+p.id+'/suites').then(function(suites){
26
+ loaded++;
27
+ if(Array.isArray(suites)&&suites.length>0){
28
+ hasAny=true;totalSuites+=suites.length;
29
+ var label=el('div',{style:'grid-column:1/-1;font-family:var(--sans);font-size:13px;font-weight:600;margin-top:'+(grid.children.length?'16':'0')+'px;padding-bottom:6px;border-bottom:1px solid var(--border);color:var(--text2)'},p.name);
30
+ grid.appendChild(label);
31
+ renderSuiteCards(grid,suites,p.id);
32
+ }
33
+ if(loaded===projects.length){
34
+ $('#badgeSuites').textContent=totalSuites;
35
+ if(!hasAny){empty.style.display='block';empty.querySelector('p').textContent='No test suites found.'}
36
+ }
37
+ }).catch(function(){loaded++;});
38
+ });
39
+ }).catch(function(){});
40
+ }
41
+ }
42
+
43
+ function renderProjectAccordion(container,project,suites){
44
+ var totalTests=suites.reduce(function(sum,s){return sum+(s.testCount||0)},0);
45
+ var body=el('div',{className:'project-accordion-body'});
46
+ var innerGrid=el('div',{className:'suite-grid'});
47
+ renderSuiteCards(innerGrid,suites,project.id);
48
+ body.appendChild(innerGrid);
49
+
50
+ var header=el('div',{className:'project-accordion-header'},[
51
+ el('span',{className:'project-accordion-chevron'},'\u25B6'),
52
+ el('span',{className:'project-accordion-name'},project.name),
53
+ el('div',{className:'project-accordion-meta'},[
54
+ el('span',{className:'project-accordion-badge'},suites.length+(suites.length===1?' suite':' suites')),
55
+ el('span',{className:'project-accordion-badge'},totalTests+(totalTests===1?' test':' tests'))
56
+ ])
57
+ ]);
58
+
59
+ var wrapper=el('div',{className:'project-accordion'},[header,body]);
60
+ header.addEventListener('click',function(){wrapper.classList.toggle('open')});
61
+ container.appendChild(wrapper);
62
+ }
63
+
64
+ /* ── Suite Modal ── */
65
+ var _suiteCache={};
66
+
67
+ function openSuiteModal(suiteName,projectId){
68
+ var overlay=$('#suiteModalOverlay');
69
+ var body=$('#suiteModalBody');
70
+ $('#suiteModalName').textContent=suiteName;
71
+ $('#suiteModalFile').textContent=suiteName+'.json';
72
+ body.textContent='';
73
+ body.appendChild(el('div',{className:'suite-modal-loading'},'Loading\u2026'));
74
+ overlay.classList.add('open');
75
+
76
+ $('#suiteModalRun').onclick=function(){triggerRun(suiteName,projectId)};
77
+ $('#suiteModalClose').onclick=function(){overlay.classList.remove('open')};
78
+ overlay.addEventListener('click',function(e){if(e.target===overlay)overlay.classList.remove('open')});
79
+
80
+ var cacheKey=projectId+'::'+suiteName;
81
+ var p=_suiteCache[cacheKey]||api('/api/db/projects/'+projectId+'/suites/'+encodeURIComponent(suiteName));
82
+ _suiteCache[cacheKey]=p;
83
+ p.then(function(data){
84
+ body.textContent='';
85
+ if(!data||!data.tests||!data.tests.length){
86
+ body.appendChild(el('div',{className:'suite-modal-loading'},'No tests found'));
87
+ return;
88
+ }
89
+ data.tests.forEach(function(test){
90
+ var actionsDiv=el('div',{className:'suite-modal-test-actions'});
91
+ (test.actions||[]).forEach(function(a,i){
92
+ var detailContent;
93
+ if(a.selector&&(a.value||a.text)){
94
+ detailContent=[el('span',{className:'step-sel'},a.selector),el('span',{className:'step-arrow'},'\u2192'),el('span',{className:'step-val'},a.text||a.value)];
95
+ } else {
96
+ detailContent=a.selector||a.value||a.text||'';
97
+ }
98
+ actionsDiv.appendChild(el('div',{className:'suite-modal-step'},[
99
+ el('span',{className:'suite-modal-step-num'},String(i+1)),
100
+ el('span',{className:'suite-modal-step-type'},a.type),
101
+ el('span',{className:'suite-modal-step-detail'},detailContent)
102
+ ]));
103
+ });
104
+
105
+ var header=el('div',{className:'suite-modal-test-header'},[
106
+ el('span',{className:'suite-modal-test-chevron'},'\u25B6'),
107
+ el('span',{className:'suite-modal-test-name'},test.name),
108
+ el('span',{className:'suite-modal-test-badge'},(test.actions||[]).length+' actions')
109
+ ]);
110
+
111
+ var testEl=el('div',{className:'suite-modal-test'},[header,actionsDiv]);
112
+ if(test.expect){
113
+ var expectText=Array.isArray(test.expect)?test.expect.join(', '):test.expect;
114
+ var expectEl=el('div',{className:'suite-modal-expect'},[
115
+ el('span',{className:'suite-modal-expect-label'},'Expect:'),
116
+ document.createTextNode(expectText)
117
+ ]);
118
+ testEl.insertBefore(expectEl,actionsDiv);
119
+ }
120
+ header.addEventListener('click',function(){testEl.classList.toggle('open')});
121
+ body.appendChild(testEl);
122
+ });
123
+ }).catch(function(){
124
+ body.textContent='';
125
+ body.appendChild(el('div',{className:'suite-modal-loading',style:'color:var(--red)'},'Failed to load suite'));
126
+ });
127
+ }
128
+
129
+ function renderSuiteCards(container,suites,projectId){
130
+ suites.forEach(function(s){
131
+ var tests=el('ul',{className:'suite-card-tests'});
132
+ var pid=projectId;
133
+ (s.tests||[]).forEach(function(t){
134
+ var li=el('li',null,t);
135
+ li.addEventListener('click',function(e){
136
+ e.stopPropagation();
137
+ var existing=li.querySelector('.suite-test-steps');
138
+ if(existing){existing.remove();li.classList.remove('expanded');return}
139
+ tests.querySelectorAll('.suite-test-steps').forEach(function(d){d.remove()});
140
+ tests.querySelectorAll('li.expanded').forEach(function(l){l.classList.remove('expanded')});
141
+ var stepsDiv=el('div',{className:'suite-test-steps'});
142
+ stepsDiv.appendChild(el('div',{style:'color:var(--text3);font-size:10px'},'Loading...'));
143
+ li.appendChild(stepsDiv);
144
+ li.classList.add('expanded');
145
+ var cacheKey=pid+'::'+s.name;
146
+ var p=_suiteCache[cacheKey]||api('/api/db/projects/'+pid+'/suites/'+encodeURIComponent(s.name));
147
+ _suiteCache[cacheKey]=p;
148
+ p.then(function(data){
149
+ stepsDiv.textContent='';
150
+ var test=(data.tests||[]).find(function(x){return x.name===t});
151
+ if(!test||!test.actions||!test.actions.length){
152
+ stepsDiv.appendChild(el('div',{style:'color:var(--text3);font-size:10px'},'No actions'));
153
+ return;
154
+ }
155
+ if(test.serial){
156
+ var sb=el('span',{className:'serial-badge'},'Serial');
157
+ li.insertBefore(sb,li.querySelector('.suite-test-steps'));
158
+ }
159
+ test.actions.forEach(function(a,i){
160
+ var detail=a.selector||a.value||a.text||'';
161
+ if(a.selector&&(a.value||a.text))detail=a.selector+' \u2192 '+(a.text||a.value);
162
+ stepsDiv.appendChild(el('div',{className:'lt-step'},[
163
+ el('span',{className:'step-icon',style:'color:var(--text3)'},String(i+1)),
164
+ el('span',{className:'step-type'},a.type),
165
+ el('span',{className:'step-detail'},detail)
166
+ ]));
167
+ });
168
+ }).catch(function(){
169
+ stepsDiv.textContent='';
170
+ stepsDiv.appendChild(el('div',{style:'color:var(--red);font-size:10px'},'Failed to load'));
171
+ });
172
+ });
173
+ tests.appendChild(li);
174
+ });
175
+
176
+ var cardHead=el('div',{className:'suite-card-head',style:'cursor:pointer'},[
177
+ el('div',{className:'suite-card-icon'},'\u25B6'),
178
+ el('div',{className:'suite-card-info'},[
179
+ el('div',{className:'suite-card-name'},s.name),
180
+ el('div',{className:'suite-card-file'},s.file||s.name+'.json')
181
+ ]),
182
+ el('div',{className:'suite-card-count'},[
183
+ el('div',{className:'suite-card-count-num'},String(s.testCount||0)),
184
+ el('div',{className:'suite-card-count-lbl'},'tests')
185
+ ])
186
+ ]);
187
+ (function(name,projId){
188
+ cardHead.addEventListener('click',function(){openSuiteModal(name,projId)});
189
+ })(s.name,pid);
190
+
191
+ var card=el('div',{className:'suite-card'},[
192
+ cardHead,
193
+ el('div',{className:'suite-card-body'},[tests]),
194
+ el('div',{className:'suite-card-footer'},[
195
+ el('button',{className:'btn sm primary',onclick:function(){triggerRun(s.name,pid)}},'Run Suite')
196
+ ])
197
+ ]);
198
+ container.appendChild(card);
199
+ });
200
+ }
201
+
202
+ function renderModules(container,modules){
203
+ if(!Array.isArray(modules)||modules.length===0)return;
204
+ var title=el('div',{className:'module-section-title'},[
205
+ el('span',{className:'mod-icon'},'\u{1F9E9}'),
206
+ document.createTextNode(' Reusable Modules ('+modules.length+')')
207
+ ]);
208
+ container.appendChild(title);
209
+ var grid=el('div',{className:'module-grid'});
210
+ modules.forEach(function(m){
211
+ var paramsEl=null;
212
+ if(m.params&&m.params.length){
213
+ var items=m.params.map(function(p){return el('li',null,typeof p==='string'?p:(p.name||String(p)))});
214
+ paramsEl=el('ul',{className:'module-card-params'},items);
215
+ }
216
+ var card=el('div',{className:'module-card'},[
217
+ el('div',{className:'module-card-name'},m.name),
218
+ m.description?el('div',{className:'module-card-desc'},m.description):null,
219
+ el('div',{className:'module-card-meta'},[
220
+ el('span',null,m.actionCount+' actions'),
221
+ m.params&&m.params.length?el('span',null,m.params.length+' params'):null
222
+ ]),
223
+ paramsEl
224
+ ]);
225
+ grid.appendChild(card);
226
+ });
227
+ container.appendChild(grid);
228
+ }
229
+
230
+ /* ── Variables ── */
231
+ function refreshVariables(){
232
+ var container=$('#variablesContainer'),empty=$('#variablesEmpty');
233
+ container.textContent='';
234
+ if(!S.project){empty.style.display='block';empty.querySelector('p').textContent='Select a project to manage variables.';return}
235
+ api('/api/db/projects/'+S.project+'/variables').then(function(vars){
236
+ if(!Array.isArray(vars)||!vars.length){empty.style.display='block';empty.querySelector('p').textContent='No variables set. Add variables to use {{var.KEY}} in your tests.';return}
237
+ empty.style.display='none';
238
+ renderVariables(vars);
239
+ }).catch(function(){empty.style.display='block'});
240
+ }
241
+
242
+ function renderVariables(vars){
243
+ var container=$('#variablesContainer');
244
+ var tbl=el('table',{className:'var-table'});
245
+ var thead=document.createElement('thead');
246
+ var hr=document.createElement('tr');
247
+ ['Key','Value','Scope','Actions'].forEach(function(h){hr.appendChild(el('th',null,h))});
248
+ thead.appendChild(hr);tbl.appendChild(thead);
249
+ var tbody=document.createElement('tbody');
250
+ vars.forEach(function(v){
251
+ var tr=document.createElement('tr');
252
+ tr.appendChild(el('td',null,[el('code',null,v.key)]));
253
+ tr.appendChild(el('td',{style:'max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'},v.is_secret?'\u2022\u2022\u2022\u2022\u2022\u2022':v.value));
254
+ tr.appendChild(el('td',{style:'color:var(--text3)'},v.scope||'project'));
255
+ var delBtn=el('button',{className:'btn sm danger',onclick:function(){
256
+ if(!confirm('Delete variable "'+v.key+'"?'))return;
257
+ fetch('/api/db/projects/'+S.project+'/variables/'+encodeURIComponent(v.key),{method:'DELETE'}).then(function(){refreshVariables();showToast('Variable deleted','success')}).catch(function(){showToast('Delete failed','error')});
258
+ }},'\u2715');
259
+ tr.appendChild(el('td',null,[delBtn]));
260
+ tbody.appendChild(tr);
261
+ });
262
+ tbl.appendChild(tbody);
263
+ container.appendChild(tbl);
264
+ }
265
+
266
+ /* ── Variable Add Form ── */
267
+ $('#btnAddVar').addEventListener('click',function(){
268
+ var form=$('#varAddForm');
269
+ if(form.style.display==='none'){
270
+ form.style.display='';
271
+ form.textContent='';
272
+ var keyInput=el('input',{type:'text',placeholder:'KEY',style:'margin-right:8px;width:120px'});
273
+ var valInput=el('input',{type:'text',placeholder:'Value',style:'margin-right:8px;width:200px'});
274
+ var secretCheck=el('input',{type:'checkbox',style:'margin-right:4px'});
275
+ var saveBtn=el('button',{className:'btn sm primary',onclick:function(){
276
+ var k=keyInput.value.trim(),v=valInput.value;
277
+ if(!k){showToast('Key is required','error');return}
278
+ if(!S.project){showToast('Select a project first','error');return}
279
+ fetch('/api/db/projects/'+S.project+'/variables',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key:k,value:v,is_secret:secretCheck.checked})}).then(function(r){return r.json()}).then(function(){
280
+ form.style.display='none';refreshVariables();showToast('Variable saved','success');
281
+ }).catch(function(){showToast('Save failed','error')});
282
+ }},'Save');
283
+ var cancelBtn=el('button',{className:'btn sm',onclick:function(){form.style.display='none'}},'Cancel');
284
+ form.appendChild(el('div',{className:'var-add-form',style:'display:flex;align-items:center;gap:8px;flex-wrap:wrap'},[
285
+ keyInput,valInput,
286
+ el('label',{style:'font-size:11px;color:var(--text2);display:flex;align-items:center;gap:4px'},[secretCheck,document.createTextNode('Secret')]),
287
+ saveBtn,cancelBtn
288
+ ]));
289
+ } else {
290
+ form.style.display='none';
291
+ }
292
+ });
293
+
294
+ $('#btnRunAll').addEventListener('click',function(){triggerRun()});
@@ -0,0 +1,242 @@
1
+ /* ══════════════════════════════════════════════════════════════════
2
+ Watch View — Project Cards + Sparklines + Event Log
3
+ ══════════════════════════════════════════════════════════════════ */
4
+ var _watchInterval=null;
5
+ var _countdownInterval=null;
6
+ var _watchData=null;
7
+
8
+ function refreshWatch(){
9
+ // Fetch projects overview (sparklines)
10
+ api('/api/db/projects/overview').then(function(projects){
11
+ if(!Array.isArray(projects)||!projects.length){
12
+ $('#watchCards').textContent='';
13
+ $('#watchEmpty').style.display='block';
14
+ return;
15
+ }
16
+ $('#watchEmpty').style.display='none';
17
+ _watchData=projects;
18
+ renderWatchCards(projects);
19
+ }).catch(function(){
20
+ // Fallback: use regular projects list
21
+ api('/api/db/projects').then(function(projects){
22
+ if(!Array.isArray(projects)||!projects.length){$('#watchEmpty').style.display='block';return}
23
+ $('#watchEmpty').style.display='none';
24
+ _watchData=projects.map(function(p){return Object.assign({},p,{sparkline:[]})});
25
+ renderWatchCards(_watchData);
26
+ }).catch(function(){});
27
+ });
28
+
29
+ // Fetch event log (recent runs)
30
+ var runsUrl=S.project?'/api/db/projects/'+S.project+'/runs':'/api/db/runs';
31
+ api(runsUrl).then(function(runs){
32
+ renderEventLog(runs);
33
+ }).catch(function(){});
34
+
35
+ // Fetch watch jobs status for countdown
36
+ fetch('/api/watch/status').then(function(r){
37
+ if(!r.ok)throw new Error('not running');
38
+ return r.json();
39
+ }).then(function(jobs){
40
+ applyWatchJobData(jobs);
41
+ }).catch(function(){
42
+ // Watch engine not running — that's fine, cards still show
43
+ });
44
+ }
45
+
46
+ function renderWatchCards(projects){
47
+ var container=$('#watchCards');
48
+ container.textContent='';
49
+
50
+ projects.forEach(function(p){
51
+ var sparkline=p.sparkline||[];
52
+ var lastRate=sparkline.length?sparkline[sparkline.length-1]:null;
53
+ var rateColor=lastRate===null?'dim':lastRate>=90?'green':lastRate>=70?'amber':'red';
54
+ var dotColor=rateColor;
55
+
56
+ var sparkEl=el('div',{className:'watch-sparkline'});
57
+ if(sparkline.length>=2){
58
+ sparkEl.appendChild(buildSparkline(sparkline));
59
+ } else {
60
+ sparkEl.style.cssText='height:40px;display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:10px';
61
+ sparkEl.textContent=sparkline.length?'1 run':'No runs yet';
62
+ }
63
+
64
+ var triggerBtn=el('button',{className:'btn sm',onclick:function(e){e.stopPropagation();triggerRun(null,p.id)}},'\u25B6');
65
+ var detailBtn=el('button',{className:'btn sm',onclick:function(e){
66
+ e.stopPropagation();
67
+ S.project=p.id;$('#projectSelect').value=p.id;
68
+ showView('runs');
69
+ refreshRuns();refreshSuites();
70
+ }},'\uD83D\uDD0D');
71
+
72
+ var card=el('div',{className:'watch-card',id:'watch-card-'+p.id},[
73
+ el('div',{className:'watch-card-header'},[
74
+ el('div',{className:'watch-card-name'},p.name),
75
+ el('div',{className:'watch-card-icons'},[triggerBtn,detailBtn])
76
+ ]),
77
+ sparkEl,
78
+ el('div',{className:'watch-card-footer'},[
79
+ el('div',{className:'watch-card-status'},[
80
+ el('span',{className:'status-dot '+dotColor}),
81
+ el('span',{className:'watch-card-rate '+rateColor},lastRate!==null?lastRate+'%':'—')
82
+ ]),
83
+ el('span',{style:'color:var(--text3);font-size:10px'},p.runCount?p.runCount+' runs':'')
84
+ ]),
85
+ el('div',{className:'watch-card-meta'},[
86
+ el('span',{className:'watch-card-countdown',id:'watch-countdown-'+p.id},''),
87
+ p.lastCommit?el('span',{className:'watch-card-commit'},'\u{1F4CB} '+p.lastCommit.slice(0,8)):null
88
+ ])
89
+ ]);
90
+
91
+ container.appendChild(card);
92
+ });
93
+ }
94
+
95
+ function buildSparkline(data){
96
+ var ns='http://www.w3.org/2000/svg';
97
+ var svg=document.createElementNS(ns,'svg');
98
+ svg.setAttribute('viewBox','0 0 200 40');
99
+ svg.setAttribute('preserveAspectRatio','none');
100
+
101
+ var n=data.length;
102
+ var w=200/(n-1||1);
103
+ var pts=data.map(function(v,i){return (i*w)+','+(40-v*0.4)}).join(' ');
104
+
105
+ // Gradient fill
106
+ var poly=document.createElementNS(ns,'polygon');
107
+ poly.setAttribute('points','0,40 '+pts+' '+((n-1)*w)+',40');
108
+ poly.setAttribute('fill','var(--accent-dim)');
109
+ svg.appendChild(poly);
110
+
111
+ // Line
112
+ var pl=document.createElementNS(ns,'polyline');
113
+ pl.setAttribute('points',pts);
114
+ pl.setAttribute('fill','none');
115
+ pl.setAttribute('stroke','var(--accent)');
116
+ pl.setAttribute('stroke-width','1.5');
117
+ svg.appendChild(pl);
118
+
119
+ // End dot
120
+ if(n>0){
121
+ var lastVal=data[n-1];
122
+ var dotColor=lastVal>=90?'var(--green)':lastVal>=70?'var(--amber)':'var(--red)';
123
+ var circle=document.createElementNS(ns,'circle');
124
+ circle.setAttribute('cx',''+(n-1)*w);
125
+ circle.setAttribute('cy',''+(40-lastVal*0.4));
126
+ circle.setAttribute('r','3');
127
+ circle.setAttribute('fill',dotColor);
128
+ svg.appendChild(circle);
129
+ }
130
+
131
+ return svg;
132
+ }
133
+
134
+ function applyWatchJobData(jobs){
135
+ if(!jobs||!jobs.length)return;
136
+ jobs.forEach(function(j){
137
+ // Find matching card by project name
138
+ if(!_watchData)return;
139
+ var match=_watchData.find(function(p){return p.name===j.name||p.cwd===j.cwd});
140
+ if(!match)return;
141
+ var cdEl=$('#watch-countdown-'+match.id);
142
+ if(cdEl&&j.nextRunAt){
143
+ cdEl.dataset.nextRunAt=j.nextRunAt;
144
+ updateCountdown(cdEl);
145
+ }
146
+ });
147
+ startCountdownTimer();
148
+ }
149
+
150
+ function startCountdownTimer(){
151
+ if(_countdownInterval)return;
152
+ _countdownInterval=setInterval(function(){
153
+ $$('.watch-card-countdown[data-next-run-at]').forEach(updateCountdown);
154
+ },1000);
155
+ }
156
+
157
+ function updateCountdown(cdEl){
158
+ var next=cdEl.dataset.nextRunAt;
159
+ if(!next){cdEl.textContent='';return}
160
+ var diff=new Date(next)-Date.now();
161
+ if(diff<=0){cdEl.textContent='\u23F1 Running...';return}
162
+ var m=Math.floor(diff/60000);
163
+ var s=Math.floor((diff%60000)/1000);
164
+ cdEl.textContent='\u23F1 Next: '+m+'m '+String(s).padStart(2,'0')+'s';
165
+ }
166
+
167
+ function renderEventLog(runs){
168
+ var container=$('#watchEventLog');
169
+ if(!container)return;
170
+ container.textContent='';
171
+
172
+ if(!Array.isArray(runs)||!runs.length){
173
+ container.appendChild(el('div',{style:'padding:16px;text-align:center;color:var(--text3);font-size:11px'},'No runs recorded yet.'));
174
+ return;
175
+ }
176
+
177
+ // Column header row
178
+ container.appendChild(el('div',{className:'watch-event-row we-header'},[
179
+ el('span',null,'Time'),
180
+ el('span',null,'Project'),
181
+ el('span',null,'Suite'),
182
+ el('span',{style:'justify-self:center'},'Status'),
183
+ el('span',{style:'text-align:center'},'Tests'),
184
+ el('span',{style:'text-align:right'},'Rate'),
185
+ el('span',{style:'text-align:right'},'Duration'),
186
+ el('span',{style:'text-align:right'},'Source')
187
+ ]));
188
+
189
+ var recent=runs.slice(0,30);
190
+ recent.forEach(function(r){
191
+ var rate=parseFloat(r.pass_rate)||0;
192
+ var badgeCls=r.failed>0?'fail':'pass';
193
+ var badgeText=r.failed>0?'FAIL':'PASS';
194
+
195
+ // Test counts: "5/5" or "3/5 (2 fail)"
196
+ var countsText=r.passed+'/'+r.total;
197
+ var countsParts=[el('span',{className:'we-counts-ok'},String(r.passed))];
198
+ countsParts.push(document.createTextNode('/'+r.total));
199
+ if(r.failed>0){
200
+ countsParts.push(document.createTextNode(' ('));
201
+ countsParts.push(el('span',{style:'color:var(--red)'},r.failed+' fail'));
202
+ countsParts.push(document.createTextNode(')'));
203
+ }
204
+
205
+ // Trigger badge
206
+ var triggerIcon={'cli':'\u2318','dashboard':'\uD83D\uDCBB','mcp':'\u2699','watch':'\u23F1','api':'\u26A1'};
207
+ var trigSrc=r.triggered_by||'cli';
208
+ var trigEl=el('span',{className:'we-trigger',title:'Triggered by: '+trigSrc},(triggerIcon[trigSrc]||'\u2318')+' '+trigSrc);
209
+
210
+ var row=el('div',{className:'watch-event-row',style:'cursor:pointer'},[
211
+ el('span',{className:'watch-event-time'},fdate(r.generated_at)),
212
+ el('span',{className:'watch-event-project'},r.project_name||'—'),
213
+ el('span',{className:'watch-event-suite'},r.suite_name||'all'),
214
+ el('span',{className:'watch-event-result'},[el('span',{className:'badge '+badgeCls},badgeText)]),
215
+ el('span',{className:'watch-event-counts'},countsParts),
216
+ el('span',{className:'watch-event-rate'},rate>0?rate.toFixed(0)+'%':'—'),
217
+ el('span',{className:'watch-event-duration'},r.duration?dur(r.duration):'—'),
218
+ trigEl
219
+ ]);
220
+
221
+ // Click to navigate to run detail
222
+ (function(run){
223
+ row.addEventListener('click',function(){
224
+ S.project=run.project_id;$('#projectSelect').value=run.project_id;
225
+ showView('runs');
226
+ refreshRuns();
227
+ });
228
+ })(r);
229
+
230
+ container.appendChild(row);
231
+ });
232
+ }
233
+
234
+ function startWatchPolling(){
235
+ if(_watchInterval)return;
236
+ refreshWatch();
237
+ _watchInterval=setInterval(refreshWatch,10000);
238
+ }
239
+ function stopWatchPolling(){
240
+ if(_watchInterval){clearInterval(_watchInterval);_watchInterval=null}
241
+ if(_countdownInterval){clearInterval(_countdownInterval);_countdownInterval=null}
242
+ }
@@ -0,0 +1,110 @@
1
+ /* ── WebSocket ── */
2
+ function connectWS(){
3
+ var proto=location.protocol==='https:'?'wss:':'ws:';
4
+ S.ws=new WebSocket(proto+'//'+location.host);
5
+ S.ws.onopen=function(){
6
+ $('#wsDot').style.background='var(--green)';$('#wsLabel').textContent='ws: connected';$('#wsLabel').style.color='var(--green)';
7
+ showToast('WebSocket connected','info');
8
+ };
9
+ S.ws.onclose=function(){
10
+ $('#wsDot').style.background='var(--red)';$('#wsLabel').textContent='ws: disconnected';$('#wsLabel').style.color='var(--text3)';
11
+ setTimeout(connectWS,3000);
12
+ };
13
+ S.ws.onerror=function(){};
14
+ S.ws.onmessage=function(e){try{handleWS(JSON.parse(e.data))}catch(x){}};
15
+ }
16
+
17
+ function getLiveRun(m){
18
+ var rid=m.runId;if(!rid)return null;
19
+ if(!S.liveRuns[rid])S.liveRuns[rid]={on:true,done:false,total:0,completed:0,passed:0,failed:0,active:0,tests:{},project:m.project||null,cwd:m.cwd||null,triggeredBy:m.triggeredBy||null,runId:rid,_lastEvent:Date.now()};
20
+ S.liveRuns[rid]._lastEvent=Date.now();
21
+ return S.liveRuns[rid];
22
+ }
23
+ function anyLiveRunning(){for(var k in S.liveRuns)if(S.liveRuns[k].on)return true;return false}
24
+
25
+ setInterval(function(){
26
+ var changed=false;
27
+ for(var k in S.liveRuns){
28
+ var r=S.liveRuns[k];
29
+ var age=Date.now()-r._lastEvent;
30
+ if(r.on&&!r.done){
31
+ if(r.total===0&&age>10000){r.on=false;r.done=true;r.stale=true;r.active=0;changed=true}
32
+ else if(r.completed>=r.total&&r.total>0&&age>15000){r.on=false;r.done=true;r.active=0;changed=true}
33
+ else if(age>30000){r.on=false;r.done=true;r.stale=true;r.active=0;changed=true}
34
+ }
35
+ if(r.done&&r.stale&&r.total===0&&age>15000){delete S.liveRuns[k];changed=true}
36
+ else if(r.done&&age>120000){delete S.liveRuns[k];changed=true}
37
+ }
38
+ if(changed)renderLive();
39
+ },5000);
40
+
41
+ function handleWS(m){
42
+ switch(m.event){
43
+ case 'pool:status':renderPool(m.data);break;
44
+ case 'run:start':
45
+ for(var dk in S.liveRuns){if(S.liveRuns[dk].done)delete S.liveRuns[dk]}
46
+ var r=getLiveRun(m);
47
+ r.total=m.total;r.on=true;r.done=false;
48
+ S.liveCollapsed=new Set();S.liveSSOpen=new Set();
49
+ showView('live');renderLive();break;
50
+ case 'test:start':
51
+ var r2=getLiveRun(m);if(!r2)break;
52
+ r2.active=m.activeCount;
53
+ r2.tests[m.name]={status:'running',actions:0,totalActions:0,error:null,actionLog:[],screenshots:[],serial:m.serial||false};
54
+ renderLive();break;
55
+ case 'test:pool':
56
+ var rp=getLiveRun(m);if(!rp||!rp.tests[m.name])break;
57
+ rp.tests[m.name].poolUrl=m.poolUrl||null;
58
+ rp.tests[m.name].actionLog.unshift({type:'pool',narrative:'\uD83D\uDD17 '+m.name+' \u2192 '+(m.poolUrl||'').replace('ws://','').replace('wss://',''),success:true,duration:null,isPoolLog:true});
59
+ renderLive();break;
60
+ case 'test:action':
61
+ var r3=getLiveRun(m);if(!r3||!r3.tests[m.name])break;
62
+ var t=r3.tests[m.name];
63
+ t.actions=m.actionIndex+1;t.totalActions=m.totalActions;t.actionType=m.action.type;
64
+ t.actionLog.push({type:m.action.type,selector:m.action.selector||null,value:m.action.value||null,text:m.action.text||null,success:m.success,duration:m.duration,error:m.error||null,narrative:m.narrative||null,actionRetries:m.action.retries||0});
65
+ if(m.screenshotPath)t.screenshots.push(m.screenshotPath);
66
+ renderLive();break;
67
+ case 'test:retry':
68
+ var r4=getLiveRun(m);if(!r4||!r4.tests[m.name])break;
69
+ r4.tests[m.name].retry=m.attempt+'/'+m.maxAttempts;
70
+ renderLive();break;
71
+ case 'test:complete':
72
+ var r5=getLiveRun(m);if(!r5)break;
73
+ r5.completed++;
74
+ if(m.success){r5.passed++;if(r5.tests[m.name])r5.tests[m.name].status='passed'}
75
+ else{r5.failed++;if(r5.tests[m.name]){r5.tests[m.name].status='failed';r5.tests[m.name].error=m.error}}
76
+ if(r5.tests[m.name]){
77
+ r5.tests[m.name].duration=m.duration;
78
+ if(m.screenshots&&m.screenshots.length)r5.tests[m.name].screenshots=m.screenshots;
79
+ if(m.errorScreenshot)r5.tests[m.name].errorScreenshot=m.errorScreenshot;
80
+ if(m.networkLogs&&m.networkLogs.length)r5.tests[m.name].networkLogs=m.networkLogs;
81
+ if(m.poolUrl)r5.tests[m.name].poolUrl=m.poolUrl;
82
+ }
83
+ r5.active=Math.max(0,r5.active-1);
84
+ renderLive();break;
85
+ case 'run:complete':
86
+ var r6=getLiveRun(m);if(r6){r6.on=false;r6.done=true;r6.active=0}
87
+ var summary=m.summary||{};
88
+ var baseMsg='Run complete: '+(summary.failed>0?summary.failed+' failed':'all '+(summary.total||0)+' passed');
89
+ var baseType=summary.failed>0?'error':'success';
90
+ var healthUrl=S.project?'/api/db/projects/'+S.project+'/health':'/api/db/health';
91
+ fetch(healthUrl).then(function(r){return r.json()}).then(function(h){
92
+ if(h&&h.passRate!==undefined){
93
+ var extra='. Pass rate: '+h.passRate+'%';
94
+ if(h.passRateTrend==='declining')extra+=' (declining, '+h.trendDelta+'%)';
95
+ else if(h.passRateTrend==='improving')extra+=' (improving, +'+h.trendDelta+'%)';
96
+ if(h.flakyCount>0)extra+='. '+h.flakyCount+' flaky test(s)';
97
+ showEnrichedToast(baseMsg+extra,baseType);
98
+ } else {
99
+ showToast(baseMsg,baseType);
100
+ }
101
+ }).catch(function(){showToast(baseMsg,baseType)});
102
+ renderLive();refreshRuns();refreshProjects();refreshWatch();break;
103
+ case 'run:error':
104
+ var r7=getLiveRun(m);if(r7){r7.on=false;r7.done=true;r7.tests.__error={status:'failed',error:m.error}}
105
+ showToast('Run error: '+m.error,'error');
106
+ renderLive();break;
107
+ case 'db:updated':
108
+ refreshRuns();refreshProjects();refreshScreenshots();refreshLearnings();refreshWatch();break;
109
+ }
110
+ }