donobu 5.43.1 → 5.45.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.
@@ -1678,7 +1678,7 @@ function renderHtml(report, triage, outputDir) {
1678
1678
  ? `<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
1679
  : '';
1680
1680
  testSectionsHtml += `
1681
- <div class="test-card ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" id="${testId}" data-status="${test.status}" data-tags="${esc(JSON.stringify(test.tags))}"${test.plan ? ` data-reason="${esc(test.plan.plan.failureReason)}"` : ''} ${hasDetails ? `data-detail="${testId}"` : ''}>
1681
+ <div class="test-card ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" id="${testId}" data-status="${test.status}" data-file="${esc(test.file)}" data-search="${esc((displayFileName + ' ' + test.specTitle).toLowerCase())}" data-tags="${esc(JSON.stringify(test.tags))}"${test.plan ? ` data-reason="${esc(test.plan.plan.failureReason)}"` : ''} ${hasDetails ? `data-detail="${testId}"` : ''}>
1682
1682
  <div class="test-summary">
1683
1683
  ${chevron}
1684
1684
  <span class="status-dot" style="background:${sc.color}" title="${sc.label}"></span>
@@ -1761,6 +1761,7 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
1761
1761
  .test-bar-block.bar-failed{background:var(--bar-fail)}
1762
1762
  .test-bar-block.bar-timedout,.test-bar-block.bar-interrupted{background:var(--bar-warn)}
1763
1763
  .test-bar-block.bar-skipped{background:var(--bar-skip)}
1764
+ .test-bar-block.hidden-by-filter{display:none}
1764
1765
  .summary-stats{display:flex;align-items:center;gap:8px;padding:12px 0}
1765
1766
  .stat-pills{display:flex;gap:6px;flex-wrap:wrap;flex:1}
1766
1767
  .stat-pill{display:flex;align-items:center;gap:8px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:6px 14px;cursor:pointer;font-size:13px;font-weight:600;font-family:inherit;color:var(--text-muted);transition:all .2s}
@@ -1768,6 +1769,17 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
1768
1769
  .stat-pill.active{background:var(--accent);border-color:var(--accent);color:#fff}
1769
1770
  .stat-pill.active .pill-count{background:rgba(255,255,255,.25);color:#fff}
1770
1771
  .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}
1772
+ /* Substring search across test filename + spec title. Lives in the same row
1773
+ * as the stat pills and tag/diagnosis filter. */
1774
+ .filter-search-wrap{position:relative;display:inline-flex;align-items:center;flex-shrink:0}
1775
+ .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}
1776
+ .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}
1777
+ .filter-search::placeholder{color:var(--text-dim)}
1778
+ .filter-search:hover{border-color:var(--text-dim)}
1779
+ .filter-search:focus{border-color:var(--accent);background:var(--surface-raised)}
1780
+ /* Hide the WebKit search clear "x" — Clear Filters wipes it via the same UI. */
1781
+ .filter-search::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}
1782
+
1771
1783
  .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}
1772
1784
  .clear-filter:hover{background:var(--surface-raised);border-color:var(--text-dim);color:var(--text)}
1773
1785
  .clear-filter.visible{display:flex}
@@ -1793,12 +1805,6 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
1793
1805
  .tag-chip-remove{background:transparent;border:none;color:inherit;cursor:pointer;font-size:14px;line-height:1;padding:0 4px;font-family:inherit;opacity:.7;transition:opacity .15s}
1794
1806
  .tag-chip-remove:hover{opacity:1}
1795
1807
 
1796
- /* Total of cards visible under the currently composed filters (status + tags).
1797
- * The stat-pill counts always reflect totals; this disambiguates when filters
1798
- * intersect. Hidden until any filter is active. */
1799
- .match-count{display:none;align-items:center;font-size:12px;color:var(--text-muted);font-family:var(--mono);padding:0 8px;height:28px;flex-shrink:0}
1800
- .match-count.visible{display:inline-flex}
1801
- .match-count-value{color:var(--text);font-weight:600;margin-left:6px}
1802
1808
 
1803
1809
  /* Test cards */
1804
1810
  .test-card{background:var(--surface);border:1px solid var(--border-subtle);border-radius:var(--radius-lg);margin-bottom:10px;overflow:hidden}
@@ -2132,10 +2138,14 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2132
2138
  ${mergedBanner}
2133
2139
 
2134
2140
  <div class="summary-card">
2135
- <div class="summary-sub">${total} test${total !== 1 ? 's' : ''} across ${uniqueFiles.size} file${uniqueFiles.size !== 1 ? 's' : ''}</div>
2141
+ <div class="summary-sub" data-summary-sub data-total-tests="${total}" data-total-files="${uniqueFiles.size}">${total} test${total !== 1 ? 's' : ''} across ${uniqueFiles.size} file${uniqueFiles.size !== 1 ? 's' : ''}</div>
2136
2142
  <div class="test-bar">${testBarHtml}</div>
