@matware/e2e-runner 1.3.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/LICENSE +1 -1
  4. package/README.md +491 -225
  5. package/agents/test-creator.md +4 -2
  6. package/agents/test-improver.md +7 -4
  7. package/bin/cli.js +93 -19
  8. package/package.json +4 -3
  9. package/skills/e2e-testing/SKILL.md +5 -3
  10. package/skills/e2e-testing/references/action-types.md +35 -18
  11. package/skills/e2e-testing/references/test-json-format.md +23 -0
  12. package/skills/e2e-testing/references/troubleshooting.md +2 -26
  13. package/src/actions.js +181 -15
  14. package/src/config.js +6 -0
  15. package/src/dashboard.js +185 -9
  16. package/src/db.js +26 -0
  17. package/src/mcp-tools.js +238 -69
  18. package/src/module-analysis.js +247 -0
  19. package/src/module-resolver.js +35 -2
  20. package/src/narrate.js +33 -1
  21. package/src/pool-manager.js +46 -1
  22. package/src/pool.js +177 -20
  23. package/src/runner.js +144 -19
  24. package/src/visual-diff.js +74 -4
  25. package/src/websocket.js +14 -3
  26. package/src/wizard.js +184 -0
  27. package/templates/build-dashboard.js +3 -0
  28. package/templates/dashboard/js/api.js +60 -3
  29. package/templates/dashboard/js/init.js +46 -0
  30. package/templates/dashboard/js/keyboard.js +8 -7
  31. package/templates/dashboard/js/quicksearch.js +277 -0
  32. package/templates/dashboard/js/state.js +61 -7
  33. package/templates/dashboard/js/toast.js +1 -1
  34. package/templates/dashboard/js/utils.js +23 -2
  35. package/templates/dashboard/js/view-live.js +235 -42
  36. package/templates/dashboard/js/view-runs.js +469 -42
  37. package/templates/dashboard/js/view-tests.js +157 -16
  38. package/templates/dashboard/js/view-tools.js +234 -0
  39. package/templates/dashboard/js/view-watch.js +2 -2
  40. package/templates/dashboard/js/websocket.js +33 -3
  41. package/templates/dashboard/styles/base.css +489 -53
  42. package/templates/dashboard/styles/components.css +736 -84
  43. package/templates/dashboard/styles/view-live.css +459 -78
  44. package/templates/dashboard/styles/view-runs.css +826 -177
  45. package/templates/dashboard/styles/view-tests.css +440 -77
  46. package/templates/dashboard/styles/view-tools.css +206 -0
  47. package/templates/dashboard/styles/view-watch.css +198 -41
  48. package/templates/dashboard/template.html +356 -58
  49. package/templates/dashboard.html +5354 -722
  50. package/templates/docker-compose-lightpanda.yml +7 -0
@@ -4,43 +4,67 @@
4
4
  function refreshSuites(){
5
5
  var grid=$('#suiteGrid'),empty=$('#suitesEmpty'),accordion=$('#suiteAccordionContainer');
6
6
  grid.textContent='';
7
+ accordion.textContent='';
7
8
  var moduleSection=$('#moduleSection');
8
9
  moduleSection.textContent='';
10
+ var toolbar=$('#suitesToolbar');
9
11
 
10
12
  if(S.project){
13
+ // Keep the toolbar visible so users can still search suites within a
14
+ // single project — only the expand/collapse buttons are dropped since
15
+ // there are no project accordions to expand in single-project view.
16
+ if(toolbar){
17
+ toolbar.style.display='';
18
+ toolbar.classList.add('single-project');
19
+ }
11
20
  api('/api/db/projects/'+S.project+'/suites').then(function(suites){
12
21
  if(!Array.isArray(suites)||suites.length===0){empty.style.display='block';empty.querySelector('p').textContent='No test suites found for this project.';return}
13
22
  empty.style.display='none';
14
23
  $('#badgeSuites').textContent=suites.length;
15
24
  renderSuiteCards(grid,suites,S.project);
25
+ applyTestsSearch();
16
26
  }).catch(function(){});
17
27
  api('/api/db/projects/'+S.project+'/modules').then(function(modules){
18
28
  renderModules(moduleSection,modules);
29
+ applyTestsSearch();
19
30
  }).catch(function(){});
20
31
  } else {
32
+ if(toolbar){toolbar.style.display='';toolbar.classList.remove('single-project')}
21
33
  api('/api/db/projects').then(function(projects){
22
34
  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){
35
+ var sorted=projects.slice().sort(function(a,b){return (a.name||'').localeCompare(b.name||'')});
36
+ var pending=sorted.length,results=[];
37
+ sorted.forEach(function(p,idx){
25
38
  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++;});
39
+ results[idx]={project:p,suites:Array.isArray(suites)?suites:[]};
40
+ }).catch(function(){
41
+ results[idx]={project:p,suites:[]};
42
+ }).then(function(){
43
+ pending--;
44
+ if(pending===0)renderAllProjectAccordions(results);
45
+ });
38
46
  });
