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.js CHANGED
@@ -693,50 +693,162 @@ function initTheme() {
693
693
  }
694
694
  `;
695
695
  var JS_CORE = `
696
+ // Filter state
697
+ var activeTags = new Set();
698
+ var activeStatus = null;
699
+
696
700
  // Search functionality
697
701
  function initSearch() {
698
- const input = document.querySelector('.search-input');
702
+ var input = document.querySelector('.search-input');
699
703
  if (!input) return;
700
704
 
701
- let debounceTimer;
702
- input.addEventListener('input', (e) => {
705
+ var debounceTimer;
706
+ input.addEventListener('input', function() {
703
707
  clearTimeout(debounceTimer);
704
- debounceTimer = setTimeout(() => {
705
- filterScenarios(e.target.value.toLowerCase().trim());
708
+ debounceTimer = setTimeout(function() {
709
+ applyAllFilters();
706
710
  }, 150);
707
711
  });
708
712
 
709
713
  // Clear search on Escape
710
- input.addEventListener('keydown', (e) => {
714
+ input.addEventListener('keydown', function(e) {
711
715
  if (e.key === 'Escape') {
712
716
  e.target.value = '';
713
- filterScenarios('');
717
+ applyAllFilters();
714
718
  }
715
719
  });
716
720
  }
717
721
 
718
- function filterScenarios(query) {
719
- const features = document.querySelectorAll('.feature');
722
+ // Tag filter
723
+ function initTagFilter() {
724
+ document.querySelectorAll('.tag-pill').forEach(function(pill) {
725
+ pill.addEventListener('click', function() {
726
+ var tag = pill.dataset.tag;
727
+ if (activeTags.has(tag)) {
728
+ activeTags.delete(tag);
729
+ pill.classList.remove('active');
730
+ } else {
731
+ activeTags.add(tag);
732
+ pill.classList.add('active');
733
+ }
734
+ updateClearButton();
735
+ applyAllFilters();
736
+ });
737
+ });
720
738
 
721
- features.forEach(feature => {
722
- const scenarios = feature.querySelectorAll('.scenario');
723
- let visibleCount = 0;
739
+ var clearBtn = document.querySelector('.tag-bar-clear');
740
+ if (clearBtn) {
741
+ clearBtn.addEventListener('click', function() {
742
+ activeTags.clear();
743
+ document.querySelectorAll('.tag-pill.active').forEach(function(p) { p.classList.remove('active'); });
744
+ updateClearButton();
745
+ applyAllFilters();
746
+ });
747
+ }
748
+ }
724
749
 
725
- scenarios.forEach(scenario => {
726
- const title = scenario.querySelector('.scenario-title')?.textContent?.toLowerCase() || '';
727
- const tags = Array.from(scenario.querySelectorAll('.tag')).map(t => t.textContent.toLowerCase());
728
- const steps = Array.from(scenario.querySelectorAll('.step-text')).map(s => s.textContent.toLowerCase());
750
+ function updateClearButton() {
751
+ var clearBtn = document.querySelector('.tag-bar-clear');
752
+ if (clearBtn) {
753
+ clearBtn.style.display = activeTags.size > 0 ? '' : 'none';
754
+ }
755
+ }
729
756
 
730
- const matches = !query ||
731
- title.includes(query) ||
732
- tags.some(t => t.includes(query)) ||
733
- steps.some(s => s.includes(query));
757
+ // Status filter (clickable summary cards)
758
+ function initStatusFilter() {
759
+ document.querySelectorAll('.summary-card').forEach(function(card) {
760
+ card.style.cursor = 'pointer';
761
+ if (!card.classList.contains('passed') && !card.classList.contains('failed') && !card.classList.contains('skipped')) {
762
+ card.addEventListener('click', function() {
763
+ activeStatus = null;
764
+ document.querySelectorAll('.summary-card').forEach(function(c) { c.classList.remove('status-active'); });
765
+ applyAllFilters();
766
+ });
767
+ return;
768
+ }
769
+ card.addEventListener('click', function() {
770
+ var status = card.classList.contains('passed') ? 'passed' :
771
+ card.classList.contains('failed') ? 'failed' : 'skipped';
772
+ if (activeStatus === status) {
773
+ activeStatus = null;
774
+ card.classList.remove('status-active');
775
+ } else {
776
+ activeStatus = status;
777
+ document.querySelectorAll('.summary-card').forEach(function(c) { c.classList.remove('status-active'); });
778
+ card.classList.add('status-active');
779
+ }
780
+ applyAllFilters();
781
+ });
782
+ });
783
+ }
734
784
 
735
- scenario.style.display = matches ? '' : 'none';
736
- if (matches) visibleCount++;
785
+ // Unified filter: composes search + tags + status
786
+ function applyAllFilters() {
787
+ var searchInput = document.querySelector('.search-input');
788
+ var searchQuery = searchInput ? searchInput.value.toLowerCase().trim() : '';
789
+ var features = document.querySelectorAll('.feature');
790
+ var visibleCount = 0;
791
+ var totalCount = 0;
792
+
793
+ features.forEach(function(feature) {
794
+ var scenarios = feature.querySelectorAll('.scenario');
795
+ var featureVisible = 0;
796
+
797
+ scenarios.forEach(function(scenario) {
798
+ totalCount++;
799
+ var title = (scenario.querySelector('.scenario-title') || {}).textContent || '';
800
+ title = title.toLowerCase();
801
+ var tags = Array.from(scenario.querySelectorAll('.scenario-meta .tag')).map(function(t) { return t.textContent.toLowerCase(); });
802
+ var steps = Array.from(scenario.querySelectorAll('.step-text')).map(function(s) { return s.textContent.toLowerCase(); });
803
+ var statusEl = scenario.querySelector('.status-icon');
804
+ var status = statusEl && statusEl.classList.contains('status-passed') ? 'passed' :
805
+ statusEl && statusEl.classList.contains('status-failed') ? 'failed' :
806
+ statusEl && statusEl.classList.contains('status-skipped') ? 'skipped' : 'pending';
807
+
808
+ var matchesSearch = !searchQuery ||
809
+ title.includes(searchQuery) ||
810
+ tags.some(function(t) { return t.includes(searchQuery); }) ||
811
+ steps.some(function(s) { return s.includes(searchQuery); });
812
+
813
+ var matchesTags = activeTags.size === 0 ||
814
+ tags.some(function(t) { return activeTags.has(t); });
815
+
816
+ var matchesStatus = !activeStatus ||
817
+ status === activeStatus ||
818
+ (activeStatus === 'skipped' && status === 'pending');
819
+
820
+ var visible = matchesSearch && matchesTags && matchesStatus;
821
+ scenario.style.display = visible ? '' : 'none';
822
+ if (visible) { visibleCount++; featureVisible++; }
737
823
  });
738
824
 
739
- feature.style.display = visibleCount > 0 ? '' : 'none';
825
+ feature.style.display = featureVisible > 0 ? '' : 'none';
826
+ });
827
+
828
+ updateFilterResults(visibleCount, totalCount);
829
+ }
830
+
831
+ function updateFilterResults(visible, total) {
832
+ var el = document.querySelector('.filter-results');
833
+ if (!el) return;
834
+ var searchInput = document.querySelector('.search-input');
835
+ var isFiltering = activeTags.size > 0 || activeStatus ||
836
+ (searchInput && searchInput.value.trim().length > 0);
837
+ el.style.display = isFiltering ? '' : 'none';
838
+ var vc = el.querySelector('.visible-count');
839
+ var tc = el.querySelector('.total-count');
840
+ if (vc) vc.textContent = visible;
841
+ if (tc) tc.textContent = total;
842
+ }
843
+
844
+ // Keyboard shortcuts
845
+ function initKeyboardShortcuts() {
846
+ document.addEventListener('keydown', function(e) {
847
+ if (e.key === '/' && !e.ctrlKey && !e.metaKey && e.target.tagName !== 'INPUT') {
848
+ e.preventDefault();
849
+ var input = document.querySelector('.search-input');
850
+ if (input) input.focus();
851
+ }
740
852
  });
741
853
  }
742
854
 
@@ -821,6 +933,9 @@ function generateScript(options) {
821
933
  initCalls.push("initTheme();");
822
934
  }
823
935
  initCalls.push("initSearch();");
936
+ initCalls.push("initTagFilter();");
937
+ initCalls.push("initStatusFilter();");
938
+ initCalls.push("initKeyboardShortcuts();");
824
939
  initCalls.push("initCollapse();");
825
940
  const initScript = `
826
941
  // Initialize on load
@@ -1304,6 +1419,98 @@ body {
1304
1419
  }
1305
1420
  .summary-card.pending .value { color: var(--pending); }
1306
1421
 
1422
+ /* ============================================================================
1423
+ Tag Filter Bar
1424
+ ============================================================================ */
1425
+ .tag-bar {
1426
+ margin-bottom: 1rem;
1427
+ padding: 0.75rem 1rem;
1428
+ background: var(--card);
1429
+ border: 1px solid var(--border);
1430
+ border-radius: var(--radius);
1431
+ position: sticky;
1432
+ top: 0;
1433
+ z-index: 10;
1434
+ }
1435
+
1436
+ .tag-bar-header {
1437
+ display: flex;
1438
+ justify-content: space-between;
1439
+ align-items: center;
1440
+ margin-bottom: 0.5rem;
1441
+ }
1442
+
1443
+ .tag-bar-label {
1444
+ font-size: 0.6875rem;
1445
+ text-transform: uppercase;
1446
+ letter-spacing: 0.05em;
1447
+ color: var(--muted-foreground);
1448
+ font-weight: 500;
1449
+ }
1450
+
1451
+ .tag-bar-clear {
1452
+ font-size: 0.75rem;
1453
+ font-weight: 500;
1454
+ color: var(--primary);
1455
+ background: none;
1456
+ border: none;
1457
+ cursor: pointer;
1458
+ padding: 0.125rem 0.5rem;
1459
+ border-radius: var(--radius);
1460
+ transition: all 0.15s ease;
1461
+ }
1462
+
1463
+ .tag-bar-clear:hover {
1464
+ background: var(--muted);
1465
+ }
1466
+
1467
+ .tag-bar-pills {
1468
+ display: flex;
1469
+ flex-wrap: wrap;
1470
+ gap: 0.375rem;
1471
+ }
1472
+
1473
+ .tag-pill {
1474
+ font-size: 0.75rem;
1475
+ font-weight: 500;
1476
+ padding: 0.25rem 0.625rem;
1477
+ background: var(--tag-bg);
1478
+ color: var(--tag-color);
1479
+ border: 1px solid var(--tag-border);
1480
+ border-radius: 9999px;
1481
+ font-family: var(--font-mono);
1482
+ cursor: pointer;
1483
+ transition: all 0.15s ease;
1484
+ }
1485
+
1486
+ .tag-pill:hover {
1487
+ background: var(--success-border);
1488
+ }
1489
+
1490
+ .tag-pill.active {
1491
+ background: var(--primary);
1492
+ color: var(--primary-foreground);
1493
+ border-color: var(--primary);
1494
+ }
1495
+
1496
+ /* ============================================================================
1497
+ Summary Card Status Filter
1498
+ ============================================================================ */
1499
+ .summary-card.status-active {
1500
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
1501
+ }
1502
+
1503
+ /* ============================================================================
1504
+ Filter Results Counter
1505
+ ============================================================================ */
1506
+ .filter-results {
1507
+ text-align: center;
1508
+ font-size: 0.8125rem;
1509
+ color: var(--muted-foreground);
1510
+ margin-bottom: 1rem;
1511
+ font-weight: 500;
1512
+ }
1513
+
1307
1514
  /* ============================================================================
1308
1515
  Feature Sections - shadcn accordion style
1309
1516
  ============================================================================ */
@@ -1691,8 +1898,10 @@ body {
1691
1898
  padding: 0;
1692
1899
  }
1693
1900
 
1694
- .header-actions {
1695
- display: none;
1901
+ .header-actions,
1902
+ .tag-bar,
1903
+ .filter-results {
1904
+ display: none !important;
1696
1905
  }
1697
1906
 
1698
1907
  .feature,
@@ -2266,6 +2475,28 @@ function renderSummary(args, _deps) {
2266
2475
  </div>`;
2267
2476
  }
2268
2477
 
2478
+ // src/formatters/html/renderers/tag-bar.ts
2479
+ function renderTagBar(args, deps) {
2480
+ const { tags, totalScenarios } = args;
2481
+ if (tags.length === 0) return "";
2482
+ const pills = tags.map(
2483
+ (tag) => `<button type="button" class="tag-pill" data-tag="${deps.escapeHtml(tag)}">${deps.escapeHtml(tag)}</button>`
2484
+ ).join("\n ");
2485
+ return `
2486
+ <div class="tag-bar">
2487
+ <div class="tag-bar-header">
2488
+ <span class="tag-bar-label">Filter by tag</span>
2489
+ <button type="button" class="tag-bar-clear" style="display:none">Clear</button>
2490
+ </div>
2491
+ <div class="tag-bar-pills">
2492
+ ${pills}
2493
+ </div>
2494
+ </div>
2495
+ <div class="filter-results" style="display:none">
2496
+ Showing <span class="visible-count">0</span> of <span class="total-count">${totalScenarios}</span> scenarios
2497
+ </div>`;
2498
+ }
2499
+
2269
2500
  // src/formatters/html/renderers/error-box.ts
2270
2501
  function renderErrorBox(args, deps) {
2271
2502
  const body = args.stack != null ? `${deps.escapeHtml(args.message)}
@@ -2565,6 +2796,15 @@ function buildBody(args, deps) {
2565
2796
  deps.summaryDeps
2566
2797
  )
2567
2798
  );
2799
+ const allTags = [
2800
+ ...new Set(run.testCases.flatMap((tc) => tc.tags))
2801
+ ].sort();
2802
+ parts.push(
2803
+ deps.renderTagBar(
2804
+ { tags: allTags, totalScenarios: total },
2805
+ deps.tagBarDeps
2806
+ )
2807
+ );
2568
2808
  const byFile = groupBy(run.testCases, (tc) => tc.sourceFile);
2569
2809
  for (const [file, testCases] of byFile) {
2570
2810
  parts.push(
@@ -2621,12 +2861,15 @@ function createHtmlFormatter(options = {}) {
2621
2861
  renderScenario: (args) => renderScenario(args, scenarioDeps),
2622
2862
  scenarioDeps
2623
2863
  };
2864
+ const tagBarDeps = { escapeHtml };
2624
2865
  const bodyDeps = {
2625
2866
  renderMetaInfo,
2626
2867
  renderSummary,
2868
+ renderTagBar,
2627
2869
  renderFeature,
2628
2870
  metaDeps: { escapeHtml },
2629
2871
  summaryDeps: {},
2872
+ tagBarDeps,
2630
2873
  featureDeps
2631
2874
  };
2632
2875
  return {