2137
2143
  <div class="summary-stats">
2138
2144
  <div class="stat-pills">${statPillsHtml}</div>
2145
+ <label class="filter-search-wrap" title="Search test titles">
2146
+ <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>
2147
+ <input type="search" class="filter-search" data-filter-search placeholder="Search tests…" autocomplete="off" spellcheck="false" />
2148
+ </label>
2139
2149
  <div class="tag-filter-controls" data-tag-filter-controls hidden>
2140
2150
  <div class="tag-filter-trigger-wrap">
2141
2151
  <button class="add-tag-filter" data-add-tag-filter title="Filter by tag or diagnosis"><span class="add-tag-plus">+</span> Filter</button>
@@ -2143,7 +2153,6 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2143
2153
  </div>
2144
2154
  <div class="active-tag-filters" data-active-tag-filters></div>
2145
2155
  </div>
2146
- <span class="match-count" data-match-count>Matches:<span class="match-count-value" data-match-count-value>0</span></span>
2147
2156
  <button class="clear-filter" data-clear-filter>&#x2716; Clear Filters</button>
2148
2157
  </div>
2149
2158
  </div>
@@ -2171,17 +2180,46 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2171
2180
  var activeStatus=null;
2172
2181
  var activeTags=new Set();
2173
2182
  var activeReasons=new Set();
2183
+ var activeSearch=''; // lowercase substring match against data-search
2174
2184
  var allTags=[];
2175
2185
  var allReasons=[]; // ordered list of REASON keys present in the report
2176
2186
  var REASON_LABELS=${JSON.stringify(REASON_LABELS)};
2177
2187
 
2178
2188
  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 []}}
2179
- function tagCount(t){var n=0;document.querySelectorAll('.test-card').forEach(function(c){if(cardTags(c).indexOf(t)!==-1)n++});return n}
2189
+
2190
+ // Faceted-search counts. Each filter option's badge shows "how many tests
2191
+ // would this option contribute given the rest of the filters." The semantics
2192
+ // per dimension match how clicking interacts:
2193
+ // - Status pills (single-select replace): ignore current activeStatus.
2194
+ // - Tag menu items (multi-select AND): use ALL current filters.
2195
+ // - Reason menu items (multi-select OR): ignore current activeReasons.
2196
+ // Search is free-form and not counted.
2197
+ function cardsMatching(ignoreStatus,ignoreTags,ignoreReasons){
2198
+ var out=[];
2199
+ document.querySelectorAll('.test-card').forEach(function(card){
2200
+ var statusOk=ignoreStatus||activeStatus===null||card.getAttribute('data-status')===activeStatus;
2201
+ var tagsOk=true;
2202
+ if(!ignoreTags&&activeTags.size>0){
2203
+ var t=cardTags(card);
2204
+ activeTags.forEach(function(w){if(t.indexOf(w)===-1)tagsOk=false});
2205
+ }
2206
+ var reasonOk=ignoreReasons||activeReasons.size===0||activeReasons.has(card.getAttribute('data-reason')||'');
2207
+ var searchOk=activeSearch.length===0||(card.getAttribute('data-search')||'').indexOf(activeSearch)!==-1;
2208
+ if(statusOk&&tagsOk&&reasonOk&&searchOk)out.push(card);
2209
+ });
2210
+ return out;
2211
+ }
2212
+ function tagCount(t){
2213
+ var pool=cardsMatching(false,false,false);
2214
+ var n=0;for(var i=0;i<pool.length;i++){if(cardTags(pool[i]).indexOf(t)!==-1)n++}
2215
+ return n;
2216
+ }
2180
2217
 
2181
2218
  function applyFilters(){
2182
- var anyActive=activeStatus!==null||activeTags.size>0||activeReasons.size>0;
2219
+ var anyActive=activeStatus!==null||activeTags.size>0||activeReasons.size>0||activeSearch.length>0;
2183
2220
  document.querySelector('.clear-filter').classList.toggle('visible',anyActive);
2184
- var visible=0;
2221
+ var visibleTests=0;
2222
+ var visibleFiles=Object.create(null);
2185
2223
  document.querySelectorAll('.test-card').forEach(function(card){
2186
2224
  var statusOk=activeStatus===null||card.getAttribute('data-status')===activeStatus;
2187
2225
  var tagsOk=true;
@@ -2194,15 +2232,42 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2194
2232
  var r=card.getAttribute('data-reason')||'';
2195
2233
  reasonOk=activeReasons.has(r);
2196
2234
  }
2197
- var hide=!(statusOk&&tagsOk&&reasonOk);
2235
+ var searchOk=true;
2236
+ if(activeSearch.length>0){
2237
+ var hay=card.getAttribute('data-search')||'';
2238
+ searchOk=hay.indexOf(activeSearch)!==-1;
2239
+ }
2240
+ var hide=!(statusOk&&tagsOk&&reasonOk&&searchOk);
2198
2241
  card.classList.toggle('hidden-by-filter',hide);
2199
- if(!hide)visible++;
2242
+ if(!hide){
2243
+ visibleTests++;
2244
+ var f=card.getAttribute('data-file');
2245
+ if(f)visibleFiles[f]=true;
2246
+ }
2247
+ });
2248
+ // Mirror visibility onto the colored test-bar squares so the bar collapses
2249
+ // to the matching subset rather than dangling stale tiles.
2250
+ document.querySelectorAll('.test-bar-block[data-target]').forEach(function(block){
2251
+ var card=document.getElementById(block.getAttribute('data-target'));
2252
+ block.classList.toggle('hidden-by-filter',!!(card&&card.classList.contains('hidden-by-filter')));
2200
2253
  });