39
47
  }).catch(function(){});
40
48
  }
41
49
  }
42
50
 
43
- function renderProjectAccordion(container,project,suites){
51
+ function renderAllProjectAccordions(results){
52
+ var container=$('#suiteAccordionContainer');
53
+ var empty=$('#suitesEmpty');
54
+ container.textContent='';
55
+ var withSuites=results.filter(function(r){return r.suites.length>0});
56
+ var totalSuites=withSuites.reduce(function(s,r){return s+r.suites.length},0);
57
+ $('#badgeSuites').textContent=totalSuites;
58
+ if(withSuites.length===0){empty.style.display='block';empty.querySelector('p').textContent='No test suites found.';return}
59
+ empty.style.display='none';
60
+ var autoExpand=withSuites.length===1;
61
+ withSuites.forEach(function(r){
62
+ renderProjectAccordion(container,r.project,r.suites,autoExpand||S.testsExpanded.has(r.project.id));
63
+ });
64
+ applyTestsSearch();
65
+ }
66
+
67
+ function renderProjectAccordion(container,project,suites,startOpen){
44
68
  var totalTests=suites.reduce(function(sum,s){return sum+(s.testCount||0)},0);
45
69
  var body=el('div',{className:'project-accordion-body'});
46
70
  var innerGrid=el('div',{className:'suite-grid'});
@@ -57,10 +81,104 @@ function renderProjectAccordion(container,project,suites){
57
81
  ]);
58
82
 
59
83
  var wrapper=el('div',{className:'project-accordion'},[header,body]);
60
- header.addEventListener('click',function(){wrapper.classList.toggle('open')});
84
+ wrapper.dataset.projectId=String(project.id);
85
+ wrapper.dataset.projectName=(project.name||'').toLowerCase();
86
+ if(startOpen)wrapper.classList.add('open');
87
+ header.addEventListener('click',function(){
88
+ wrapper.classList.toggle('open');
89
+ if(wrapper.classList.contains('open'))S.testsExpanded.add(project.id);
90
+ else S.testsExpanded.delete(project.id);
91
+ });
61
92
  container.appendChild(wrapper);
62
93
  }
63
94
 
