executable-stories-formatters 0.1.0 → 0.2.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.
package/dist/index.cjs CHANGED
@@ -763,50 +763,162 @@ function initTheme() {
763
763
  }
764
764
  `;
765
765
  var JS_CORE = `
766
+ // Filter state
767
+ var activeTags = new Set();
768
+ var activeStatus = null;
769
+
766
770
  // Search functionality
767
771
  function initSearch() {
768
- const input = document.querySelector('.search-input');
772
+ var input = document.querySelector('.search-input');
769
773
  if (!input) return;
770
774
 
771
- let debounceTimer;
772
- input.addEventListener('input', (e) => {
775
+ var debounceTimer;
776
+ input.addEventListener('input', function() {
773
777
  clearTimeout(debounceTimer);
774
- debounceTimer = setTimeout(() => {
775
- filterScenarios(e.target.value.toLowerCase().trim());
778
+ debounceTimer = setTimeout(function() {
779
+ applyAllFilters();
776
780
  }, 150);
777
781
  });
778
782
 
779
783
  // Clear search on Escape
780
- input.addEventListener('keydown', (e) => {
784
+ input.addEventListener('keydown', function(e) {
781
785
  if (e.key === 'Escape') {
782
786
  e.target.value = '';
783
- filterScenarios('');
787
+ applyAllFilters();
784
788
  }
785
789
  });
786
790
  }
787
791
 
788
- function filterScenarios(query) {
789
- const features = document.querySelectorAll('.feature');
792
+ // Tag filter
793
+ function initTagFilter() {
794
+ document.querySelectorAll('.tag-pill').forEach(function(pill) {
795
+ pill.addEventListener('click', function() {
796
+ var tag = pill.dataset.tag;
797
+ if (activeTags.has(tag)) {
798
+ activeTags.delete(tag);
799
+ pill.classList.remove('active');
800
+ } else {
801
+ activeTags.add(tag);
802
+ pill.classList.add('active');
803
+ }
804
+ updateClearButton();
805
+ applyAllFilters();
806
+ });
807
+ });
790
808
 
791
- features.forEach(feature => {
792
- const scenarios = feature.querySelectorAll('.scenario');
793
- let visibleCount = 0;
809
+ var clearBtn = document.querySelector('.tag-bar-clear');
810
+ if (clearBtn) {
811
+ clearBtn.addEventListener('click', function() {
812
+ activeTags.clear();
813
+ document.querySelectorAll('.tag-pill.active').forEach(function(p) { p.classList.remove('active'); });
814
+ updateClearButton();
815
+ applyAllFilters();
816
+ });
817
+ }
818
+ }
794
819
 
795
- scenarios.forEach(scenario => {
796
- const title = scenario.querySelector('.scenario-title')?.textContent?.toLowerCase() || '';
797
- const tags = Array.from(scenario.querySelectorAll('.tag')).map(t => t.textContent.toLowerCase());
798
- const steps = Array.from(scenario.querySelectorAll('.step-text')).map(s => s.textContent.toLowerCase());
820
+ function updateClearButton() {
821
+ var clearBtn = document.querySelector('.tag-bar-clear');
822
+ if (clearBtn) {
823
+ clearBtn.style.display = activeTags.size > 0 ? '' : 'none';
824
+ }
825
+ }
799
826
 
800
- const matches = !query ||
801
- title.includes(query) ||
802
- tags.some(t => t.includes(query)) ||
803
- steps.some(s => s.includes(query));
827
+ // Status filter (clickable summary cards)
828
+ function initStatusFilter() {
829
+ document.querySelectorAll('.summary-card').forEach(function(card) {
830
+ card.style.cursor = 'pointer';
831
+ if (!card.classList.contains('passed') && !card.classList.contains('failed') && !card.classList.contains('skipped')) {
832
+ card.addEventListener('click', function() {
833
+ activeStatus = null;
834
+ document.querySelectorAll('.summary-card').forEach(function(c) { c.classList.remove('status-active'); });
835
+ applyAllFilters();
836
+ });
837
+ return;
838
+ }
839
+ card.addEventListener('click', function() {
840
+ var status = card.classList.contains('passed') ? 'passed' :
841
+ card.classList.contains('failed') ? 'failed' : 'skipped';
842
+ if (activeStatus === status) {
843
+ activeStatus = null;
844
+ card.classList.remove('status-active');
845
+ } else {
846
+ activeStatus = status;
847
+ document.querySelectorAll('.summary-card').forEach(function(c) { c.classList.remove('status-active'); });
848
+ card.classList.add('status-active');
849
+ }
850
+ applyAllFilters();
851
+ });
852
+ });
853
+ }
804
854
 
805
- scenario.style.display = matches ? '' : 'none';
806
- if (matches) visibleCount++;
855
+ // Unified filter: composes search + tags + status
856
+ function applyAllFilters() {
857
+ var searchInput = document.querySelector('.search-input');
858
+ var searchQuery = searchInput ? searchInput.value.toLowerCase().trim() : '';
859
+ var features = document.querySelectorAll('.feature');
860
+ var visibleCount = 0;
861
+ var totalCount = 0;
862
+
863
+ features.forEach(function(feature) {
864
+ var scenarios = feature.querySelectorAll('.scenario');
865
+ var featureVisible = 0;
866
+
867
+ scenarios.forEach(function(scenario) {
868
+ totalCount++;
869
+ var title = (scenario.querySelector('.scenario-title') || {}).textContent || '';
870
+ title = title.toLowerCase();
871
+ var tags = Array.from(scenario.querySelectorAll('.scenario-meta .tag')).map(function(t) { return t.textContent.toLowerCase(); });
872
+ var steps = Array.from(scenario.querySelectorAll('.step-text')).map(function(s) { return s.textContent.toLowerCase(); });
873
+ var statusEl = scenario.querySelector('.status-icon');
874
+ var status = statusEl && statusEl.classList.contains('status-passed') ? 'passed' :
875
+ statusEl && statusEl.classList.contains('status-failed') ? 'failed' :
876
+ statusEl && statusEl.classList.contains('status-skipped') ? 'skipped' : 'pending';
877
+
878
+ var matchesSearch = !searchQuery ||
879
+ title.includes(searchQuery) ||
880
+ tags.some(function(t) { return t.includes(searchQuery); }) ||
881
+ steps.some(function(s) { return s.includes(searchQuery); });
882
+
883
+ var matchesTags = activeTags.size === 0 ||
884
+ tags.some(function(t) { return activeTags.has(t); });
885
+
886
+ var matchesStatus = !activeStatus ||
887
+ status === activeStatus ||
888
+ (activeStatus === 'skipped' && status === 'pending');
889
+
890
+ var visible = matchesSearch && matchesTags && matchesStatus;
891
+ scenario.style.display = visible ? '' : 'none';
892
+ if (visible) { visibleCount++; featureVisible++; }
807
893
  });
808
894
 
809
- feature.style.display = visibleCount > 0 ? '' : 'none';
895
+ feature.style.display = featureVisible > 0 ? '' : 'none';
896
+ });
897
+
898
+ updateFilterResults(visibleCount, totalCount);
899
+ }
900
+
901
+ function updateFilterResults(visible, total) {
902
+ var el = document.querySelector('.filter-results');
903
+ if (!el) return;
904
+ var searchInput = document.querySelector('.search-input');
905
+ var isFiltering = activeTags.size > 0 || activeStatus ||
906
+ (searchInput && searchInput.value.trim().length > 0);
907
+ el.style.display = isFiltering ? '' : 'none';
908
+ var vc = el.querySelector('.visible-count');
909
+ var tc = el.querySelector('.total-count');
910
+ if (vc) vc.textContent = visible;
911
+ if (tc) tc.textContent = total;
912
+ }
913
+
914
+ // Keyboard shortcuts
915
+ function initKeyboardShortcuts() {
916
+ document.addEventListener('keydown', function(e) {
917
+ if (e.key === '/' && !e.ctrlKey && !e.metaKey && e.target.tagName !== 'INPUT') {
918
+ e.preventDefault();
919
+ var input = document.querySelector('.search-input');
920
+ if (input) input.focus();
921
+ }
810
922
  });
811
923
  }
812
924
 
@@ -891,6 +1003,9 @@ function generateScript(options) {
891
1003
  initCalls.push("initTheme();");
892
1004
  }
893
1005
  initCalls.push("initSearch();");
1006
+ initCalls.push("initTagFilter();");
1007
+ initCalls.push("initStatusFilter();");
1008
+ initCalls.push("initKeyboardShortcuts();");
894
1009
  initCalls.push("initCollapse();");
895
1010
  const initScript = `
896
1011
  // Initialize on load
@@ -1374,6 +1489,98 @@ body {
1374
1489
  }
1375
1490
  .summary-card.pending .value { color: var(--pending); }
1376
1491
 
1492
+ /* ============================================================================
1493
+ Tag Filter Bar
1494
+ ============================================================================ */
1495
+ .tag-bar {
1496
+ margin-bottom: 1rem;
1497
+ padding: 0.75rem 1rem;
1498
+ background: var(--card);
1499
+ border: 1px solid var(--border);
1500
+ border-radius: var(--radius);
1501
+ position: sticky;
1502
+ top: 0;
1503
+ z-index: 10;
1504
+ }
1505
+
1506
+ .tag-bar-header {
1507
+ display: flex;
1508
+ justify-content: space-between;
1509
+ align-items: center;
1510
+ margin-bottom: 0.5rem;
1511
+ }
1512
+
1513
+ .tag-bar-label {
1514
+ font-size: 0.6875rem;
1515
+ text-transform: uppercase;
1516
+ letter-spacing: 0.05em;
1517
+ color: var(--muted-foreground);
1518
+ font-weight: 500;
1519
+ }
1520
+
1521
+ .tag-bar-clear {
1522
+ font-size: 0.75rem;
1523
+ font-weight: 500;
1524
+ color: var(--primary);
1525
+ background: none;
1526
+ border: none;
1527
+ cursor: pointer;
1528
+ padding: 0.125rem 0.5rem;
1529
+ border-radius: var(--radius);
1530
+ transition: all 0.15s ease;
1531
+ }
1532
+
1533
+ .tag-bar-clear:hover {
1534
+ background: var(--muted);
1535
+ }
1536
+
1537
+ .tag-bar-pills {
1538
+ display: flex;
1539
+ flex-wrap: wrap;
1540
+ gap: 0.375rem;
1541
+ }
1542
+
1543
+ .tag-pill {
1544
+ font-size: 0.75rem;
1545
+ font-weight: 500;
1546
+ padding: 0.25rem 0.625rem;
1547
+ background: var(--tag-bg);
1548
+ color: var(--tag-color);
1549
+ border: 1px solid var(--tag-border);
1550
+ border-radius: 9999px;
1551
+ font-family: var(--font-mono);
1552
+ cursor: pointer;
1553
+ transition: all 0.15s ease;
1554
+ }
1555
+
1556
+ .tag-pill:hover {
1557
+ background: var(--success-border);
1558
+ }
1559
+
1560
+ .tag-pill.active {
1561
+ background: var(--primary);
1562
+ color: var(--primary-foreground);
1563
+ border-color: var(--primary);
1564
+ }
1565
+
1566
+ /* ============================================================================
1567
+ Summary Card Status Filter
1568
+ ============================================================================ */
1569
+ .summary-card.status-active {
1570
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
1571
+ }
1572
+
1573
+ /* ============================================================================
1574
+ Filter Results Counter
1575
+ ============================================================================ */
1576
+ .filter-results {
1577
+ text-align: center;
1578
+ font-size: 0.8125rem;
1579
+ color: var(--muted-foreground);
1580
+ margin-bottom: 1rem;
1581
+ font-weight: 500;
1582
+ }
1583
+
1377
1584
  /* ============================================================================
1378
1585
  Feature Sections - shadcn accordion style
1379
1586
  ============================================================================ */
@@ -1761,8 +1968,10 @@ body {
1761
1968
  padding: 0;
1762
1969
  }
1763
1970
 
1764
- .header-actions {
1765
- display: none;
1971
+ .header-actions,
1972
+ .tag-bar,
1973
+ .filter-results {
1974
+ display: none !important;
1766
1975
  }
1767
1976
 
1768
1977
  .feature,
@@ -2336,6 +2545,28 @@ function renderSummary(args, _deps) {
2336
2545
  </div>`;
2337
2546
  }
2338
2547
 
2548
+ // src/formatters/html/renderers/tag-bar.ts
2549
+ function renderTagBar(args, deps) {
2550
+ const { tags, totalScenarios } = args;
2551
+ if (tags.length === 0) return "";
2552
+ const pills = tags.map(
2553
+ (tag) => `<button type="button" class="tag-pill" data-tag="${deps.escapeHtml(tag)}">${deps.escapeHtml(tag)}</button>`
2554
+ ).join("\n ");
2555
+ return `
2556
+ <div class="tag-bar">
2557
+ <div class="tag-bar-header">
2558
+ <span class="tag-bar-label">Filter by tag</span>
2559
+ <button type="button" class="tag-bar-clear" style="display:none">Clear</button>
2560
+ </div>
2561
+ <div class="tag-bar-pills">
2562
+ ${pills}
2563
+ </div>
2564
+ </div>
2565
+ <div class="filter-results" style="display:none">
2566
+ Showing <span class="visible-count">0</span> of <span class="total-count">${totalScenarios}</span> scenarios
2567
+ </div>`;
2568
+ }
2569
+
2339
2570
  // src/formatters/html/renderers/error-box.ts
2340
2571
  function renderErrorBox(args, deps) {
2341
2572
  const body = args.stack != null ? `${deps.escapeHtml(args.message)}
@@ -2635,6 +2866,15 @@ function buildBody(args, deps) {
2635
2866
  deps.summaryDeps
2636
2867
  )
2637
2868
  );
2869
+ const allTags = [
2870
+ ...new Set(run.testCases.flatMap((tc) => tc.tags))
2871
+ ].sort();
2872
+ parts.push(
2873
+ deps.renderTagBar(
2874
+ { tags: allTags, totalScenarios: total },
2875
+ deps.tagBarDeps
2876
+ )
2877
+ );
2638
2878
  const byFile = groupBy(run.testCases, (tc) => tc.sourceFile);
2639
2879
  for (const [file, testCases] of byFile) {
2640
2880
  parts.push(
@@ -2691,12 +2931,15 @@ function createHtmlFormatter(options = {}) {
2691
2931
  renderScenario: (args) => renderScenario(args, scenarioDeps),
2692
2932
  scenarioDeps
2693
2933
  };
2934
+ const tagBarDeps = { escapeHtml };
2694
2935
  const bodyDeps = {
2695
2936
  renderMetaInfo,
2696
2937
  renderSummary,
2938
+ renderTagBar,
2697
2939
  renderFeature,
2698
2940
  metaDeps: { escapeHtml },
2699
2941
  summaryDeps: {},
2942
+ tagBarDeps,
2700
2943
  featureDeps
2701
2944
  };
2702
2945
  return {