2201
- var mc=document.querySelector('[data-match-count]');
2202
- if(mc){
2203
- mc.classList.toggle('visible',anyActive);
2204
- var mv=mc.querySelector('[data-match-count-value]');
2205
- if(mv)mv.textContent=visible;
2254
+ // Faceted-search live counts: status pills, and the tag/diagnosis menu
2255
+ // (refreshed if currently open) all reflect "given the other filters,
2256
+ // how many tests would this option contribute".
2257
+ updateStatPillCounts();
2258
+ if(tagMenuOpen())openTagMenu(); // re-render menu items with fresh counts
2259
+ // "X tests across Y files" subtitle reflects the current filter result.
2260
+ // When no filter is active the form matches the original (no "of Y").
2261
+ var sub=document.querySelector('[data-summary-sub]');
2262
+ if(sub){
2263
+ var totalTests=parseInt(sub.getAttribute('data-total-tests'),10)||0;
2264
+ var totalFiles=parseInt(sub.getAttribute('data-total-files'),10)||0;
2265
+ var visFiles=Object.keys(visibleFiles).length;
2266
+ if(anyActive){
2267
+ sub.textContent=visibleTests+' of '+totalTests+' test'+(totalTests!==1?'s':'')+' across '+visFiles+' of '+totalFiles+' file'+(totalFiles!==1?'s':'');
2268
+ }else{
2269
+ sub.textContent=totalTests+' test'+(totalTests!==1?'s':'')+' across '+totalFiles+' file'+(totalFiles!==1?'s':'');
2270
+ }
2206
2271
  }
2207
2272
  syncFiltersToUrl();
2208
2273
  }
@@ -2215,6 +2280,7 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2215
2280
  if(activeStatus)p.set('status',activeStatus);
2216
2281
  activeTags.forEach(function(t){p.append('tag',t)});
2217
2282
  activeReasons.forEach(function(r){p.append('reason',r)});
2283
+ if(activeSearch)p.set('q',activeSearch);
2218
2284
  var qs=p.toString();
2219
2285
  var next=location.pathname+(qs?'?'+qs:'')+(location.hash||'');
2220
2286
  if(next!==location.pathname+location.search+location.hash){
@@ -2265,25 +2331,42 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2265
2331
  function addReason(r){if(!r||activeReasons.has(r))return;activeReasons.add(r);renderActiveChips();applyFilters()}
2266
2332
  function removeReason(r){if(!activeReasons.delete(r))return;renderActiveChips();applyFilters()}
2267
2333
 
2268
- function reasonCount(r){var n=0;document.querySelectorAll('.test-card').forEach(function(c){if(c.getAttribute('data-reason')===r)n++});return n}
2334
+ function reasonCount(r){
2335
+ var pool=cardsMatching(false,false,true);
2336
+ var n=0;for(var i=0;i<pool.length;i++){if(pool[i].getAttribute('data-reason')===r)n++}
2337
+ return n;
2338
+ }
2339
+ function updateStatPillCounts(){
2340
+ var pool=cardsMatching(true,false,false);
2341
+ var counts=Object.create(null);
2342
+ for(var i=0;i<pool.length;i++){var s=pool[i].getAttribute('data-status');counts[s]=(counts[s]||0)+1}
2343
+ document.querySelectorAll('.stat-pill[data-filter]').forEach(function(pill){
2344
+ var key=pill.getAttribute('data-filter');
2345
+ var span=pill.querySelector('.pill-count');
2346
+ if(span)span.textContent=counts[key]||0;
2347
+ });
2348
+ }
2269
2349
 
2270
2350
  function openTagMenu(){
2271
2351
  var menu=document.querySelector('[data-tag-menu]');
2272
2352
  if(!menu)return;
2273
2353
  var trigger=document.querySelector('[data-add-tag-filter]');
2274
2354
  menu.innerHTML='';
2275
- var availTags=allTags.filter(function(t){return !activeTags.has(t)});
2276
- var availReasons=allReasons.filter(function(r){return !activeReasons.has(r)});
2355
+ // For each available tag/reason, compute its preview count under the
2356
+ // current other filters. Options with count 0 are hidden — they'd lead
2357
+ // to an empty view, so they're not useful to offer.
2358
+ 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});
2359
+ 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});
2277
2360
  var added=false;