95
+ /* ── Search / filter ── */
96
+ function applyTestsSearch(){
97
+ var q=(S.testsSearch||'').trim().toLowerCase();
98
+ // Single-project mode: filter suite cards in #suiteGrid + module cards
99
+ // in #moduleSection. The toolbar count reflects both.
100
+ if(S.project){
101
+ var visSuites=0,visModules=0;
102
+ $$('#suiteGrid .suite-card').forEach(function(card){
103
+ var sname=(card.dataset.suiteName||'').toLowerCase();
104
+ var tests=card.querySelectorAll('.suite-card-tests li');
105
+ var testHit=false;
106
+ tests.forEach(function(li){
107
+ var raw=(li.firstChild&&li.firstChild.nodeType===3?li.firstChild.nodeValue:li.textContent)||'';
108
+ var tname=raw.toLowerCase();
109
+ var matches=!q||sname.indexOf(q)>=0||tname.indexOf(q)>=0;
110
+ li.style.display=matches?'':'none';
111
+ if(q&&tname.indexOf(q)>=0)testHit=true;
112
+ });
113
+ var show=!q||sname.indexOf(q)>=0||testHit;
114
+ card.style.display=show?'':'none';
115
+ if(show)visSuites++;
116
+ });
117
+ $$('#moduleSection .module-card').forEach(function(card){
118
+ var nm=(card.querySelector('.module-card-name')?.textContent||'').toLowerCase();
119
+ var desc=(card.querySelector('.module-card-desc')?.textContent||'').toLowerCase();
120
+ var show=!q||nm.indexOf(q)>=0||desc.indexOf(q)>=0;
121
+ card.style.display=show?'':'none';
122
+ if(show)visModules++;
123
+ });
124
+ var t=$('#module-section-title')||document.querySelector('.module-section-title');
125
+ var countEl=$('#suitesToolbarCount');
126
+ if(countEl){
127
+ if(q)countEl.textContent=visSuites+' suites · '+visModules+' modules';
128
+ else countEl.textContent='';
129
+ }
130
+ return;
131
+ }
132
+ // Multi-project (All Projects) mode: filter accordions and their children
133
+ var accordions=$$('#suiteAccordionContainer .project-accordion');
134
+ var visibleProjects=0,visibleSuites=0;
135
+
136
+ accordions.forEach(function(acc){
137
+ var pname=acc.dataset.projectName||'';
138
+ var projectMatches=q&&pname.indexOf(q)>=0;
139
+ var anySuiteVisible=false;
140
+ var cards=acc.querySelectorAll('.suite-card');
141
+ cards.forEach(function(card){
142
+ var sname=(card.dataset.suiteName||'').toLowerCase();
143
+ var tests=card.querySelectorAll('.suite-card-tests li');
144
+ var testMatches=0;
145
+ tests.forEach(function(li){
146
+ var raw=(li.firstChild&&li.firstChild.nodeType===3?li.firstChild.nodeValue:li.textContent)||'';
147
+ var tname=raw.toLowerCase();
148
+ var matches=!q||projectMatches||sname.indexOf(q)>=0||tname.indexOf(q)>=0;
149
+ li.style.display=matches?'':'none';
150
+ if(matches&&q&&tname.indexOf(q)>=0)testMatches++;
151
+ });
152
+ var suiteVisible=!q||projectMatches||sname.indexOf(q)>=0||testMatches>0;
153
+ card.style.display=suiteVisible?'':'none';
154
+ if(suiteVisible){anySuiteVisible=true;visibleSuites++}
155
+ });
156
+ var projectVisible=!q||projectMatches||anySuiteVisible;
157
+ acc.style.display=projectVisible?'':'none';
158
+ if(projectVisible)visibleProjects++;
159
+ if(q&&projectVisible&&anySuiteVisible)acc.classList.add('open');
160
+ else if(q&&!projectVisible)acc.classList.remove('open');
161
+ });
162
+
163
+ var countEl=$('#suitesToolbarCount');
164
+ if(countEl){
165
+ if(q)countEl.textContent=visibleSuites+' suites · '+visibleProjects+' projects';
166
+ else countEl.textContent='';
167
+ }
168
+ }
169
+
170
+ function setSuiteAccordionsOpen(open){
171
+ $$('#suiteAccordionContainer .project-accordion').forEach(function(acc){
172
+ if(acc.style.display==='none')return;
173
+ acc.classList.toggle('open',!!open);
174
+ var pid=parseInt(acc.dataset.projectId,10);
175
+ if(!isNaN(pid)){
176
+ if(open)S.testsExpanded.add(pid);
177
+ else S.testsExpanded.delete(pid);
178
+ }
179
+ });
180
+ }
181
+
64
182
  /* ── Suite Modal ── */
65
183
  var _suiteCache={};
66
184
 
@@ -195,6 +313,7 @@ function renderSuiteCards(container,suites,projectId){
195
313
  el('button',{className:'btn sm primary',onclick:function(){triggerRun(s.name,pid)}},'Run Suite')
196
314
  ])
197
315
  ]);
316
+ card.dataset.suiteName=s.name;
198
317
  container.appendChild(card);
199
318
  });
200
319
  }
@@ -292,3 +411,25 @@ $('#btnAddVar').addEventListener('click',function(){
292
411
  });
293
412
 
294
413
  $('#btnRunAll').addEventListener('click',function(){triggerRun()});
