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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Jag Reehal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/cli.js CHANGED
@@ -896,50 +896,162 @@ function initTheme() {
896
896
  }
897
897
  `;
898
898
  var JS_CORE = `
899
+ // Filter state
900
+ var activeTags = new Set();
901
+ var activeStatus = null;
902
+
899
903
  // Search functionality
900
904
  function initSearch() {
901
- const input = document.querySelector('.search-input');
905
+ var input = document.querySelector('.search-input');
902
906
  if (!input) return;
903
907
 
904
- let debounceTimer;
905
- input.addEventListener('input', (e) => {
908
+ var debounceTimer;
909
+ input.addEventListener('input', function() {
906
910
  clearTimeout(debounceTimer);
907
- debounceTimer = setTimeout(() => {
908
- filterScenarios(e.target.value.toLowerCase().trim());
911
+ debounceTimer = setTimeout(function() {
912
+ applyAllFilters();
909
913
  }, 150);
910
914
  });
911
915
 
912
916
  // Clear search on Escape
913
- input.addEventListener('keydown', (e) => {
917
+ input.addEventListener('keydown', function(e) {
914
918
  if (e.key === 'Escape') {
915
919
  e.target.value = '';
916
- filterScenarios('');
920
+ applyAllFilters();
917
921
  }
918
922
  });
919
923
  }
920
924
 
921
- function filterScenarios(query) {
922
- const features = document.querySelectorAll('.feature');
925
+ // Tag filter
926
+ function initTagFilter() {
927
+ document.querySelectorAll('.tag-pill').forEach(function(pill) {
928
+ pill.addEventListener('click', function() {
929
+ var tag = pill.dataset.tag;
930
+ if (activeTags.has(tag)) {
931
+ activeTags.delete(tag);
932
+ pill.classList.remove('active');
933
+ } else {
934
+ activeTags.add(tag);
935
+ pill.classList.add('active');
936
+ }
937
+ updateClearButton();
938
+ applyAllFilters();
939
+ });
940
+ });
923
941
 
924
- features.forEach(feature => {
925
- const scenarios = feature.querySelectorAll('.scenario');
926
- let visibleCount = 0;
942
+ var clearBtn = document.querySelector('.tag-bar-clear');
943
+ if (clearBtn) {
944
+ clearBtn.addEventListener('click', function() {
945
+ activeTags.clear();
946
+ document.querySelectorAll('.tag-pill.active').forEach(function(p) { p.classList.remove('active'); });
947
+ updateClearButton();
948
+ applyAllFilters();
949
+ });
950
+ }
951
+ }
927
952
 
928
- scenarios.forEach(scenario => {
929
- const title = scenario.querySelector('.scenario-title')?.textContent?.toLowerCase() || '';
930
- const tags = Array.from(scenario.querySelectorAll('.tag')).map(t => t.textContent.toLowerCase());
931
- const steps = Array.from(scenario.querySelectorAll('.step-text')).map(s => s.textContent.toLowerCase());
953
+ function updateClearButton() {
954
+ var clearBtn = document.querySelector('.tag-bar-clear');
955
+ if (clearBtn) {
956
+ clearBtn.style.display = activeTags.size > 0 ? '' : 'none';
957
+ }
958
+ }
932
959
 
933
- const matches = !query ||
934
- title.includes(query) ||
935
- tags.some(t => t.includes(query)) ||
936
- steps.some(s => s.includes(query));
960
+ // Status filter (clickable summary cards)
961
+ function initStatusFilter() {
962
+ document.querySelectorAll('.summary-card').forEach(function(card) {
963
+ card.style.cursor = 'pointer';
964
+ if (!card.classList.contains('passed') && !card.classList.contains('failed') && !card.classList.contains('skipped')) {
965
+ card.addEventListener('click', function() {
966
+ activeStatus = null;
967
+ document.querySelectorAll('.summary-card').forEach(function(c) { c.classList.remove('status-active'); });
968
+ applyAllFilters();
969
+ });
970
+ return;
971
+ }
972
+ card.addEventListener('click', function() {
973
+ var status = card.classList.contains('passed') ? 'passed' :
974
+ card.classList.contains('failed') ? 'failed' : 'skipped';
975
+ if (activeStatus === status) {
976
+ activeStatus = null;
977
+ card.classList.remove('status-active');
978
+ } else {
979
+ activeStatus = status;
980
+ document.querySelectorAll('.summary-card').forEach(function(c) { c.classList.remove('status-active'); });
981
+ card.classList.add('status-active');
982
+ }
983
+ applyAllFilters();
984
+ });
985
+ });
986
+ }
937
987
 
938
- scenario.style.display = matches ? '' : 'none';
939
- if (matches) visibleCount++;
988
+ // Unified filter: composes search + tags + status
989
+ function applyAllFilters() {
990
+ var searchInput = document.querySelector('.search-input');
991
+ var searchQuery = searchInput ? searchInput.value.toLowerCase().trim() : '';
992
+ var features = document.querySelectorAll('.feature');
993
+ var visibleCount = 0;
994
+ var totalCount = 0;
995
+
996
+ features.forEach(function(feature) {
997
+ var scenarios = feature.querySelectorAll('.scenario');
998
+ var featureVisible = 0;
999
+
1000
+ scenarios.forEach(function(scenario) {
1001
+ totalCount++;
1002
+ var title = (scenario.querySelector('.scenario-title') || {}).textContent || '';
1003
+ title = title.toLowerCase();
1004
+ var tags = Array.from(scenario.querySelectorAll('.scenario-meta .tag')).map(function(t) { return t.textContent.toLowerCase(); });
1005
+ var steps = Array.from(scenario.querySelectorAll('.step-text')).map(function(s) { return s.textContent.toLowerCase(); });
1006
+ var statusEl = scenario.querySelector('.status-icon');
1007
+ var status = statusEl && statusEl.classList.contains('status-passed') ? 'passed' :
1008
+ statusEl && statusEl.classList.contains('status-failed') ? 'failed' :
1009
+ statusEl && statusEl.classList.contains('status-skipped') ? 'skipped' : 'pending';
1010
+
1011
+ var matchesSearch = !searchQuery ||
1012
+ title.includes(searchQuery) ||
1013
+ tags.some(function(t) { return t.includes(searchQuery); }) ||
1014
+ steps.some(function(s) { return s.includes(searchQuery); });
1015
+
1016
+ var matchesTags = activeTags.size === 0 ||
1017
+ tags.some(function(t) { return activeTags.has(t); });
1018
+
1019
+ var matchesStatus = !activeStatus ||
1020
+ status === activeStatus ||
1021
+ (activeStatus === 'skipped' && status === 'pending');
1022
+
1023
+ var visible = matchesSearch && matchesTags && matchesStatus;
1024
+ scenario.style.display = visible ? '' : 'none';
1025
+ if (visible) { visibleCount++; featureVisible++; }
940
1026
  });
941
1027
 
942
- feature.style.display = visibleCount > 0 ? '' : 'none';
1028
+ feature.style.display = featureVisible > 0 ? '' : 'none';
1029
+ });
1030
+
1031
+ updateFilterResults(visibleCount, totalCount);
1032
+ }
1033
+
1034
+ function updateFilterResults(visible, total) {
1035
+ var el = document.querySelector('.filter-results');
1036
+ if (!el) return;
1037
+ var searchInput = document.querySelector('.search-input');
1038
+ var isFiltering = activeTags.size > 0 || activeStatus ||
1039
+ (searchInput && searchInput.value.trim().length > 0);
1040
+ el.style.display = isFiltering ? '' : 'none';
1041
+ var vc = el.querySelector('.visible-count');
1042
+ var tc = el.querySelector('.total-count');
1043
+ if (vc) vc.textContent = visible;
1044
+ if (tc) tc.textContent = total;
1045
+ }
1046
+
1047
+ // Keyboard shortcuts
1048
+ function initKeyboardShortcuts() {
1049
+ document.addEventListener('keydown', function(e) {
1050
+ if (e.key === '/' && !e.ctrlKey && !e.metaKey && e.target.tagName !== 'INPUT') {
1051
+ e.preventDefault();
1052
+ var input = document.querySelector('.search-input');
1053
+ if (input) input.focus();
1054
+ }
943
1055
  });
944
1056
  }
945
1057
 
@@ -1024,6 +1136,9 @@ function generateScript(options) {
1024
1136
  initCalls.push("initTheme();");
1025
1137
  }
1026
1138
  initCalls.push("initSearch();");
1139
+ initCalls.push("initTagFilter();");
1140
+ initCalls.push("initStatusFilter();");
1141
+ initCalls.push("initKeyboardShortcuts();");
1027
1142
  initCalls.push("initCollapse();");
1028
1143
  const initScript = `
1029
1144
  // Initialize on load
@@ -1507,6 +1622,98 @@ body {
1507
1622
  }
1508
1623
  .summary-card.pending .value { color: var(--pending); }
1509
1624
 
1625
+ /* ============================================================================
1626
+ Tag Filter Bar
1627
+ ============================================================================ */
1628
+ .tag-bar {
1629
+ margin-bottom: 1rem;
1630
+ padding: 0.75rem 1rem;
1631
+ background: var(--card);
1632
+ border: 1px solid var(--border);
1633
+ border-radius: var(--radius);
1634
+ position: sticky;
1635
+ top: 0;
1636
+ z-index: 10;
1637
+ }
1638
+
1639
+ .tag-bar-header {
1640
+ display: flex;
1641
+ justify-content: space-between;
1642
+ align-items: center;
1643
+ margin-bottom: 0.5rem;
1644
+ }
1645
+
1646
+ .tag-bar-label {
1647
+ font-size: 0.6875rem;
1648
+ text-transform: uppercase;
1649
+ letter-spacing: 0.05em;
1650
+ color: var(--muted-foreground);
1651
+ font-weight: 500;
1652
+ }
1653
+
1654
+ .tag-bar-clear {
1655
+ font-size: 0.75rem;
1656
+ font-weight: 500;
1657
+ color: var(--primary);
1658
+ background: none;
1659
+ border: none;
1660
+ cursor: pointer;
1661
+ padding: 0.125rem 0.5rem;
1662
+ border-radius: var(--radius);
1663
+ transition: all 0.15s ease;
1664
+ }
1665
+
1666
+ .tag-bar-clear:hover {
1667
+ background: var(--muted);
1668
+ }
1669
+
1670
+ .tag-bar-pills {
1671
+ display: flex;
1672
+ flex-wrap: wrap;
1673
+ gap: 0.375rem;
1674
+ }
1675
+
1676
+ .tag-pill {
1677
+ font-size: 0.75rem;
1678
+ font-weight: 500;
1679
+ padding: 0.25rem 0.625rem;
1680
+ background: var(--tag-bg);
1681
+ color: var(--tag-color);
1682
+ border: 1px solid var(--tag-border);
1683
+ border-radius: 9999px;
1684
+ font-family: var(--font-mono);
1685
+ cursor: pointer;
1686
+ transition: all 0.15s ease;
1687
+ }
1688
+
1689
+ .tag-pill:hover {
1690
+ background: var(--success-border);
1691
+ }
1692
+
1693
+ .tag-pill.active {
1694
+ background: var(--primary);
1695
+ color: var(--primary-foreground);
1696
+ border-color: var(--primary);
1697
+ }
1698
+
1699
+ /* ============================================================================
1700
+ Summary Card Status Filter
1701
+ ============================================================================ */
1702
+ .summary-card.status-active {
1703
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
1704
+ }
1705
+
1706
+ /* ============================================================================
1707
+ Filter Results Counter
1708
+ ============================================================================ */
1709
+ .filter-results {
1710
+ text-align: center;
1711
+ font-size: 0.8125rem;
1712
+ color: var(--muted-foreground);
1713
+ margin-bottom: 1rem;
1714
+ font-weight: 500;
1715
+ }
1716
+
1510
1717
  /* ============================================================================
1511
1718
  Feature Sections - shadcn accordion style
1512
1719
  ============================================================================ */
@@ -1894,8 +2101,10 @@ body {
1894
2101
  padding: 0;
1895
2102
  }
1896
2103
 
1897
- .header-actions {
1898
- display: none;
2104
+ .header-actions,
2105
+ .tag-bar,
2106
+ .filter-results {
2107
+ display: none !important;
1899
2108
  }
1900
2109
 
1901
2110
  .feature,
@@ -2469,6 +2678,28 @@ function renderSummary(args, _deps) {
2469
2678
  </div>`;
2470
2679
  }
2471
2680
 
2681
+ // src/formatters/html/renderers/tag-bar.ts
2682
+ function renderTagBar(args, deps) {
2683
+ const { tags, totalScenarios } = args;
2684
+ if (tags.length === 0) return "";
2685
+ const pills = tags.map(
2686
+ (tag) => `<button type="button" class="tag-pill" data-tag="${deps.escapeHtml(tag)}">${deps.escapeHtml(tag)}</button>`
2687
+ ).join("\n ");
2688
+ return `
2689
+ <div class="tag-bar">
2690
+ <div class="tag-bar-header">
2691
+ <span class="tag-bar-label">Filter by tag</span>
2692
+ <button type="button" class="tag-bar-clear" style="display:none">Clear</button>
2693
+ </div>
2694
+ <div class="tag-bar-pills">
2695
+ ${pills}
2696
+ </div>
2697
+ </div>
2698
+ <div class="filter-results" style="display:none">
2699
+ Showing <span class="visible-count">0</span> of <span class="total-count">${totalScenarios}</span> scenarios
2700
+ </div>`;
2701
+ }
2702
+
2472
2703
  // src/formatters/html/renderers/error-box.ts
2473
2704
  function renderErrorBox(args, deps) {
2474
2705
  const body = args.stack != null ? `${deps.escapeHtml(args.message)}
@@ -2768,6 +2999,15 @@ function buildBody(args, deps) {
2768
2999
  deps.summaryDeps
2769
3000
  )
2770
3001
  );
3002
+ const allTags = [
3003
+ ...new Set(run.testCases.flatMap((tc) => tc.tags))
3004
+ ].sort();
3005
+ parts.push(
3006
+ deps.renderTagBar(
3007
+ { tags: allTags, totalScenarios: total },
3008
+ deps.tagBarDeps
3009
+ )
3010
+ );
2771
3011
  const byFile = groupBy(run.testCases, (tc) => tc.sourceFile);
2772
3012
  for (const [file, testCases] of byFile) {
2773
3013
  parts.push(
@@ -2824,12 +3064,15 @@ function createHtmlFormatter(options = {}) {
2824
3064
  renderScenario: (args) => renderScenario(args, scenarioDeps),
2825
3065
  scenarioDeps
2826
3066
  };
3067
+ const tagBarDeps = { escapeHtml };
2827
3068
  const bodyDeps = {
2828
3069
  renderMetaInfo,
2829
3070
  renderSummary,
3071
+ renderTagBar,
2830
3072
  renderFeature,
2831
3073
  metaDeps: { escapeHtml },
2832
3074
  summaryDeps: {},
3075
+ tagBarDeps,
2833
3076
  featureDeps
2834
3077
  };
2835
3078
  return {