2278
2361
  if(allTags.length>0){
2279
2362
  var hT=document.createElement('div');hT.className='tag-menu-section';hT.textContent='Tags';menu.appendChild(hT);
2280
- if(availTags.length===0){
2281
- var emptyT=document.createElement('div');emptyT.className='tag-menu-empty';emptyT.textContent='All tags selected';menu.appendChild(emptyT);
2363
+ if(tagsWithCounts.length===0){
2364
+ 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);
2282
2365
  }else{
2283
- availTags.forEach(function(t){
2284
- var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-tag-menu-item',t);
2285
- var label=document.createElement('span');label.textContent=t;
2286
- var count=document.createElement('span');count.className='tag-menu-count';count.textContent=tagCount(t);
2366
+ tagsWithCounts.forEach(function(x){
2367
+ var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-tag-menu-item',x.key);
2368
+ var label=document.createElement('span');label.textContent=x.key;
2369
+ var count=document.createElement('span');count.className='tag-menu-count';count.textContent=x.count;
2287
2370
  item.appendChild(label);item.appendChild(count);
2288
2371
  menu.appendChild(item);
2289
2372
  });
@@ -2292,14 +2375,14 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2292
2375
  }
2293
2376
  if(allReasons.length>0){
2294
2377
  var hR=document.createElement('div');hR.className='tag-menu-section';hR.textContent='Diagnoses';menu.appendChild(hR);
2295
- if(availReasons.length===0){
2296
- var emptyR=document.createElement('div');emptyR.className='tag-menu-empty';emptyR.textContent='All diagnoses selected';menu.appendChild(emptyR);
2378
+ if(reasonsWithCounts.length===0){
2379
+ 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);
2297
2380
  }else{
2298
- availReasons.forEach(function(r){
2299
- var meta=REASON_LABELS[r]||REASON_LABELS['UNKNOWN'];
2300
- var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-reason-menu-item',r);
2381
+ reasonsWithCounts.forEach(function(x){
2382
+ var meta=REASON_LABELS[x.key]||REASON_LABELS['UNKNOWN'];
2383
+ var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-reason-menu-item',x.key);
2301
2384
  var label=document.createElement('span');label.textContent=meta.label;label.style.color=meta.color;
2302
- var count=document.createElement('span');count.className='tag-menu-count';count.textContent=reasonCount(r);
2385
+ var count=document.createElement('span');count.className='tag-menu-count';count.textContent=x.count;
2303
2386
  item.appendChild(label);item.appendChild(count);
2304
2387
  menu.appendChild(item);
2305
2388
  });
@@ -2323,7 +2406,10 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2323
2406
  activeStatus=null;
2324
2407
  activeTags.clear();
2325
2408
  activeReasons.clear();
2409
+ activeSearch='';
2326
2410
  document.querySelectorAll('.stat-pill').forEach(function(p){p.classList.remove('active')});
2411
+ var searchInput=document.querySelector('[data-filter-search]');
2412
+ if(searchInput)searchInput.value='';
2327
2413
  renderActiveChips();
2328
2414
  closeTagMenu();
2329
2415
  applyFilters();
@@ -2451,8 +2537,20 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2451
2537
  p.getAll('tag').forEach(function(t){if(tagSet[t])activeTags.add(t)});
2452
2538
  var reasonSet={};allReasons.forEach(function(r){reasonSet[r]=true});
2453
2539
  p.getAll('reason').forEach(function(r){if(reasonSet[r])activeReasons.add(r)});
2540
+ var q=p.get('q');
2541
+ var searchInput=document.querySelector('[data-filter-search]');
2542
+ if(q){
2543
+ activeSearch=q.toLowerCase();
2544
+ if(searchInput)searchInput.value=q;
2545
+ }
2546
+ if(searchInput){
2547
+ searchInput.addEventListener('input',function(){
2548
+ activeSearch=searchInput.value.trim().toLowerCase();
2549
+ applyFilters();
2550
+ });
2551
+ }
2454
2552
  if(activeTags.size>0||activeReasons.size>0)renderActiveChips();
2455
- if(activeStatus!==null||activeTags.size>0||activeReasons.size>0)applyFilters();
2553
+ if(activeStatus!==null||activeTags.size>0||activeReasons.size>0||activeSearch.length>0)applyFilters();
2456
2554
  })();
2457
2555
 
2458
2556
  // Open #?testId=<id> deep links to the matching test card. Used by the
@@ -1678,7 +1678,7 @@ function renderHtml(report, triage, outputDir) {
1678
1678
  ? `<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
1679
  : '';
1680
1680
  testSectionsHtml += `
1681
- <div class="test-card ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" id="${testId}" data-status="${test.status}" data-tags="${esc(JSON.stringify(test.tags))}"${test.plan ? ` data-reason="${esc(test.plan.plan.failureReason)}"` : ''} ${hasDetails ? `data-detail="${testId}"` : ''}>
1681
+ <div class="test-card ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" id="${testId}" data-status="${test.status}" data-file="${esc(test.file)}" data-search="${esc((displayFileName + ' ' + test.specTitle).toLowerCase())}" data-tags="${esc(JSON.stringify(test.tags))}"${test.plan ? ` data-reason="${esc(test.plan.plan.failureReason)}"` : ''} ${hasDetails ? `data-detail="${testId}"` : ''}>
1682
1682
  <div class="test-summary">
1683
1683
  ${chevron}
1684
1684
  <span class="status-dot" style="background:${sc.color}" title="${sc.label}"></span>
@@ -1761,6 +1761,7 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
1761
1761
  .test-bar-block.bar-failed{background:var(--bar-fail)}
1762
1762
  .test-bar-block.bar-timedout,.test-bar-block.bar-interrupted{background:var(--bar-warn)}
1763
1763
  .test-bar-block.bar-skipped{background:var(--bar-skip)}
1764
+ .test-bar-block.hidden-by-filter{display:none}
1764
1765
  .summary-stats{display:flex;align-items:center;gap:8px;padding:12px 0}
1765
1766
  .stat-pills{display:flex;gap:6px;flex-wrap:wrap;flex:1}
1766
1767
  .stat-pill{display:flex;align-items:center;gap:8px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:6px 14px;cursor:pointer;font-size:13px;font-weight:600;font-family:inherit;color:var(--text-muted);transition:all .2s}
@@ -1768,6 +1769,17 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
1768
1769
  .stat-pill.active{background:var(--accent);border-color:var(--accent);color:#fff}
1769
1770
  .stat-pill.active .pill-count{background:rgba(255,255,255,.25);color:#fff}
1770
1771
  .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}
1772
+ /* Substring search across test filename + spec title. Lives in the same row
1773
+ * as the stat pills and tag/diagnosis filter. */
1774
+ .filter-search-wrap{position:relative;display:inline-flex;align-items:center;flex-shrink:0}
1775
+ .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}
1776
+ .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}
1777
+ .filter-search::placeholder{color:var(--text-dim)}
1778
+ .filter-search:hover{border-color:var(--text-dim)}
1779
+ .filter-search:focus{border-color:var(--accent);background:var(--surface-raised)}
1780
+ /* Hide the WebKit search clear "x" — Clear Filters wipes it via the same UI. */
1781
+ .filter-search::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}
1782
+
1771
1783
  .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}
1772
1784
  .clear-filter:hover{background:var(--surface-raised);border-color:var(--text-dim);color:var(--text)}
1773
1785
  .clear-filter.visible{display:flex}
@@ -1793,12 +1805,6 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
1793
1805
  .tag-chip-remove{background:transparent;border:none;color:inherit;cursor:pointer;font-size:14px;line-height:1;padding:0 4px;font-family:inherit;opacity:.7;transition:opacity .15s}
1794
1806
  .tag-chip-remove:hover{opacity:1}
1795
1807
 
1796
- /* Total of cards visible under the currently composed filters (status + tags).
1797
- * The stat-pill counts always reflect totals; this disambiguates when filters
1798
- * intersect. Hidden until any filter is active. */
1799
- .match-count{display:none;align-items:center;font-size:12px;color:var(--text-muted);font-family:var(--mono);padding:0 8px;height:28px;flex-shrink:0}
1800
- .match-count.visible{display:inline-flex}
1801
- .match-count-value{color:var(--text);font-weight:600;margin-left:6px}
1802
1808
 
1803
1809
  /* Test cards */
1804
1810
  .test-card{background:var(--surface);border:1px solid var(--border-subtle);border-radius:var(--radius-lg);margin-bottom:10px;overflow:hidden}
@@ -2132,10 +2138,14 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2132
2138
  ${mergedBanner}
2133
2139
 
2134
2140
  <div class="summary-card">
2135
- <div class="summary-sub">${total} test${total !== 1 ? 's' : ''} across ${uniqueFiles.size} file${uniqueFiles.size !== 1 ? 's' : ''}</div>
2141
+ <div class="summary-sub" data-summary-sub data-total-tests="${total}" data-total-files="${uniqueFiles.size}">${total} test${total !== 1 ? 's' : ''} across ${uniqueFiles.size} file${uniqueFiles.size !== 1 ? 's' : ''}</div>
2136
2142
  <div class="test-bar">${testBarHtml}</div>
2137
2143
  <div class="summary-stats">
2138
2144
  <div class="stat-pills">${statPillsHtml}</div>
2145
+ <label class="filter-search-wrap" title="Search test titles">
2146
+ <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>
2147
+ <input type="search" class="filter-search" data-filter-search placeholder="Search tests…" autocomplete="off" spellcheck="false" />
2148
+ </label>
2139
2149
  <div class="tag-filter-controls" data-tag-filter-controls hidden>
2140
2150
  <div class="tag-filter-trigger-wrap">
2141
2151
  <button class="add-tag-filter" data-add-tag-filter title="Filter by tag or diagnosis"><span class="add-tag-plus">+</span> Filter</button>
@@ -2143,7 +2153,6 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2143
2153
  </div>
2144
2154
  <div class="active-tag-filters" data-active-tag-filters></div>
2145
2155
  </div>
2146
- <span class="match-count" data-match-count>Matches:<span class="match-count-value" data-match-count-value>0</span></span>
2147
2156
  <button class="clear-filter" data-clear-filter>&#x2716; Clear Filters</button>
2148
2157
  </div>
2149
2158
  </div>
@@ -2171,17 +2180,46 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2171
2180
  var activeStatus=null;
2172
2181
  var activeTags=new Set();
2173
2182
  var activeReasons=new Set();
2183
+ var activeSearch=''; // lowercase substring match against data-search
2174
2184
  var allTags=[];
2175
2185
  var allReasons=[]; // ordered list of REASON keys present in the report
2176
2186
  var REASON_LABELS=${JSON.stringify(REASON_LABELS)};
2177
2187
 
2178
2188
  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 []}}
2179
- function tagCount(t){var n=0;document.querySelectorAll('.test-card').forEach(function(c){if(cardTags(c).indexOf(t)!==-1)n++});return n}
2189
+
2190
+ // Faceted-search counts. Each filter option's badge shows "how many tests
2191
+ // would this option contribute given the rest of the filters." The semantics
2192
+ // per dimension match how clicking interacts:
2193
+ // - Status pills (single-select replace): ignore current activeStatus.
2194
+ // - Tag menu items (multi-select AND): use ALL current filters.
2195
+ // - Reason menu items (multi-select OR): ignore current activeReasons.
2196
+ // Search is free-form and not counted.
2197
+ function cardsMatching(ignoreStatus,ignoreTags,ignoreReasons){
2198
+ var out=[];
2199
+ document.querySelectorAll('.test-card').forEach(function(card){
2200
+ var statusOk=ignoreStatus||activeStatus===null||card.getAttribute('data-status')===activeStatus;
2201
+ var tagsOk=true;
2202
+ if(!ignoreTags&&activeTags.size>0){
2203
+ var t=cardTags(card);
2204
+ activeTags.forEach(function(w){if(t.indexOf(w)===-1)tagsOk=false});
2205
+ }
2206
+ var reasonOk=ignoreReasons||activeReasons.size===0||activeReasons.has(card.getAttribute('data-reason')||'');
2207
+ var searchOk=activeSearch.length===0||(card.getAttribute('data-search')||'').indexOf(activeSearch)!==-1;
2208
+ if(statusOk&&tagsOk&&reasonOk&&searchOk)out.push(card);
2209
+ });
2210
+ return out;
2211
+ }
2212
+ function tagCount(t){
2213
+ var pool=cardsMatching(false,false,false);
2214
+ var n=0;for(var i=0;i<pool.length;i++){if(cardTags(pool[i]).indexOf(t)!==-1)n++}
2215
+ return n;
2216
+ }
2180
2217
 
2181
2218
  function applyFilters(){
2182
- var anyActive=activeStatus!==null||activeTags.size>0||activeReasons.size>0;
2219
+ var anyActive=activeStatus!==null||activeTags.size>0||activeReasons.size>0||activeSearch.length>0;
2183
2220
  document.querySelector('.clear-filter').classList.toggle('visible',anyActive);
2184
- var visible=0;
2221
+ var visibleTests=0;
2222
+ var visibleFiles=Object.create(null);
2185
2223
  document.querySelectorAll('.test-card').forEach(function(card){
2186
2224
  var statusOk=activeStatus===null||card.getAttribute('data-status')===activeStatus;
2187
2225
  var tagsOk=true;
@@ -2194,15 +2232,42 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2194
2232
  var r=card.getAttribute('data-reason')||'';
2195
2233
  reasonOk=activeReasons.has(r);
2196
2234
  }
2197
- var hide=!(statusOk&&tagsOk&&reasonOk);
2235
+ var searchOk=true;
2236
+ if(activeSearch.length>0){
2237
+ var hay=card.getAttribute('data-search')||'';
2238
+ searchOk=hay.indexOf(activeSearch)!==-1;
2239
+ }
2240
+ var hide=!(statusOk&&tagsOk&&reasonOk&&searchOk);
2198
2241
  card.classList.toggle('hidden-by-filter',hide);
2199
- if(!hide)visible++;
2242
+ if(!hide){
2243
+ visibleTests++;
2244
+ var f=card.getAttribute('data-file');
2245
+ if(f)visibleFiles[f]=true;
2246
+ }
2247
+ });
2248
+ // Mirror visibility onto the colored test-bar squares so the bar collapses
2249
+ // to the matching subset rather than dangling stale tiles.
2250
+ document.querySelectorAll('.test-bar-block[data-target]').forEach(function(block){
2251
+ var card=document.getElementById(block.getAttribute('data-target'));
2252
+ block.classList.toggle('hidden-by-filter',!!(card&&card.classList.contains('hidden-by-filter')));
2200
2253
  });
2201
- var mc=document.querySelector('[data-match-count]');
2202
- if(mc){
2203
- mc.classList.toggle('visible',anyActive);
2204
- var mv=mc.querySelector('[data-match-count-value]');
2205
- if(mv)mv.textContent=visible;
2254
+ // Faceted-search live counts: status pills, and the tag/diagnosis menu
2255
+ // (refreshed if currently open) all reflect "given the other filters,
2256
+ // how many tests would this option contribute".
2257
+ updateStatPillCounts();
2258
+ if(tagMenuOpen())openTagMenu(); // re-render menu items with fresh counts
2259
+ // "X tests across Y files" subtitle reflects the current filter result.
2260
+ // When no filter is active the form matches the original (no "of Y").
2261
+ var sub=document.querySelector('[data-summary-sub]');
2262
+ if(sub){
2263
+ var totalTests=parseInt(sub.getAttribute('data-total-tests'),10)||0;
2264
+ var totalFiles=parseInt(sub.getAttribute('data-total-files'),10)||0;
2265
+ var visFiles=Object.keys(visibleFiles).length;
2266
+ if(anyActive){
2267
+ sub.textContent=visibleTests+' of '+totalTests+' test'+(totalTests!==1?'s':'')+' across '+visFiles+' of '+totalFiles+' file'+(totalFiles!==1?'s':'');
2268
+ }else{
2269
+ sub.textContent=totalTests+' test'+(totalTests!==1?'s':'')+' across '+totalFiles+' file'+(totalFiles!==1?'s':'');
2270
+ }
2206
2271
  }
2207
2272
  syncFiltersToUrl();
2208
2273
  }
@@ -2215,6 +2280,7 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2215
2280
  if(activeStatus)p.set('status',activeStatus);
2216
2281
  activeTags.forEach(function(t){p.append('tag',t)});
2217
2282
  activeReasons.forEach(function(r){p.append('reason',r)});
2283
+ if(activeSearch)p.set('q',activeSearch);
2218
2284
  var qs=p.toString();
2219
2285
  var next=location.pathname+(qs?'?'+qs:'')+(location.hash||'');
2220
2286
  if(next!==location.pathname+location.search+location.hash){
@@ -2265,25 +2331,42 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2265
2331
  function addReason(r){if(!r||activeReasons.has(r))return;activeReasons.add(r);renderActiveChips();applyFilters()}
2266
2332
  function removeReason(r){if(!activeReasons.delete(r))return;renderActiveChips();applyFilters()}
2267
2333
 
2268
- function reasonCount(r){var n=0;document.querySelectorAll('.test-card').forEach(function(c){if(c.getAttribute('data-reason')===r)n++});return n}
2334
+ function reasonCount(r){
2335
+ var pool=cardsMatching(false,false,true);
2336
+ var n=0;for(var i=0;i<pool.length;i++){if(pool[i].getAttribute('data-reason')===r)n++}
2337
+ return n;
2338
+ }
2339
+ function updateStatPillCounts(){
2340
+ var pool=cardsMatching(true,false,false);
2341
+ var counts=Object.create(null);
2342
+ for(var i=0;i<pool.length;i++){var s=pool[i].getAttribute('data-status');counts[s]=(counts[s]||0)+1}
2343
+ document.querySelectorAll('.stat-pill[data-filter]').forEach(function(pill){
2344
+ var key=pill.getAttribute('data-filter');
2345
+ var span=pill.querySelector('.pill-count');
2346
+ if(span)span.textContent=counts[key]||0;
2347
+ });
2348
+ }
2269
2349
 
2270
2350
  function openTagMenu(){
2271
2351
  var menu=document.querySelector('[data-tag-menu]');
2272
2352
  if(!menu)return;
2273
2353
  var trigger=document.querySelector('[data-add-tag-filter]');
2274
2354
  menu.innerHTML='';
2275
- var availTags=allTags.filter(function(t){return !activeTags.has(t)});
2276
- var availReasons=allReasons.filter(function(r){return !activeReasons.has(r)});
2355
+ // For each available tag/reason, compute its preview count under the
2356
+ // current other filters. Options with count 0 are hidden — they'd lead
2357
+ // to an empty view, so they're not useful to offer.
2358
+ 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});
2359
+ 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});
2277
2360
  var added=false;