414
+
415
+ /* ── Tests toolbar (search + expand/collapse all) ── */
416
+ (function(){
417
+ var input=$('#suitesSearchInput');
418
+ if(input){
419
+ var debounce;
420
+ input.addEventListener('input',function(){
421
+ clearTimeout(debounce);
422
+ debounce=setTimeout(function(){
423
+ S.testsSearch=input.value||'';
424
+ applyTestsSearch();
425
+ },90);
426
+ });
427
+ input.addEventListener('keydown',function(e){
428
+ if(e.key==='Escape'){input.value='';S.testsSearch='';applyTestsSearch()}
429
+ });
430
+ }
431
+ var bExp=$('#btnExpandAllSuites');
432
+ if(bExp)bExp.addEventListener('click',function(){setSuiteAccordionsOpen(true)});
433
+ var bCol=$('#btnCollapseAllSuites');
434
+ if(bCol)bCol.addEventListener('click',function(){setSuiteAccordionsOpen(false)});
435
+ })();
@@ -0,0 +1,234 @@
1
+ /* ══════════════════════════════════════════════════════════════════
2
+ Tools View — Module Analysis, Capture, Analyze, Verify, Agent prompts
3
+ ══════════════════════════════════════════════════════════════════ */
4
+
5
+ /* ── Module Analysis ─────────────────────────────────────────── */
6
+ var MA_LAST = null;
7
+
8
+ function refreshModuleAnalysis(){
9
+ var body=$('#modAnalysisBody');var btnCopy=$('#btnCopyModulePrompt');
10
+ if(!body)return;
11
+ if(!S.project){
12
+ body.innerHTML='';
13
+ body.appendChild(el('div',{className:'tool-empty'},'Pick a project from the sidebar, then click Run analysis.'));
14
+ if(btnCopy)btnCopy.disabled=true;
15
+ return;
16
+ }
17
+ body.innerHTML='';
18
+ body.appendChild(el('div',{className:'tool-empty'},'Running analysis…'));
19
+ api('/api/tools/module-analysis/'+S.project).then(function(data){
20
+ if(data&&data.error){
21
+ body.innerHTML='';
22
+ body.appendChild(el('div',{className:'tool-result is-error'},'Error: '+data.error));
23
+ if(btnCopy)btnCopy.disabled=true;
24
+ return;
25
+ }
26
+ MA_LAST=data;
27
+ if(btnCopy)btnCopy.disabled=!data.agentPrompt;
28
+ renderModuleAnalysis(body,data);
29
+ }).catch(function(e){
30
+ body.innerHTML='';
31
+ body.appendChild(el('div',{className:'tool-result is-error'},'Request failed: '+(e&&e.message||'unknown')));
32
+ });
33
+ }
34
+
35
+ function renderModuleAnalysis(body,data){
36
+ body.innerHTML='';
37
+ var s=data.summary||{};
38
+ var summary=el('div',{className:'mod-summary'},[
39
+ summaryCell('Tests',s.testCount),
40
+ summaryCell('Modules',s.moduleCount),
41
+ summaryCell('Candidates',s.candidateCount,'signal'),
42
+ summaryCell('Unused',s.unusedModules,s.unusedModules>0?'warn':''),
43
+ ]);
44
+ body.appendChild(summary);
45
+
46
+ // Extraction candidates
47
+ if(data.candidates&&data.candidates.length){
48
+ body.appendChild(el('div',{className:'mod-section-title'},[
49
+ document.createTextNode('Extraction candidates'),
50
+ el('span',{className:'count'},String(data.candidates.length))
51
+ ]));
52
+ data.candidates.forEach(function(c){
53
+ body.appendChild(renderCandidateRow(c));
54
+ });
55
+ }else{
56
+ body.appendChild(el('div',{className:'mod-section-title'},[
57
+ document.createTextNode('Extraction candidates'),
58
+ el('span',{className:'count'},'0')
59
+ ]));
60
+ body.appendChild(el('div',{className:'tool-empty'},'No duplicated 3-8-action sequences found across tests. Your suite is well factored — or maybe under-modularized?'));
61
+ }
62
+
63
+ // Existing modules
64
+ if(data.modules&&data.modules.length){
65
+ body.appendChild(el('div',{className:'mod-section-title'},[
66
+ document.createTextNode('Existing modules'),
67
+ el('span',{className:'count'},String(data.modules.length))
68
+ ]));
69
+ var sorted=data.modules.slice().sort(function(a,b){return (b.usageCount||0)-(a.usageCount||0)});
70
+ sorted.forEach(function(m){body.appendChild(renderModuleRow(m))});
71
+ }
72
+ }
73
+
74
+ function summaryCell(label,value,cls){
75
+ return el('div',{className:'mod-summary-cell'+(cls?' '+cls:'')},[
76
+ el('div',{className:'mod-summary-cell-lbl'},label),
77
+ el('div',{className:'mod-summary-cell-val'},String(value!=null?value:'—'))
78
+ ]);
79
+ }
80
+
81
+ function renderModuleRow(m){
82
+ var cls='mod-row'+(m.usageCount===0?' unused':'');
83
+ var metaItems=[
84
+ el('span',null,(m.actionCount||0)+' actions'),
85
+ el('span',null,(m.params&&m.params.length||0)+' params'),
86
+ ];
87
+ if(m.usedBy&&m.usedBy.length)metaItems.push(el('span',null,'used by: '+m.usedBy.slice(0,3).join(', ')+(m.usedBy.length>3?' +'+(m.usedBy.length-3):'')));
88
+ return el('div',{className:cls},[
89
+ el('div',{className:'mod-row-main'},[
90
+ el('div',{className:'mod-row-name'},m.name),
91
+ m.description?el('div',{className:'mod-row-desc'},m.description):null,
92
+ el('div',{className:'mod-row-meta'},metaItems)
93
+ ]),
94
+ el('div',{className:'mod-row-usage'},(m.usageCount||0)+'×')
95
+ ]);
96
+ }
97
+
98
+ function renderCandidateRow(c){
99
+ var preview=(c.sample||[]).map(function(a,i){
100
+ var bits=[String(i+1).padStart(2,'0')+'.',a.type||'?'];
101
+ if(a.selector)bits.push('@'+a.selector);
102
+ if(a.text!=null)bits.push('"'+String(a.text).slice(0,40)+'"');
103
+ if(a.value!=null&&!a.text)bits.push('= '+String(a.value).slice(0,40));
104
+ return bits.join(' ');
105
+ }).join('\n');
106
+ var usedBy=(c.usedBy||[]).map(function(u){return u.suite+' › '+u.test+(u.occurrences>1?' (×'+u.occurrences+')':'')}).join(', ');
107
+ return el('div',{className:'cand-row'},[
108
+ el('div',{className:'cand-row-head'},[
109
+ el('div',{className:'cand-name'},'Suggested: '+(c.suggestedName||'module')),
110
+ el('div',{className:'cand-stats'},[
111
+ document.createTextNode((c.length||0)+' actions · '),
112
+ el('strong',null,(c.testCount||0)+' tests'),
113
+ document.createTextNode(' · '),
114
+ el('strong',null,(c.occurrenceCount||0)+' occurrences')
115
+ ])
116
+ ]),
117
+ el('div',{className:'cand-actions-preview'},preview),
118
+ el('div',{className:'cand-used-by'},[el('strong',null,'used by: '),document.createTextNode(usedBy)])
119
+ ]);
120
+ }
121
+
122
+ /* ── Capture URL ─────────────────────────────────────────── */
123
+ function runCapture(){
124
+ var url=$('#captureUrl').value.trim();var out=$('#captureResult');
125
+ if(!url){out.textContent='URL required';out.classList.add('is-error');return}
126
+ out.classList.remove('is-error');out.textContent='Capturing…';
127
+ var body={url:url,fullPage:$('#captureFullPage').checked};
128
+ if(S.project)body.projectId=S.project;
129
+ fetch('/api/tool/capture',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
130
+ .then(function(r){return r.json()})
131
+ .then(function(d){
132
+ if(d.error){out.classList.add('is-error');out.textContent='Error: '+d.error;return}
133
+ out.textContent='';
134
+ if(d.hash)out.appendChild(el('div',null,'Hash: '+d.hash));
135
+ if(d.path){
136
+ var img=document.createElement('img');img.src='/api/image?path='+encodeURIComponent(d.path);img.alt='capture';
137
+ out.appendChild(img);
138
+ }
139
+ })
140
+ .catch(function(e){out.classList.add('is-error');out.textContent='Request failed: '+(e&&e.message||'unknown')});
141
+ }
142
+
143
+ /* ── Analyze Page ────────────────────────────────────────── */
144
+ function runAnalyze(){
145
+ var url=$('#analyzeUrl').value.trim();var out=$('#analyzeResult');
146
+ if(!url){out.textContent='URL required';out.classList.add('is-error');return}
147
+ out.classList.remove('is-error');out.textContent='Analyzing…';
148
+ var body={url:url};if(S.project)body.projectId=S.project;
149
+ fetch('/api/tool/analyze',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
150
+ .then(function(r){return r.json()})
151
+ .then(function(d){
152
+ if(d.error){out.classList.add('is-error');out.textContent='Error: '+d.error;return}
153
+ out.textContent='';
154
+ var pre=el('pre',null,JSON.stringify(d,null,2));
155
+ out.appendChild(pre);
156
+ })
157
+ .catch(function(e){out.classList.add('is-error');out.textContent='Request failed: '+(e&&e.message||'unknown')});
158
+ }
159
+
160
+ /* ── Verify Issue ────────────────────────────────────────── */
161
+ function copyIssuePrompt(){
162
+ var url=$('#issueUrl').value.trim();
163
+ if(!url){showToast&&showToast('Issue URL required','warn');return}
164
+ var prompt='Use the e2e-runner test-creator agent to verify this issue end-to-end:\n\n'+
165
+ '1. Call e2e_issue with url="'+url+'" to fetch the issue details and the suggested test prompt.\n'+
166
+ '2. Generate the test JSON based on the issue requirements.\n'+
167
+ '3. Save it via e2e_create_test.\n'+
168
+ '4. Run it via e2e_run and report pass/fail with screenshots.';
169
+ copyToClipboard(prompt);
170
+ }
171
+ function runIssueVerify(){
172
+ var url=$('#issueUrl').value.trim();var out=$('#issueResult');
173
+ if(!url){out.textContent='URL required';out.classList.add('is-error');return}
174
+ out.classList.remove('is-error');out.textContent='Calling Anthropic API... this can take a minute.';
175
+ fetch('/api/tool/issue-verify',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:url,projectId:S.project||null})})
176
+ .then(function(r){return r.json()})
177
+ .then(function(d){
178
+ if(d.error){out.classList.add('is-error');out.textContent='Error: '+d.error;return}
179
+ out.textContent='';
180
+ out.appendChild(el('pre',null,JSON.stringify(d,null,2)));
181
+ })
182
+ .catch(function(e){out.classList.add('is-error');out.textContent='Request failed: '+(e&&e.message||'unknown')});
183
+ }
184
+
185
+ /* ── Agent prompt copy buttons ───────────────────────────── */
186
+ var AGENT_PROMPTS={
187
+ improver:'Run the test-improver agent on this project. Tasks:\n'+
188
+ '1. Use e2e_list to enumerate suites + modules.\n'+
189
+ '2. Identify duplicated 3+ action sequences across tests (canonical $use candidates).\n'+
190
+ '3. For each candidate, call e2e_create_module with sensible parameters, then Edit the test files to replace the inline sequence with {"$use":"<module-name>","params":{...}}.\n'+
191
+ '4. Replace any verbose evaluate blocks with built-in actions where possible.\n'+
192
+ '5. Run the affected suites via e2e_run and confirm no regressions.\n'+
193
+ '6. Report a summary: modules created, sequences replaced, tests touched.',
194
+ creator:'Run the test-creator agent on this project. Tasks:\n'+
195
+ '1. Ask me which feature/page you should write a new test for.\n'+
196
+ '2. Use e2e_analyze on the target URL to map interactive elements.\n'+
197
+ '3. Design a clear action sequence (goto, asserts, click, type, etc.).\n'+
198
+ '4. Save via e2e_create_test and run it via e2e_run.\n'+
199
+ '5. If it passes, show me the JSON + screenshot. If it fails, debug and fix.',
200
+ analyzer:'Run the test-analyzer agent on this project. Tasks:\n'+
201
+ '1. Use e2e_learnings query="summary" to get the current stability state.\n'+
202
+ '2. Use e2e_learnings query="flaky" and "errors" to drill into problems.\n'+
203
+ '3. For each top issue, check e2e_network_logs for the relevant runDbIds.\n'+
204
+ '4. Recommend concrete fixes: stabilization (waits, retries), selector hardening, or root-cause investigations.',
205
+ };
206
+ function copyAgentPrompt(name){
207
+ var p=AGENT_PROMPTS[name];if(!p)return;
208
+ copyToClipboard(p);
209
+ }
210
+
211
+ function copyToClipboard(text){
212
+ try{
213
+ if(navigator.clipboard){navigator.clipboard.writeText(text).then(function(){showToast&&showToast('Copied to clipboard','success')});return}
214
+ }catch(e){}
215
+ var ta=document.createElement('textarea');ta.value=text;ta.style.position='fixed';ta.style.opacity='0';
216
+ document.body.appendChild(ta);ta.select();
217
+ try{document.execCommand('copy');showToast&&showToast('Copied to clipboard','success')}catch(e){showToast&&showToast('Could not copy','error')}
218
+ document.body.removeChild(ta);
219
+ }
220
+
221
+ /* ── Wire up buttons ─────────────────────────────────────── */
222
+ (function(){
223
+ var b1=$('#btnRunModuleAnalysis');if(b1)b1.addEventListener('click',refreshModuleAnalysis);
224
+ var b2=$('#btnCopyModulePrompt');if(b2)b2.addEventListener('click',function(){
225
+ if(MA_LAST&&MA_LAST.agentPrompt)copyToClipboard(MA_LAST.agentPrompt);
226
+ });
227
+ var c1=$('#btnRunCapture');if(c1)c1.addEventListener('click',runCapture);
228
+ var a1=$('#btnRunAnalyze');if(a1)a1.addEventListener('click',runAnalyze);
229
+ var i1=$('#btnIssuePrompt');if(i1)i1.addEventListener('click',copyIssuePrompt);
230
+ var i2=$('#btnIssueVerify');if(i2)i2.addEventListener('click',runIssueVerify);
231
+ document.querySelectorAll('[data-prompt]').forEach(function(btn){
232
+ btn.addEventListener('click',function(){copyAgentPrompt(btn.dataset.prompt)});
233
+ });
234
+ })();
@@ -65,7 +65,7 @@ function renderWatchCards(projects){
65
65
  var detailBtn=el('button',{className:'btn sm',onclick:function(e){
66
66
  e.stopPropagation();
67
67
  S.project=p.id;$('#projectSelect').value=p.id;
68
- showView('runs');
68
+ showView('investigate');
69
69
  refreshRuns();refreshSuites();
70
70
  }},'\uD83D\uDD0D');
71
71
 
@@ -222,7 +222,7 @@ function renderEventLog(runs){
222
222
  (function(run){
223
223
  row.addEventListener('click',function(){
224
224
  S.project=run.project_id;$('#projectSelect').value=run.project_id;
225
- showView('runs');
225
+ showView('investigate');
226
226
  refreshRuns();
227
227
  });
228
228
  })(r);
@@ -105,9 +105,39 @@ function handleWS(m){
105
105
  showToast('Run error: '+m.error,'error');
106
106
  renderLive();break;
107
107
  case 'test:frame':
108
- if(S.screencastTest===m.name&&m.data){
109
- var img=$('#screencastImg');
110
- if(img)img.src='data:image/jpeg;base64,'+m.data;
108
+ if(m.data){
109
+ var pinned=S.screencastSel;
110
+ var showThis;
111
+ if(pinned){
112
+ // Pinned: only the watched test's frames.
113
+ showThis=pinned.runId===m.runId&&pinned.name===m.name;
114
+ }else if(S.screencastAuto!==false){
115
+ // Auto: sticky — stay on the current test until it stops running,
116
+ // then adopt the next one. Never interleave two tests' frames.
117
+ var cur=S.screencastLast;
118
+ var curRun=cur&&S.liveRuns[cur.runId];
119
+ var curT=curRun&&curRun.tests?curRun.tests[cur.name]:null;
120
+ var curRunning=curT&&curT.status==='running';
121
+ showThis=!cur||!curRunning||(cur.runId===m.runId&&cur.name===m.name);
122
+ }else{showThis=false}
123
+ if(showThis){
124
+ var frameSrc='data:image/jpeg;base64,'+m.data;
125
+ // Switching to a different test? Clear the strip so they don't pile up.
126
+ var changed=!pinned&&(!S.screencastLast||S.screencastLast.runId!==m.runId||S.screencastLast.name!==m.name);
127
+ if(changed&&S.screencastLast&&typeof resetFilmstrip==='function')resetFilmstrip();
128
+ var img=$('#screencastImg');
129
+ if(img){
130
+ img.src=frameSrc;
131
+ img.style.display='block';
132
+ var vp=img.closest('.screencast-viewport');if(vp)vp.classList.add('has-frame');
133
+ var ph2=$('#screencastPlaceholder');if(ph2)ph2.style.display='none';
134
+ }
135
+ if(!pinned){
136
+ S.screencastLast={runId:m.runId,name:m.name};
137
+ if(changed&&typeof updateScreencastUI==='function')updateScreencastUI();
138
+ }
139
+ if(typeof pushFilmFrame==='function')pushFilmFrame(frameSrc,m.name,m.runId);
140
+ }
111
141
  }
112
142
  break;
113
143
  case 'db:updated':