donobu 5.43.2 → 5.46.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.
@@ -1597,7 +1597,9 @@ function renderHtml(report, triage, outputDir) {
1597
1597
  const testId = testIds[ti];
1598
1598
  const hasMultipleResults = test.results.length > 1;
1599
1599
  const lastResult = test.results.at(-1);
1600
- const displayFileName = test.file.split('/').pop() ?? test.file;
1600
+ // Path is already relative to rootDir (see buildReport.ts), so it's safe
1601
+ // to display directly — no absolute paths leak through.
1602
+ const displayFilePath = test.file;
1601
1603
  let detailsHtml = '';
1602
1604
  // 0. Skip reason — surface the description and call site from
1603
1605
  // test.skip()/test.fixme() so a skipped test explains itself.
@@ -1678,12 +1680,19 @@ function renderHtml(report, triage, outputDir) {
1678
1680
  ? `<div class="flow-id-detail"><span class="detail-label">Flow ID</span><span class="flow-id-value">${esc(test.flowId)}<button class="copy-flow-id" data-flow-id="${esc(test.flowId)}" title="Copy flow ID"><svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button></span></div>`
1679
1681
  : '';
1680
1682
  testSectionsHtml += `
1681
- <div class="test-card ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" id="${testId}" data-status="${test.status}" data-file="${esc(test.file)}" data-tags="${esc(JSON.stringify(test.tags))}"${test.plan ? ` data-reason="${esc(test.plan.plan.failureReason)}"` : ''} ${hasDetails ? `data-detail="${testId}"` : ''}>
1683
+ <div class="test-card ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" id="${testId}" data-status="${test.status}" data-file="${esc(test.file)}" data-search="${esc((displayFilePath + ' ' + test.specTitle).toLowerCase())}" data-tags="${esc(JSON.stringify(test.tags))}"${test.plan ? ` data-reason="${esc(test.plan.plan.failureReason)}"` : ''} ${hasDetails ? `data-detail="${testId}"` : ''}>
1682
1684
  <div class="test-summary">
1683
1685
  ${chevron}
1684
1686
  <span class="status-dot" style="background:${sc.color}" title="${sc.label}"></span>
1685
1687
  <div class="test-name-group">
1686
- <span class="test-name"><span class="test-file">${esc(displayFileName)}</span> (${esc(test.specTitle)})</span>
1688
+ <div class="test-file-line">
1689
+ <span class="test-file">${esc(displayFilePath)}</span>
1690
+ <button class="copy-text" data-copy-text="${esc(displayFilePath)}" title="Copy file name"><svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button>
1691
+ </div>
1692
+ <div class="test-title-line">
1693
+ <span class="test-title">${esc(test.specTitle)}</span>
1694
+ <button class="copy-text" data-copy-text="${esc(test.specTitle)}" title="Copy test name"><svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button>
1695
+ </div>
1687
1696
  </div>
1688
1697
  ${test.plan ? `<span class="inline-reason" style="color:${reasonCfg(test.plan.plan.failureReason).color}" title="${esc(test.plan.plan.failureReason)}">${esc(reasonCfg(test.plan.plan.failureReason).label)}</span>` : ''}
1689
1698
  ${test.tags.map((t) => `<span class="test-tag">${esc(t)}</span>`).join('')}
@@ -1769,6 +1778,17 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
1769
1778
  .stat-pill.active{background:var(--accent);border-color:var(--accent);color:#fff}
1770
1779
  .stat-pill.active .pill-count{background:rgba(255,255,255,.25);color:#fff}
1771
1780
  .pill-count{font-size:11px;font-weight:700;background:var(--overlay-light-active);color:var(--text-dim);padding:1px 7px;border-radius:calc(var(--radius) - 2px);min-width:20px;text-align:center;transition:all .2s}
1781
+ /* Substring search across test filename + spec title. Lives in the same row
1782
+ * as the stat pills and tag/diagnosis filter. */
1783
+ .filter-search-wrap{position:relative;display:inline-flex;align-items:center;flex-shrink:0}
1784
+ .filter-search-icon{position:absolute;left:8px;width:14px;height:14px;color:var(--text-muted);fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;pointer-events:none}
1785
+ .filter-search{background:var(--surface);border:1px solid var(--border);color:var(--text);font:inherit;font-size:12px;height:28px;padding:0 10px 0 28px;border-radius:var(--radius);width:200px;outline:none;transition:border-color .15s,background .15s}
1786
+ .filter-search::placeholder{color:var(--text-dim)}
1787
+ .filter-search:hover{border-color:var(--text-dim)}
1788
+ .filter-search:focus{border-color:var(--accent);background:var(--surface-raised)}
1789
+ /* Hide the WebKit search clear "x" — Clear Filters wipes it via the same UI. */
1790
+ .filter-search::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}
1791
+
1772
1792
  .clear-filter{background:var(--surface);border:1px solid var(--border);color:var(--text-muted);padding:6px 14px;border-radius:var(--radius);cursor:pointer;font-size:12px;font-weight:500;font-family:inherit;display:none;align-items:center;gap:5px;flex-shrink:0;transition:all .2s}
1773
1793
  .clear-filter:hover{background:var(--surface-raised);border-color:var(--text-dim);color:var(--text)}
1774
1794
  .clear-filter.visible{display:flex}
@@ -1806,9 +1826,15 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
1806
1826
  .chevron-spacer{width:18px;flex-shrink:0}
1807
1827
  .test-card.expanded .chevron{transform:rotate(90deg)}
1808
1828
  .status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
1809
- .test-file{color:var(--text);font-weight:600}
1810
1829
  .test-name-group{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
1811
- .test-name{font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1830
+ .test-file-line,.test-title-line{display:flex;align-items:center;gap:6px;min-width:0}
1831
+ .test-file{color:var(--text-muted);font-family:var(--mono);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1832
+ .test-title{color:var(--text);font-weight:500;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1833
+ .copy-text{background:transparent;border:none;border-radius:4px;cursor:pointer;padding:2px;display:flex;align-items:center;justify-content:center;color:var(--text-dim);opacity:0;transition:opacity .15s,background .15s,color .15s;flex-shrink:0}
1834
+ .test-summary:hover .copy-text,.copy-text:focus-visible{opacity:1}
1835
+ .copy-text:hover{background:var(--overlay-light-active);color:var(--text)}
1836
+ .copy-text svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
1837
+ .copy-text .check-icon{color:#22c55e;opacity:1}
1812
1838
  .flow-id{font-size:11px;color:var(--text-dim);opacity:.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:flex;align-items:center;gap:4px}
1813
1839
  .copy-flow-id{background:var(--overlay-light-hover);border:none;border-radius:4px;cursor:pointer;padding:3px;display:flex;align-items:center;justify-content:center;color:var(--text-dim);transition:background .15s,color .15s;flex-shrink:0}
1814
1840
  .copy-flow-id:hover{background:var(--overlay-light-active);color:var(--text)}
@@ -2131,6 +2157,10 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2131
2157
  <div class="test-bar">${testBarHtml}</div>
2132
2158
  <div class="summary-stats">
2133
2159
  <div class="stat-pills">${statPillsHtml}</div>
2160
+ <label class="filter-search-wrap" title="Search test titles">
2161
+ <svg class="filter-search-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
2162
+ <input type="search" class="filter-search" data-filter-search placeholder="Search tests…" autocomplete="off" spellcheck="false" />
2163
+ </label>
2134
2164
  <div class="tag-filter-controls" data-tag-filter-controls hidden>
2135
2165
  <div class="tag-filter-trigger-wrap">
2136
2166
  <button class="add-tag-filter" data-add-tag-filter title="Filter by tag or diagnosis"><span class="add-tag-plus">+</span> Filter</button>
@@ -2165,15 +2195,43 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2165
2195
  var activeStatus=null;
2166
2196
  var activeTags=new Set();
2167
2197
  var activeReasons=new Set();
2198
+ var activeSearch=''; // lowercase substring match against data-search
2168
2199
  var allTags=[];
2169
2200
  var allReasons=[]; // ordered list of REASON keys present in the report
2170
2201
  var REASON_LABELS=${JSON.stringify(REASON_LABELS)};
2171
2202
 
2172
2203
  function cardTags(card){var raw=card.getAttribute('data-tags');if(!raw)return [];try{var v=JSON.parse(raw);return Array.isArray(v)?v:[]}catch(_){return []}}
2173
- function tagCount(t){var n=0;document.querySelectorAll('.test-card').forEach(function(c){if(cardTags(c).indexOf(t)!==-1)n++});return n}
2204
+
2205
+ // Faceted-search counts. Each filter option's badge shows "how many tests
2206
+ // would this option contribute given the rest of the filters." The semantics
2207
+ // per dimension match how clicking interacts:
2208
+ // - Status pills (single-select replace): ignore current activeStatus.
2209
+ // - Tag menu items (multi-select AND): use ALL current filters.
2210
+ // - Reason menu items (multi-select OR): ignore current activeReasons.
2211
+ // Search is free-form and not counted.
2212
+ function cardsMatching(ignoreStatus,ignoreTags,ignoreReasons){
2213
+ var out=[];
2214
+ document.querySelectorAll('.test-card').forEach(function(card){
2215
+ var statusOk=ignoreStatus||activeStatus===null||card.getAttribute('data-status')===activeStatus;
2216
+ var tagsOk=true;
2217
+ if(!ignoreTags&&activeTags.size>0){
2218
+ var t=cardTags(card);
2219
+ activeTags.forEach(function(w){if(t.indexOf(w)===-1)tagsOk=false});
2220
+ }
2221
+ var reasonOk=ignoreReasons||activeReasons.size===0||activeReasons.has(card.getAttribute('data-reason')||'');
2222
+ var searchOk=activeSearch.length===0||(card.getAttribute('data-search')||'').indexOf(activeSearch)!==-1;
2223
+ if(statusOk&&tagsOk&&reasonOk&&searchOk)out.push(card);
2224
+ });
2225
+ return out;
2226
+ }
2227
+ function tagCount(t){
2228
+ var pool=cardsMatching(false,false,false);
2229
+ var n=0;for(var i=0;i<pool.length;i++){if(cardTags(pool[i]).indexOf(t)!==-1)n++}
2230
+ return n;
2231
+ }
2174
2232
 
2175
2233
  function applyFilters(){
2176
- var anyActive=activeStatus!==null||activeTags.size>0||activeReasons.size>0;
2234
+ var anyActive=activeStatus!==null||activeTags.size>0||activeReasons.size>0||activeSearch.length>0;
2177
2235
  document.querySelector('.clear-filter').classList.toggle('visible',anyActive);
2178
2236
  var visibleTests=0;
2179
2237
  var visibleFiles=Object.create(null);
@@ -2189,7 +2247,12 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2189
2247
  var r=card.getAttribute('data-reason')||'';
2190
2248
  reasonOk=activeReasons.has(r);
2191
2249
  }
2192
- var hide=!(statusOk&&tagsOk&&reasonOk);
2250
+ var searchOk=true;
2251
+ if(activeSearch.length>0){
2252
+ var hay=card.getAttribute('data-search')||'';
2253
+ searchOk=hay.indexOf(activeSearch)!==-1;
2254
+ }
2255
+ var hide=!(statusOk&&tagsOk&&reasonOk&&searchOk);
2193
2256
  card.classList.toggle('hidden-by-filter',hide);
2194
2257
  if(!hide){
2195
2258
  visibleTests++;
@@ -2203,6 +2266,11 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2203
2266
  var card=document.getElementById(block.getAttribute('data-target'));
2204
2267
  block.classList.toggle('hidden-by-filter',!!(card&&card.classList.contains('hidden-by-filter')));
2205
2268
  });
2269
+ // Faceted-search live counts: status pills, and the tag/diagnosis menu
2270
+ // (refreshed if currently open) all reflect "given the other filters,
2271
+ // how many tests would this option contribute".
2272
+ updateStatPillCounts();
2273
+ if(tagMenuOpen())openTagMenu(); // re-render menu items with fresh counts
2206
2274
  // "X tests across Y files" subtitle reflects the current filter result.
2207
2275
  // When no filter is active the form matches the original (no "of Y").
2208
2276
  var sub=document.querySelector('[data-summary-sub]');
@@ -2227,6 +2295,7 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2227
2295
  if(activeStatus)p.set('status',activeStatus);
2228
2296
  activeTags.forEach(function(t){p.append('tag',t)});
2229
2297
  activeReasons.forEach(function(r){p.append('reason',r)});
2298
+ if(activeSearch)p.set('q',activeSearch);
2230
2299
  var qs=p.toString();
2231
2300
  var next=location.pathname+(qs?'?'+qs:'')+(location.hash||'');
2232
2301
  if(next!==location.pathname+location.search+location.hash){
@@ -2277,25 +2346,42 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2277
2346
  function addReason(r){if(!r||activeReasons.has(r))return;activeReasons.add(r);renderActiveChips();applyFilters()}
2278
2347
  function removeReason(r){if(!activeReasons.delete(r))return;renderActiveChips();applyFilters()}
2279
2348
 
2280
- function reasonCount(r){var n=0;document.querySelectorAll('.test-card').forEach(function(c){if(c.getAttribute('data-reason')===r)n++});return n}
2349
+ function reasonCount(r){
2350
+ var pool=cardsMatching(false,false,true);
2351
+ var n=0;for(var i=0;i<pool.length;i++){if(pool[i].getAttribute('data-reason')===r)n++}
2352
+ return n;
2353
+ }
2354
+ function updateStatPillCounts(){
2355
+ var pool=cardsMatching(true,false,false);
2356
+ var counts=Object.create(null);
2357
+ for(var i=0;i<pool.length;i++){var s=pool[i].getAttribute('data-status');counts[s]=(counts[s]||0)+1}
2358
+ document.querySelectorAll('.stat-pill[data-filter]').forEach(function(pill){
2359
+ var key=pill.getAttribute('data-filter');
2360
+ var span=pill.querySelector('.pill-count');
2361
+ if(span)span.textContent=counts[key]||0;
2362
+ });
2363
+ }
2281
2364
 
2282
2365
  function openTagMenu(){
2283
2366
  var menu=document.querySelector('[data-tag-menu]');
2284
2367
  if(!menu)return;
2285
2368
  var trigger=document.querySelector('[data-add-tag-filter]');
2286
2369
  menu.innerHTML='';
2287
- var availTags=allTags.filter(function(t){return !activeTags.has(t)});
2288
- var availReasons=allReasons.filter(function(r){return !activeReasons.has(r)});
2370
+ // For each available tag/reason, compute its preview count under the
2371
+ // current other filters. Options with count 0 are hidden — they'd lead
2372
+ // to an empty view, so they're not useful to offer.
2373
+ var tagsWithCounts=allTags.filter(function(t){return !activeTags.has(t)}).map(function(t){return {key:t,count:tagCount(t)}}).filter(function(x){return x.count>0});
2374
+ var reasonsWithCounts=allReasons.filter(function(r){return !activeReasons.has(r)}).map(function(r){return {key:r,count:reasonCount(r)}}).filter(function(x){return x.count>0});
2289
2375
  var added=false;
2290
2376
  if(allTags.length>0){
2291
2377
  var hT=document.createElement('div');hT.className='tag-menu-section';hT.textContent='Tags';menu.appendChild(hT);
2292
- if(availTags.length===0){
2293
- var emptyT=document.createElement('div');emptyT.className='tag-menu-empty';emptyT.textContent='All tags selected';menu.appendChild(emptyT);
2378
+ if(tagsWithCounts.length===0){
2379
+ var emptyT=document.createElement('div');emptyT.className='tag-menu-empty';emptyT.textContent=allTags.length===activeTags.size?'All tags selected':'No matching tags';menu.appendChild(emptyT);
2294
2380
  }else{
2295
- availTags.forEach(function(t){
2296
- var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-tag-menu-item',t);
2297
- var label=document.createElement('span');label.textContent=t;
2298
- var count=document.createElement('span');count.className='tag-menu-count';count.textContent=tagCount(t);
2381
+ tagsWithCounts.forEach(function(x){
2382
+ var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-tag-menu-item',x.key);
2383
+ var label=document.createElement('span');label.textContent=x.key;
2384
+ var count=document.createElement('span');count.className='tag-menu-count';count.textContent=x.count;
2299
2385
  item.appendChild(label);item.appendChild(count);
2300
2386
  menu.appendChild(item);
2301
2387
  });
@@ -2304,14 +2390,14 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2304
2390
  }
2305
2391
  if(allReasons.length>0){
2306
2392
  var hR=document.createElement('div');hR.className='tag-menu-section';hR.textContent='Diagnoses';menu.appendChild(hR);
2307
- if(availReasons.length===0){
2308
- var emptyR=document.createElement('div');emptyR.className='tag-menu-empty';emptyR.textContent='All diagnoses selected';menu.appendChild(emptyR);
2393
+ if(reasonsWithCounts.length===0){
2394
+ var emptyR=document.createElement('div');emptyR.className='tag-menu-empty';emptyR.textContent=allReasons.length===activeReasons.size?'All diagnoses selected':'No matching diagnoses';menu.appendChild(emptyR);
2309
2395
  }else{
2310
- availReasons.forEach(function(r){
2311
- var meta=REASON_LABELS[r]||REASON_LABELS['UNKNOWN'];
2312
- var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-reason-menu-item',r);
2396
+ reasonsWithCounts.forEach(function(x){
2397
+ var meta=REASON_LABELS[x.key]||REASON_LABELS['UNKNOWN'];
2398
+ var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-reason-menu-item',x.key);
2313
2399
  var label=document.createElement('span');label.textContent=meta.label;label.style.color=meta.color;
2314
- var count=document.createElement('span');count.className='tag-menu-count';count.textContent=reasonCount(r);
2400
+ var count=document.createElement('span');count.className='tag-menu-count';count.textContent=x.count;
2315
2401
  item.appendChild(label);item.appendChild(count);
2316
2402
  menu.appendChild(item);
2317
2403
  });
@@ -2335,7 +2421,10 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2335
2421
  activeStatus=null;
2336
2422
  activeTags.clear();
2337
2423
  activeReasons.clear();
2424
+ activeSearch='';
2338
2425
  document.querySelectorAll('.stat-pill').forEach(function(p){p.classList.remove('active')});
2426
+ var searchInput=document.querySelector('[data-filter-search]');
2427
+ if(searchInput)searchInput.value='';
2339
2428
  renderActiveChips();
2340
2429
  closeTagMenu();
2341
2430
  applyFilters();
@@ -2365,7 +2454,9 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2365
2454
  if(el){navigator.clipboard.writeText(el.textContent);var s=copyBtn.innerHTML;copyBtn.innerHTML='<svg class="check-icon" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>';setTimeout(function(){copyBtn.innerHTML=s},2000)}
2366
2455
  return;
2367
2456
  }
2368
- // Copy buttons (flow ID + JSON)
2457
+ // Copy buttons (flow ID + JSON + inline text via data-copy-text)
2458
+ var copyTextBtn=e.target.closest('.copy-text[data-copy-text]');
2459
+ if(copyTextBtn){e.stopPropagation();e.preventDefault();navigator.clipboard.writeText(copyTextBtn.getAttribute('data-copy-text'));var copyTextSvg=copyTextBtn.innerHTML;copyTextBtn.innerHTML='<svg class="check-icon" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>';setTimeout(function(){copyTextBtn.innerHTML=copyTextSvg},2000);return}
2369
2460
  var flowBtn=e.target.closest('.copy-flow-id');
2370
2461
  if(flowBtn){e.stopPropagation();navigator.clipboard.writeText(flowBtn.getAttribute('data-flow-id'));var copySvg=flowBtn.innerHTML;flowBtn.innerHTML='<svg class="check-icon" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>';setTimeout(function(){flowBtn.innerHTML=copySvg},2000);return}
2371
2462
  var jsonBtn=e.target.closest('.copy-json');
@@ -2463,8 +2554,20 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2463
2554
  p.getAll('tag').forEach(function(t){if(tagSet[t])activeTags.add(t)});
2464
2555
  var reasonSet={};allReasons.forEach(function(r){reasonSet[r]=true});
2465
2556
  p.getAll('reason').forEach(function(r){if(reasonSet[r])activeReasons.add(r)});
2557
+ var q=p.get('q');
2558
+ var searchInput=document.querySelector('[data-filter-search]');
2559
+ if(q){
2560
+ activeSearch=q.toLowerCase();
2561
+ if(searchInput)searchInput.value=q;
2562
+ }
2563
+ if(searchInput){
2564
+ searchInput.addEventListener('input',function(){
2565
+ activeSearch=searchInput.value.trim().toLowerCase();
2566
+ applyFilters();
2567
+ });
2568
+ }
2466
2569
  if(activeTags.size>0||activeReasons.size>0)renderActiveChips();
2467
- if(activeStatus!==null||activeTags.size>0||activeReasons.size>0)applyFilters();
2570
+ if(activeStatus!==null||activeTags.size>0||activeReasons.size>0||activeSearch.length>0)applyFilters();
2468
2571
  })();
2469
2572
 
2470
2573
  // Open #?testId=<id> deep links to the matching test card. Used by the
@@ -1597,7 +1597,9 @@ function renderHtml(report, triage, outputDir) {
1597
1597
  const testId = testIds[ti];
1598
1598
  const hasMultipleResults = test.results.length > 1;
1599
1599
  const lastResult = test.results.at(-1);
1600
- const displayFileName = test.file.split('/').pop() ?? test.file;
1600
+ // Path is already relative to rootDir (see buildReport.ts), so it's safe
1601
+ // to display directly — no absolute paths leak through.
1602
+ const displayFilePath = test.file;
1601
1603
  let detailsHtml = '';
1602
1604
  // 0. Skip reason — surface the description and call site from
1603
1605
  // test.skip()/test.fixme() so a skipped test explains itself.
@@ -1678,12 +1680,19 @@ function renderHtml(report, triage, outputDir) {
1678
1680
  ? `<div class="flow-id-detail"><span class="detail-label">Flow ID</span><span class="flow-id-value">${esc(test.flowId)}<button class="copy-flow-id" data-flow-id="${esc(test.flowId)}" title="Copy flow ID"><svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button></span></div>`
1679
1681
  : '';
1680
1682
  testSectionsHtml += `
1681
- <div class="test-card ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" id="${testId}" data-status="${test.status}" data-file="${esc(test.file)}" data-tags="${esc(JSON.stringify(test.tags))}"${test.plan ? ` data-reason="${esc(test.plan.plan.failureReason)}"` : ''} ${hasDetails ? `data-detail="${testId}"` : ''}>
1683
+ <div class="test-card ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" id="${testId}" data-status="${test.status}" data-file="${esc(test.file)}" data-search="${esc((displayFilePath + ' ' + test.specTitle).toLowerCase())}" data-tags="${esc(JSON.stringify(test.tags))}"${test.plan ? ` data-reason="${esc(test.plan.plan.failureReason)}"` : ''} ${hasDetails ? `data-detail="${testId}"` : ''}>
1682
1684
  <div class="test-summary">
1683
1685
  ${chevron}
1684
1686
  <span class="status-dot" style="background:${sc.color}" title="${sc.label}"></span>
1685
1687
  <div class="test-name-group">
1686
- <span class="test-name"><span class="test-file">${esc(displayFileName)}</span> (${esc(test.specTitle)})</span>
1688
+ <div class="test-file-line">
1689
+ <span class="test-file">${esc(displayFilePath)}</span>
1690
+ <button class="copy-text" data-copy-text="${esc(displayFilePath)}" title="Copy file name"><svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button>
1691
+ </div>
1692
+ <div class="test-title-line">
1693
+ <span class="test-title">${esc(test.specTitle)}</span>
1694
+ <button class="copy-text" data-copy-text="${esc(test.specTitle)}" title="Copy test name"><svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button>
1695
+ </div>
1687
1696
  </div>
1688
1697
  ${test.plan ? `<span class="inline-reason" style="color:${reasonCfg(test.plan.plan.failureReason).color}" title="${esc(test.plan.plan.failureReason)}">${esc(reasonCfg(test.plan.plan.failureReason).label)}</span>` : ''}
1689
1698
  ${test.tags.map((t) => `<span class="test-tag">${esc(t)}</span>`).join('')}
@@ -1769,6 +1778,17 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
1769
1778
  .stat-pill.active{background:var(--accent);border-color:var(--accent);color:#fff}
1770
1779
  .stat-pill.active .pill-count{background:rgba(255,255,255,.25);color:#fff}
1771
1780
  .pill-count{font-size:11px;font-weight:700;background:var(--overlay-light-active);color:var(--text-dim);padding:1px 7px;border-radius:calc(var(--radius) - 2px);min-width:20px;text-align:center;transition:all .2s}
1781
+ /* Substring search across test filename + spec title. Lives in the same row
1782
+ * as the stat pills and tag/diagnosis filter. */
1783
+ .filter-search-wrap{position:relative;display:inline-flex;align-items:center;flex-shrink:0}
1784
+ .filter-search-icon{position:absolute;left:8px;width:14px;height:14px;color:var(--text-muted);fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;pointer-events:none}
1785
+ .filter-search{background:var(--surface);border:1px solid var(--border);color:var(--text);font:inherit;font-size:12px;height:28px;padding:0 10px 0 28px;border-radius:var(--radius);width:200px;outline:none;transition:border-color .15s,background .15s}
1786
+ .filter-search::placeholder{color:var(--text-dim)}
1787
+ .filter-search:hover{border-color:var(--text-dim)}
1788
+ .filter-search:focus{border-color:var(--accent);background:var(--surface-raised)}
1789
+ /* Hide the WebKit search clear "x" — Clear Filters wipes it via the same UI. */
1790
+ .filter-search::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}
1791
+
1772
1792
  .clear-filter{background:var(--surface);border:1px solid var(--border);color:var(--text-muted);padding:6px 14px;border-radius:var(--radius);cursor:pointer;font-size:12px;font-weight:500;font-family:inherit;display:none;align-items:center;gap:5px;flex-shrink:0;transition:all .2s}
1773
1793
  .clear-filter:hover{background:var(--surface-raised);border-color:var(--text-dim);color:var(--text)}
1774
1794
  .clear-filter.visible{display:flex}
@@ -1806,9 +1826,15 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
1806
1826
  .chevron-spacer{width:18px;flex-shrink:0}
1807
1827
  .test-card.expanded .chevron{transform:rotate(90deg)}
1808
1828
  .status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
1809
- .test-file{color:var(--text);font-weight:600}
1810
1829
  .test-name-group{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
1811
- .test-name{font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1830
+ .test-file-line,.test-title-line{display:flex;align-items:center;gap:6px;min-width:0}
1831
+ .test-file{color:var(--text-muted);font-family:var(--mono);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1832
+ .test-title{color:var(--text);font-weight:500;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1833
+ .copy-text{background:transparent;border:none;border-radius:4px;cursor:pointer;padding:2px;display:flex;align-items:center;justify-content:center;color:var(--text-dim);opacity:0;transition:opacity .15s,background .15s,color .15s;flex-shrink:0}
1834
+ .test-summary:hover .copy-text,.copy-text:focus-visible{opacity:1}
1835
+ .copy-text:hover{background:var(--overlay-light-active);color:var(--text)}
1836
+ .copy-text svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
1837
+ .copy-text .check-icon{color:#22c55e;opacity:1}
1812
1838
  .flow-id{font-size:11px;color:var(--text-dim);opacity:.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:flex;align-items:center;gap:4px}
1813
1839
  .copy-flow-id{background:var(--overlay-light-hover);border:none;border-radius:4px;cursor:pointer;padding:3px;display:flex;align-items:center;justify-content:center;color:var(--text-dim);transition:background .15s,color .15s;flex-shrink:0}
1814
1840
  .copy-flow-id:hover{background:var(--overlay-light-active);color:var(--text)}
@@ -2131,6 +2157,10 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2131
2157
  <div class="test-bar">${testBarHtml}</div>
2132
2158
  <div class="summary-stats">
2133
2159
  <div class="stat-pills">${statPillsHtml}</div>
2160
+ <label class="filter-search-wrap" title="Search test titles">
2161
+ <svg class="filter-search-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
2162
+ <input type="search" class="filter-search" data-filter-search placeholder="Search tests…" autocomplete="off" spellcheck="false" />
2163
+ </label>
2134
2164
  <div class="tag-filter-controls" data-tag-filter-controls hidden>
2135
2165
  <div class="tag-filter-trigger-wrap">
2136
2166
  <button class="add-tag-filter" data-add-tag-filter title="Filter by tag or diagnosis"><span class="add-tag-plus">+</span> Filter</button>
@@ -2165,15 +2195,43 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2165
2195
  var activeStatus=null;
2166
2196
  var activeTags=new Set();
2167
2197
  var activeReasons=new Set();
2198
+ var activeSearch=''; // lowercase substring match against data-search
2168
2199
  var allTags=[];
2169
2200
  var allReasons=[]; // ordered list of REASON keys present in the report
2170
2201
  var REASON_LABELS=${JSON.stringify(REASON_LABELS)};
2171
2202
 
2172
2203
  function cardTags(card){var raw=card.getAttribute('data-tags');if(!raw)return [];try{var v=JSON.parse(raw);return Array.isArray(v)?v:[]}catch(_){return []}}
2173
- function tagCount(t){var n=0;document.querySelectorAll('.test-card').forEach(function(c){if(cardTags(c).indexOf(t)!==-1)n++});return n}
2204
+
2205
+ // Faceted-search counts. Each filter option's badge shows "how many tests
2206
+ // would this option contribute given the rest of the filters." The semantics
2207
+ // per dimension match how clicking interacts:
2208
+ // - Status pills (single-select replace): ignore current activeStatus.
2209
+ // - Tag menu items (multi-select AND): use ALL current filters.
2210
+ // - Reason menu items (multi-select OR): ignore current activeReasons.
2211
+ // Search is free-form and not counted.
2212
+ function cardsMatching(ignoreStatus,ignoreTags,ignoreReasons){
2213
+ var out=[];
2214
+ document.querySelectorAll('.test-card').forEach(function(card){
2215
+ var statusOk=ignoreStatus||activeStatus===null||card.getAttribute('data-status')===activeStatus;
2216
+ var tagsOk=true;
2217
+ if(!ignoreTags&&activeTags.size>0){
2218
+ var t=cardTags(card);
2219
+ activeTags.forEach(function(w){if(t.indexOf(w)===-1)tagsOk=false});
2220
+ }
2221
+ var reasonOk=ignoreReasons||activeReasons.size===0||activeReasons.has(card.getAttribute('data-reason')||'');
2222
+ var searchOk=activeSearch.length===0||(card.getAttribute('data-search')||'').indexOf(activeSearch)!==-1;
2223
+ if(statusOk&&tagsOk&&reasonOk&&searchOk)out.push(card);
2224
+ });
2225
+ return out;
2226
+ }
2227
+ function tagCount(t){
2228
+ var pool=cardsMatching(false,false,false);
2229
+ var n=0;for(var i=0;i<pool.length;i++){if(cardTags(pool[i]).indexOf(t)!==-1)n++}
2230
+ return n;
2231
+ }
2174
2232
 
2175
2233
  function applyFilters(){
2176
- var anyActive=activeStatus!==null||activeTags.size>0||activeReasons.size>0;
2234
+ var anyActive=activeStatus!==null||activeTags.size>0||activeReasons.size>0||activeSearch.length>0;
2177
2235
  document.querySelector('.clear-filter').classList.toggle('visible',anyActive);
2178
2236
  var visibleTests=0;
2179
2237
  var visibleFiles=Object.create(null);
@@ -2189,7 +2247,12 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2189
2247
  var r=card.getAttribute('data-reason')||'';
2190
2248
  reasonOk=activeReasons.has(r);
2191
2249
  }
2192
- var hide=!(statusOk&&tagsOk&&reasonOk);
2250
+ var searchOk=true;
2251
+ if(activeSearch.length>0){
2252
+ var hay=card.getAttribute('data-search')||'';
2253
+ searchOk=hay.indexOf(activeSearch)!==-1;
2254
+ }
2255
+ var hide=!(statusOk&&tagsOk&&reasonOk&&searchOk);
2193
2256
  card.classList.toggle('hidden-by-filter',hide);
2194
2257
  if(!hide){
2195
2258
  visibleTests++;
@@ -2203,6 +2266,11 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2203
2266
  var card=document.getElementById(block.getAttribute('data-target'));
2204
2267
  block.classList.toggle('hidden-by-filter',!!(card&&card.classList.contains('hidden-by-filter')));
2205
2268
  });
2269
+ // Faceted-search live counts: status pills, and the tag/diagnosis menu
2270
+ // (refreshed if currently open) all reflect "given the other filters,
2271
+ // how many tests would this option contribute".
2272
+ updateStatPillCounts();
2273
+ if(tagMenuOpen())openTagMenu(); // re-render menu items with fresh counts
2206
2274
  // "X tests across Y files" subtitle reflects the current filter result.
2207
2275
  // When no filter is active the form matches the original (no "of Y").
2208
2276
  var sub=document.querySelector('[data-summary-sub]');
@@ -2227,6 +2295,7 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2227
2295
  if(activeStatus)p.set('status',activeStatus);
2228
2296
  activeTags.forEach(function(t){p.append('tag',t)});
2229
2297
  activeReasons.forEach(function(r){p.append('reason',r)});
2298
+ if(activeSearch)p.set('q',activeSearch);
2230
2299
  var qs=p.toString();
2231
2300
  var next=location.pathname+(qs?'?'+qs:'')+(location.hash||'');
2232
2301
  if(next!==location.pathname+location.search+location.hash){
@@ -2277,25 +2346,42 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2277
2346
  function addReason(r){if(!r||activeReasons.has(r))return;activeReasons.add(r);renderActiveChips();applyFilters()}
2278
2347
  function removeReason(r){if(!activeReasons.delete(r))return;renderActiveChips();applyFilters()}
2279
2348
 
2280
- function reasonCount(r){var n=0;document.querySelectorAll('.test-card').forEach(function(c){if(c.getAttribute('data-reason')===r)n++});return n}
2349
+ function reasonCount(r){
2350
+ var pool=cardsMatching(false,false,true);
2351
+ var n=0;for(var i=0;i<pool.length;i++){if(pool[i].getAttribute('data-reason')===r)n++}
2352
+ return n;
2353
+ }
2354
+ function updateStatPillCounts(){
2355
+ var pool=cardsMatching(true,false,false);
2356
+ var counts=Object.create(null);
2357
+ for(var i=0;i<pool.length;i++){var s=pool[i].getAttribute('data-status');counts[s]=(counts[s]||0)+1}
2358
+ document.querySelectorAll('.stat-pill[data-filter]').forEach(function(pill){
2359
+ var key=pill.getAttribute('data-filter');
2360
+ var span=pill.querySelector('.pill-count');
2361
+ if(span)span.textContent=counts[key]||0;
2362
+ });
2363
+ }
2281
2364
 
2282
2365
  function openTagMenu(){
2283
2366
  var menu=document.querySelector('[data-tag-menu]');
2284
2367
  if(!menu)return;
2285
2368
  var trigger=document.querySelector('[data-add-tag-filter]');
2286
2369
  menu.innerHTML='';
2287
- var availTags=allTags.filter(function(t){return !activeTags.has(t)});
2288
- var availReasons=allReasons.filter(function(r){return !activeReasons.has(r)});
2370
+ // For each available tag/reason, compute its preview count under the
2371
+ // current other filters. Options with count 0 are hidden — they'd lead
2372
+ // to an empty view, so they're not useful to offer.
2373
+ var tagsWithCounts=allTags.filter(function(t){return !activeTags.has(t)}).map(function(t){return {key:t,count:tagCount(t)}}).filter(function(x){return x.count>0});
2374
+ var reasonsWithCounts=allReasons.filter(function(r){return !activeReasons.has(r)}).map(function(r){return {key:r,count:reasonCount(r)}}).filter(function(x){return x.count>0});
2289
2375
  var added=false;
2290
2376
  if(allTags.length>0){
2291
2377
  var hT=document.createElement('div');hT.className='tag-menu-section';hT.textContent='Tags';menu.appendChild(hT);
2292
- if(availTags.length===0){
2293
- var emptyT=document.createElement('div');emptyT.className='tag-menu-empty';emptyT.textContent='All tags selected';menu.appendChild(emptyT);
2378
+ if(tagsWithCounts.length===0){
2379
+ var emptyT=document.createElement('div');emptyT.className='tag-menu-empty';emptyT.textContent=allTags.length===activeTags.size?'All tags selected':'No matching tags';menu.appendChild(emptyT);
2294
2380
  }else{
2295
- availTags.forEach(function(t){
2296
- var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-tag-menu-item',t);
2297
- var label=document.createElement('span');label.textContent=t;
2298
- var count=document.createElement('span');count.className='tag-menu-count';count.textContent=tagCount(t);
2381
+ tagsWithCounts.forEach(function(x){
2382
+ var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-tag-menu-item',x.key);
2383
+ var label=document.createElement('span');label.textContent=x.key;
2384
+ var count=document.createElement('span');count.className='tag-menu-count';count.textContent=x.count;
2299
2385
  item.appendChild(label);item.appendChild(count);
2300
2386
  menu.appendChild(item);
2301
2387
  });
@@ -2304,14 +2390,14 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2304
2390
  }
2305
2391
  if(allReasons.length>0){
2306
2392
  var hR=document.createElement('div');hR.className='tag-menu-section';hR.textContent='Diagnoses';menu.appendChild(hR);
2307
- if(availReasons.length===0){
2308
- var emptyR=document.createElement('div');emptyR.className='tag-menu-empty';emptyR.textContent='All diagnoses selected';menu.appendChild(emptyR);
2393
+ if(reasonsWithCounts.length===0){
2394
+ var emptyR=document.createElement('div');emptyR.className='tag-menu-empty';emptyR.textContent=allReasons.length===activeReasons.size?'All diagnoses selected':'No matching diagnoses';menu.appendChild(emptyR);
2309
2395
  }else{
2310
- availReasons.forEach(function(r){
2311
- var meta=REASON_LABELS[r]||REASON_LABELS['UNKNOWN'];
2312
- var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-reason-menu-item',r);
2396
+ reasonsWithCounts.forEach(function(x){
2397
+ var meta=REASON_LABELS[x.key]||REASON_LABELS['UNKNOWN'];
2398
+ var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-reason-menu-item',x.key);
2313
2399
  var label=document.createElement('span');label.textContent=meta.label;label.style.color=meta.color;
2314
- var count=document.createElement('span');count.className='tag-menu-count';count.textContent=reasonCount(r);
2400
+ var count=document.createElement('span');count.className='tag-menu-count';count.textContent=x.count;
2315
2401
  item.appendChild(label);item.appendChild(count);
2316
2402
  menu.appendChild(item);
2317
2403
  });
@@ -2335,7 +2421,10 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2335
2421
  activeStatus=null;
2336
2422
  activeTags.clear();
2337
2423
  activeReasons.clear();
2424
+ activeSearch='';
2338
2425
  document.querySelectorAll('.stat-pill').forEach(function(p){p.classList.remove('active')});
2426
+ var searchInput=document.querySelector('[data-filter-search]');
2427
+ if(searchInput)searchInput.value='';
2339
2428
  renderActiveChips();
2340
2429
  closeTagMenu();
2341
2430
  applyFilters();
@@ -2365,7 +2454,9 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2365
2454
  if(el){navigator.clipboard.writeText(el.textContent);var s=copyBtn.innerHTML;copyBtn.innerHTML='<svg class="check-icon" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>';setTimeout(function(){copyBtn.innerHTML=s},2000)}
2366
2455
  return;
2367
2456
  }
2368
- // Copy buttons (flow ID + JSON)
2457
+ // Copy buttons (flow ID + JSON + inline text via data-copy-text)
2458
+ var copyTextBtn=e.target.closest('.copy-text[data-copy-text]');
2459
+ if(copyTextBtn){e.stopPropagation();e.preventDefault();navigator.clipboard.writeText(copyTextBtn.getAttribute('data-copy-text'));var copyTextSvg=copyTextBtn.innerHTML;copyTextBtn.innerHTML='<svg class="check-icon" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>';setTimeout(function(){copyTextBtn.innerHTML=copyTextSvg},2000);return}
2369
2460
  var flowBtn=e.target.closest('.copy-flow-id');
2370
2461
  if(flowBtn){e.stopPropagation();navigator.clipboard.writeText(flowBtn.getAttribute('data-flow-id'));var copySvg=flowBtn.innerHTML;flowBtn.innerHTML='<svg class="check-icon" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>';setTimeout(function(){flowBtn.innerHTML=copySvg},2000);return}
2371
2462
  var jsonBtn=e.target.closest('.copy-json');
@@ -2463,8 +2554,20 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2463
2554
  p.getAll('tag').forEach(function(t){if(tagSet[t])activeTags.add(t)});
2464
2555
  var reasonSet={};allReasons.forEach(function(r){reasonSet[r]=true});
2465
2556
  p.getAll('reason').forEach(function(r){if(reasonSet[r])activeReasons.add(r)});
2557
+ var q=p.get('q');
2558
+ var searchInput=document.querySelector('[data-filter-search]');
2559
+ if(q){
2560
+ activeSearch=q.toLowerCase();
2561
+ if(searchInput)searchInput.value=q;
2562
+ }
2563
+ if(searchInput){
2564
+ searchInput.addEventListener('input',function(){
2565
+ activeSearch=searchInput.value.trim().toLowerCase();
2566
+ applyFilters();
2567
+ });
2568
+ }
2466
2569
  if(activeTags.size>0||activeReasons.size>0)renderActiveChips();
2467
- if(activeStatus!==null||activeTags.size>0||activeReasons.size>0)applyFilters();
2570
+ if(activeStatus!==null||activeTags.size>0||activeReasons.size>0||activeSearch.length>0)applyFilters();
2468
2571
  })();
2469
2572
 
2470
2573
  // Open #?testId=<id> deep links to the matching test card. Used by the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "donobu",
3
- "version": "5.43.2",
3
+ "version": "5.46.0",
4
4
  "description": "Create browser automations with an LLM agent and replay them as Playwright scripts.",
5
5
  "main": "dist/main.js",
6
6
  "module": "dist/esm/main.js",