2278
2361
  if(allTags.length>0){
2279
2362
  var hT=document.createElement('div');hT.className='tag-menu-section';hT.textContent='Tags';menu.appendChild(hT);
2280
- if(availTags.length===0){
2281
- var emptyT=document.createElement('div');emptyT.className='tag-menu-empty';emptyT.textContent='All tags selected';menu.appendChild(emptyT);
2363
+ if(tagsWithCounts.length===0){
2364
+ 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);
2282
2365
  }else{
2283
- availTags.forEach(function(t){
2284
- var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-tag-menu-item',t);
2285
- var label=document.createElement('span');label.textContent=t;
2286
- var count=document.createElement('span');count.className='tag-menu-count';count.textContent=tagCount(t);
2366
+ tagsWithCounts.forEach(function(x){
2367
+ var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-tag-menu-item',x.key);
2368
+ var label=document.createElement('span');label.textContent=x.key;
2369
+ var count=document.createElement('span');count.className='tag-menu-count';count.textContent=x.count;
2287
2370
  item.appendChild(label);item.appendChild(count);
2288
2371
  menu.appendChild(item);
2289
2372
  });
@@ -2292,14 +2375,14 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2292
2375
  }
2293
2376
  if(allReasons.length>0){
2294
2377
  var hR=document.createElement('div');hR.className='tag-menu-section';hR.textContent='Diagnoses';menu.appendChild(hR);
2295
- if(availReasons.length===0){
2296
- var emptyR=document.createElement('div');emptyR.className='tag-menu-empty';emptyR.textContent='All diagnoses selected';menu.appendChild(emptyR);
2378
+ if(reasonsWithCounts.length===0){
2379
+ 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);
2297
2380
  }else{
2298
- availReasons.forEach(function(r){
2299
- var meta=REASON_LABELS[r]||REASON_LABELS['UNKNOWN'];
2300
- var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-reason-menu-item',r);
2381
+ reasonsWithCounts.forEach(function(x){
2382
+ var meta=REASON_LABELS[x.key]||REASON_LABELS['UNKNOWN'];
2383
+ var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-reason-menu-item',x.key);
2301
2384
  var label=document.createElement('span');label.textContent=meta.label;label.style.color=meta.color;
2302
- var count=document.createElement('span');count.className='tag-menu-count';count.textContent=reasonCount(r);
2385
+ var count=document.createElement('span');count.className='tag-menu-count';count.textContent=x.count;
2303
2386
  item.appendChild(label);item.appendChild(count);
2304
2387
  menu.appendChild(item);
2305
2388
  });
@@ -2323,7 +2406,10 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2323
2406
  activeStatus=null;
2324
2407
  activeTags.clear();
2325
2408
  activeReasons.clear();
2409
+ activeSearch='';
2326
2410
  document.querySelectorAll('.stat-pill').forEach(function(p){p.classList.remove('active')});
2411
+ var searchInput=document.querySelector('[data-filter-search]');
2412
+ if(searchInput)searchInput.value='';
2327
2413
  renderActiveChips();
2328
2414
  closeTagMenu();
2329
2415
  applyFilters();
@@ -2451,8 +2537,20 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
2451
2537
  p.getAll('tag').forEach(function(t){if(tagSet[t])activeTags.add(t)});
2452
2538
  var reasonSet={};allReasons.forEach(function(r){reasonSet[r]=true});
2453
2539
  p.getAll('reason').forEach(function(r){if(reasonSet[r])activeReasons.add(r)});
2540
+ var q=p.get('q');
2541
+ var searchInput=document.querySelector('[data-filter-search]');
2542
+ if(q){
2543
+ activeSearch=q.toLowerCase();
2544
+ if(searchInput)searchInput.value=q;
2545
+ }
2546
+ if(searchInput){
2547
+ searchInput.addEventListener('input',function(){
2548
+ activeSearch=searchInput.value.trim().toLowerCase();
2549
+ applyFilters();
2550
+ });
2551
+ }
2454
2552
  if(activeTags.size>0||activeReasons.size>0)renderActiveChips();
2455
- if(activeStatus!==null||activeTags.size>0||activeReasons.size>0)applyFilters();
2553
+ if(activeStatus!==null||activeTags.size>0||activeReasons.size>0||activeSearch.length>0)applyFilters();
2456
2554
  })();
2457
2555
 
2458
2556
  // 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.1",
3
+ "version": "5.45.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",