describe-me 0.6.0 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "describe-me",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Living component documentation from your Vitest tests: the viewer and CLI of describe-me",
5
5
  "keywords": [
6
6
  "vitest",
@@ -45,7 +45,7 @@
45
45
  "sideEffects": false,
46
46
  "dependencies": {
47
47
  "rrweb-snapshot": "^2.1.6",
48
- "@describe-me/core": "^0.6.0"
48
+ "@describe-me/core": "^0.7.0"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
@@ -0,0 +1,6 @@
1
+ import { state } from './state.js'
2
+
3
+ /** A search or a tag narrows the sidebar. Collapsed suites then open, so every match shows. */
4
+ export function filterActive(): boolean {
5
+ return state.query.trim() !== '' || state.tag !== null
6
+ }
package/src/inspector.ts CHANGED
@@ -2,6 +2,7 @@ import { el } from './el.js'
2
2
  import { liveSection } from './live-section.js'
3
3
  import { liveShown } from './live-shown.js'
4
4
  import { currentTest, state } from './state.js'
5
+ import { tagButtons } from './tag-buttons.js'
5
6
  import { valueCell } from './value-cell.js'
6
7
 
7
8
  function keyHint(): HTMLElement {
@@ -26,8 +27,8 @@ function keyHint(): HTMLElement {
26
27
  }
27
28
 
28
29
  /**
29
- * The inspector's sections for one test: status, component props, the
30
- * current frame or the live section, errors.
30
+ * The inspector's sections for one test: status and tags, component props,
31
+ * the current frame or the live section, errors.
31
32
  */
32
33
  export function renderInspector(): HTMLElement[] {
33
34
  const test = currentTest()
@@ -56,6 +57,11 @@ export function renderInspector(): HTMLElement[] {
56
57
  ),
57
58
  )
58
59
 
60
+ const tags = tagButtons(test)
61
+ if (tags) {
62
+ status.append(tags)
63
+ }
64
+
59
65
  sections.push(status)
60
66
 
61
67
  if (test.component) {
package/src/keyboard.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ManifestTest } from '@describe-me/core/types'
2
+ import { filterActive } from './filter-active.js'
2
3
  import { liveShown } from './live-shown.js'
3
4
  import { regions } from './regions.js'
4
5
  import { rerender } from './rerender.js'
@@ -36,7 +37,8 @@ function nearestShown(shown: ManifestTest[], id: string, step: 1 | -1): Manifest
36
37
  /**
37
38
  * The next or previous test the sidebar shows. From a test hidden in a
38
39
  * collapsed suite, that is the nearest shown row in sidebar order. From a test
39
- * the search does not match, ↓ enters at the first match and ↑ at the last.
40
+ * the search or the tag filter leaves out, ↓ enters at the first match and ↑
41
+ * at the last.
40
42
  */
41
43
  function neighbour(id: string, step: 1 | -1): ManifestTest | undefined {
42
44
  const tests = visibleTests()
@@ -45,7 +47,7 @@ function neighbour(id: string, step: 1 | -1): ManifestTest | undefined {
45
47
  return tests[index + step]
46
48
  }
47
49
 
48
- if (state.query.trim() === '') {
50
+ if (!filterActive()) {
49
51
  return nearestShown(tests, id, step)
50
52
  }
51
53
 
@@ -0,0 +1,12 @@
1
+ import type { ManifestTest } from '@describe-me/core/types'
2
+ import { matchesQuery } from './matches-query.js'
3
+ import { state } from './state.js'
4
+
5
+ /** Whether the sidebar shows the test: it matches the search and carries the tag filtered by. */
6
+ export function matchesFilter(test: ManifestTest, moduleId: string): boolean {
7
+ if (state.tag !== null && !(test.tags ?? []).includes(state.tag)) {
8
+ return false
9
+ }
10
+
11
+ return matchesQuery(test, moduleId, state.query)
12
+ }
@@ -1,10 +1,14 @@
1
1
  import type { ManifestTest } from '@describe-me/core/types'
2
+ import { shownTags } from './shown-tags.js'
2
3
 
3
- /** Whether every word of the sidebar's search occurs in the test's full name, file or component. */
4
+ /**
5
+ * Whether every word of the sidebar's search occurs in the test's full name,
6
+ * file, component or tags.
7
+ */
4
8
  export function matchesQuery(test: ManifestTest, moduleId: string, query: string): boolean {
5
9
  const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean)
6
- const fields = [test.fullName, moduleId, test.component?.name ?? ''].map((field) =>
7
- field.toLowerCase(),
10
+ const fields = [test.fullName, moduleId, test.component?.name ?? '', ...shownTags(test)].map(
11
+ (field) => field.toLowerCase(),
8
12
  )
9
13
 
10
14
  return terms.every((term) => fields.some((field) => field.includes(term)))
package/src/regions.ts CHANGED
@@ -7,6 +7,8 @@ export interface Regions {
7
7
  sidebar: HTMLElement
8
8
  /** The sidebar's search field, above the tree. */
9
9
  search: HTMLInputElement
10
+ /** The tag the tree is narrowed to, between the search and the tree. Hidden without one. */
11
+ tagFilter: HTMLElement
10
12
  /** The sidebar's module blocks go here. */
11
13
  tree: HTMLElement
12
14
  center: HTMLElement
@@ -33,8 +35,9 @@ export function regions(): Regions {
33
35
  'aria-label': 'Search tests',
34
36
  })
35
37
 
38
+ const tagFilter = el('div', { class: 'tag-filter', role: 'status', hidden: true })
36
39
  const tree = el('div', { class: 'tree' })
37
- const sidebar = el('aside', { class: 'sidebar' }, search, tree)
40
+ const sidebar = el('aside', { class: 'sidebar' }, search, tagFilter, tree)
38
41
  const center = el('div', { class: 'center' })
39
42
  const inspector = el('aside', { class: 'inspector' })
40
43
 
@@ -42,7 +45,7 @@ export function regions(): Regions {
42
45
  .getElementById('app')!
43
46
  .replaceChildren(header, banner, el('div', { class: 'body' }, sidebar, center, inspector))
44
47
 
45
- built = { header, banner, sidebar, search, tree, center, inspector }
48
+ built = { header, banner, sidebar, search, tagFilter, tree, center, inspector }
46
49
 
47
50
  return built
48
51
  }
@@ -4,6 +4,7 @@ import { regions } from './regions.js'
4
4
  import { renderSidebar } from './sidebar.js'
5
5
  import { currentTest, state } from './state.js'
6
6
  import { suiteKey } from './suite-key.js'
7
+ import { tagFilterBar } from './tag-filter-bar.js'
7
8
 
8
9
  let revealed: string | null = null
9
10
 
@@ -20,12 +21,12 @@ function expandPathTo(test: ManifestTest): void {
20
21
  }
21
22
 
22
23
  /**
23
- * Fill the sidebar's tree. A test selected since the last reveal gets its
24
- * module and suites opened and its row scrolled into view. A refresh that
25
- * keeps the test leaves the sidebar's scroll alone.
24
+ * Fill the sidebar's tree and the tag filter above it. A test selected since
25
+ * the last reveal gets its module and suites opened and its row scrolled into
26
+ * view. A refresh that keeps the test leaves the sidebar's scroll alone.
26
27
  */
27
28
  export function renderTree(): void {
28
- const { tree } = regions()
29
+ const { tagFilter, tree } = regions()
29
30
  const test = state.suiteKey ? null : currentTest()
30
31
  const reveal = test !== null && test.id !== revealed
31
32
 
@@ -39,6 +40,8 @@ export function renderTree(): void {
39
40
  revealed = test.id
40
41
  }
41
42
 
43
+ tagFilter.replaceChildren(...tagFilterBar())
44
+ tagFilter.hidden = state.tag === null
42
45
  tree.replaceChildren(...renderSidebar(renderTree))
43
46
 
44
47
  if (reveal) {
@@ -0,0 +1,11 @@
1
+ import { DOCS_TAG, NO_DOCS_TAG, type ManifestTest } from '@describe-me/core/types'
2
+
3
+ const HIDDEN = new Set<string>([DOCS_TAG, NO_DOCS_TAG])
4
+
5
+ /**
6
+ * The tags the viewer shows for a test. `docs` and `no-docs` only pick which
7
+ * tests record, and with `stories: 'tagged'` every story carries `docs`.
8
+ */
9
+ export function shownTags(test: ManifestTest): string[] {
10
+ return (test.tags ?? []).filter((tag) => !HIDDEN.has(tag))
11
+ }
package/src/sidebar.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  import type { ManifestModule, ManifestTest } from '@describe-me/core/types'
2
2
  import { CollapsedSuites } from './collapsed-suites.js'
3
3
  import { el } from './el.js'
4
- import { matchesQuery } from './matches-query.js'
4
+ import { filterActive } from './filter-active.js'
5
+ import { matchesFilter } from './matches-filter.js'
6
+ import { shownTags } from './shown-tags.js'
5
7
  import { soleSuite } from './sole-suite.js'
6
8
  import { select, selectSuite, state } from './state.js'
7
9
  import { suiteKey } from './suite-key.js'
8
10
  import { buildTree, type SuiteNode } from './tree.js'
9
11
 
10
- /** What every row of one repaint needs to know. `collapsed` is `null` while a search is on. */
12
+ /** What every row of one repaint needs to know. `collapsed` is `null` while a filter is on. */
11
13
  interface Walk {
12
14
  moduleId: string
13
15
  collapsed: CollapsedSuites | null
@@ -69,6 +71,17 @@ function suiteButton(label: string, key: string): HTMLElement {
69
71
  )
70
72
  }
71
73
 
74
+ /** The test's tags, quiet next to its name. A row is a button, so they are not buttons too. */
75
+ function tagChips(test: ManifestTest): HTMLElement | null {
76
+ const tags = shownTags(test)
77
+
78
+ if (tags.length === 0) {
79
+ return null
80
+ }
81
+
82
+ return el('span', { class: 'test-tags' }, ...tags.map((tag) => el('span', { class: 'tag' }, tag)))
83
+ }
84
+
72
85
  function testButton(test: ManifestTest): HTMLElement {
73
86
  return el(
74
87
  'button',
@@ -79,16 +92,17 @@ function testButton(test: ManifestTest): HTMLElement {
79
92
  },
80
93
  el('span', { class: `dot ${test.state}` }),
81
94
  el('span', { class: 'test-name' }, test.name),
95
+ tagChips(test),
82
96
  el('span', { class: 'n' }, String(test.frames.length)),
83
97
  )
84
98
  }
85
99
 
86
- /** The rows under one suite, in source order. A search leaves out tests that do not match. */
100
+ /** The rows under one suite, in source order. A filter leaves out tests that do not match. */
87
101
  function renderRows(node: SuiteNode, path: string[], walk: Walk): DocumentFragment {
88
102
  const rows = document.createDocumentFragment()
89
103
  for (const child of node.children) {
90
104
  if (!('suites' in child)) {
91
- if (matchesQuery(child, walk.moduleId, state.query)) {
105
+ if (matchesFilter(child, walk.moduleId)) {
92
106
  rows.append(testButton(child))
93
107
  }
94
108
 
@@ -104,7 +118,7 @@ function renderRows(node: SuiteNode, path: string[], walk: Walk): DocumentFragme
104
118
  return rows
105
119
  }
106
120
 
107
- /** A suite and its rows, or `null` when a search leaves nothing in it. */
121
+ /** A suite and its rows, or `null` when a filter leaves nothing in it. */
108
122
  function renderSuite(node: SuiteNode, path: string[], walk: Walk): HTMLElement | null {
109
123
  const key = suiteKey(walk.moduleId, path)
110
124
  const header = headerRow(suiteButton(node.name, key), node.name, key, walk)
@@ -121,14 +135,14 @@ function renderSuite(node: SuiteNode, path: string[], walk: Walk): HTMLElement |
121
135
  return el('div', { class: 'suite' }, header, rows)
122
136
  }
123
137
 
124
- /** A module block, or `null` when a search leaves nothing in it. */
138
+ /** A module block, or `null` when a filter leaves nothing in it. */
125
139
  function renderModule(
126
140
  mod: ManifestModule,
127
141
  collapsed: CollapsedSuites | null,
128
142
  onToggle: () => void,
129
143
  ): HTMLElement | null {
130
144
  const walk: Walk = { moduleId: mod.id, collapsed, onToggle }
131
- // On the whole tree, so a module header keeps its shape while a search narrows it.
145
+ // On the whole tree, so a module header keeps its shape while a filter narrows it.
132
146
  const root = buildTree(mod)
133
147
  const only = soleSuite(root)
134
148
  const path = only ? [only.name] : []
@@ -152,9 +166,22 @@ function renderModule(
152
166
  return el('div', { class: 'module' }, header, el('div', { class: 'suite root' }, rows))
153
167
  }
154
168
 
169
+ /** What the empty sidebar says, naming the search, the tag or both. */
170
+ function noMatches(): string {
171
+ const query = state.query.trim()
172
+
173
+ if (state.tag === null) {
174
+ return `no tests match "${query}"`
175
+ }
176
+
177
+ return query
178
+ ? `no tests tagged "${state.tag}" match "${query}"`
179
+ : `no tests tagged "${state.tag}"`
180
+ }
181
+
155
182
  /**
156
183
  * One block per test module, each holding its suite tree, or a line saying the
157
- * search found nothing. `onToggle` repaints after a module or suite opens or closes.
184
+ * filter found nothing. `onToggle` repaints after a module or suite opens or closes.
158
185
  */
159
186
  export function renderSidebar(onToggle: () => void): HTMLElement[] {
160
187
  const manifest = state.manifest
@@ -162,14 +189,14 @@ export function renderSidebar(onToggle: () => void): HTMLElement[] {
162
189
  return []
163
190
  }
164
191
 
165
- const query = state.query.trim()
166
- const collapsed = query ? null : CollapsedSuites.for(manifest.root)
192
+ const filtered = filterActive()
193
+ const collapsed = filtered ? null : CollapsedSuites.for(manifest.root)
167
194
  const blocks = manifest.modules
168
195
  .map((mod) => renderModule(mod, collapsed, onToggle))
169
196
  .filter((block): block is HTMLElement => block !== null)
170
197
 
171
- if (blocks.length === 0 && query) {
172
- return [el('div', { class: 'empty' }, `no tests match "${query}"`)]
198
+ if (blocks.length === 0 && filtered) {
199
+ return [el('div', { class: 'empty' }, noMatches())]
173
200
  }
174
201
 
175
202
  return blocks
package/src/state-hash.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import type { State } from './state.js'
2
2
 
3
- /** The link of a view, without the `#`: the selection, the frame, the viewport size and Live. */
3
+ /**
4
+ * The link of a view, without the `#`: the selection, the frame, the viewport
5
+ * size, Live and the tag filter.
6
+ */
4
7
  export function stateHash(state: State): string {
5
8
  const params = new URLSearchParams()
6
9
  if (state.suiteKey) {
@@ -27,5 +30,9 @@ export function stateHash(state: State): string {
27
30
  params.set('live', '1')
28
31
  }
29
32
 
33
+ if (state.tag !== null) {
34
+ params.set('tag', state.tag)
35
+ }
36
+
30
37
  return params.toString()
31
38
  }
package/src/state.ts CHANGED
@@ -23,6 +23,8 @@ export interface State {
23
23
  scale: number
24
24
  /** The sidebar's search. Neither in the link nor stored. */
25
25
  query: string
26
+ /** The tag the sidebar is narrowed to. `tag=` in the link, so a filtered list can be shared. */
27
+ tag: string | null
26
28
  }
27
29
 
28
30
  export const state: State = {
@@ -36,6 +38,7 @@ export const state: State = {
36
38
  issuesOpen: false,
37
39
  scale: 1,
38
40
  query: '',
41
+ tag: null,
39
42
  }
40
43
 
41
44
  export function readHash(): void {
@@ -44,6 +47,7 @@ export function readHash(): void {
44
47
  state.suiteKey = params.get('suite')
45
48
  state.frame = Number(params.get('frame') ?? 0) || 0
46
49
  state.live = params.get('live') === '1'
50
+ state.tag = params.get('tag') || null
47
51
  state.viewport = {
48
52
  width: parseViewportSize(params.get('w')),
49
53
  height: parseViewportSize(params.get('h'), MIN_FRAME_HEIGHT),
@@ -87,6 +91,13 @@ export function select(testId: string, frame = 0, mode: 'push' | 'replace' = 'pu
87
91
  rerender()
88
92
  }
89
93
 
94
+ /** Narrow the sidebar to the tests with `tag`, or show every test again with `null`. */
95
+ export function filterByTag(tag: string | null): void {
96
+ state.tag = tag
97
+ writeHash('push')
98
+ rerender()
99
+ }
100
+
90
101
  /** Show the overview of a suite or module. The selected test is kept, so going back works. */
91
102
  export function selectSuite(key: string): void {
92
103
  state.suiteKey = key
package/src/style.css CHANGED
@@ -378,6 +378,32 @@ a {
378
378
  border-color: var(--ring);
379
379
  outline-offset: 0;
380
380
  }
381
+ /* The tag the tree is narrowed to, under the search. Clearing it is one click away. */
382
+ .tag-filter {
383
+ display: flex;
384
+ align-items: center;
385
+ gap: 6px;
386
+ margin: -6px 0 12px;
387
+ padding: 0 9px;
388
+ color: var(--ink-3);
389
+ font-size: 12px;
390
+ }
391
+ .tag-filter[hidden] {
392
+ display: none;
393
+ }
394
+ .tag-filter-clear {
395
+ margin-left: auto;
396
+ width: 20px;
397
+ height: 20px;
398
+ border-radius: var(--r-sm);
399
+ color: var(--ink-3);
400
+ line-height: 1;
401
+ }
402
+ .tag-filter-clear:hover {
403
+ background: var(--hover);
404
+ color: var(--ink);
405
+ }
406
+
381
407
  .tree > .empty {
382
408
  padding: 4px 9px;
383
409
  color: var(--ink-3);
@@ -518,6 +544,37 @@ a {
518
544
  border-radius: 0 2px 2px 0;
519
545
  background: var(--accent);
520
546
  }
547
+ /* Tags come from the codebase, so they are set in the mono face, quiet next to the name. */
548
+ .tag {
549
+ display: inline-block;
550
+ padding: 0 7px;
551
+ border-radius: var(--r-pill);
552
+ background: var(--tint);
553
+ color: var(--accent);
554
+ font-family: var(--font-mono);
555
+ font-size: 10.5px;
556
+ font-weight: 400;
557
+ line-height: 17px;
558
+ white-space: nowrap;
559
+ }
560
+ .test-tags {
561
+ display: inline-flex;
562
+ flex-shrink: 0;
563
+ gap: 4px;
564
+ }
565
+ .tags {
566
+ display: flex;
567
+ flex-wrap: wrap;
568
+ gap: 6px;
569
+ margin-top: 10px;
570
+ }
571
+ button.tag:hover {
572
+ background: var(--tint-strong);
573
+ }
574
+ button.tag.active {
575
+ background: var(--accent);
576
+ color: var(--panel);
577
+ }
521
578
  .test .n {
522
579
  margin-left: auto;
523
580
  color: var(--ink-3);
@@ -1,11 +1,24 @@
1
+ import type { Manifest, ManifestTest } from '@describe-me/core/types'
2
+ import { filterActive } from './filter-active.js'
1
3
  import { resolveTestId } from './resolve-test-id.js'
2
4
  import { stateHash } from './state-hash.js'
3
5
  import { allTests, currentTest, state, writeHash } from './state.js'
6
+ import { visibleTests } from './visible-tests.js'
7
+
8
+ /**
9
+ * The first test, or the first a filter leaves, so a link with only a tag
10
+ * opens one of its tests.
11
+ */
12
+ function firstTest(manifest: Manifest): ManifestTest | undefined {
13
+ const shown = filterActive() ? visibleTests() : []
14
+
15
+ return shown[0] ?? allTests(manifest)[0]
16
+ }
4
17
 
5
18
  /**
6
19
  * Point the selection at a test the manifest holds, falling back to the first
7
- * one, and clamp the frame. The link is corrected in place, without a history
8
- * entry. Does not repaint.
20
+ * one the search and the tag leave, and clamp the frame. The link is corrected
21
+ * in place, without a history entry. Does not repaint.
9
22
  */
10
23
  export function syncSelection(): void {
11
24
  if (!state.manifest) {
@@ -16,7 +29,7 @@ export function syncSelection(): void {
16
29
  if (resolved) {
17
30
  state.testId = resolved
18
31
  } else {
19
- state.testId = allTests(state.manifest)[0]?.id ?? null
32
+ state.testId = firstTest(state.manifest)?.id ?? null
20
33
  state.frame = 0
21
34
  }
22
35
 
@@ -0,0 +1,30 @@
1
+ import type { ManifestTest } from '@describe-me/core/types'
2
+ import { el } from './el.js'
3
+ import { shownTags } from './shown-tags.js'
4
+ import { filterByTag, state } from './state.js'
5
+
6
+ /** The test's tags in the inspector. One narrows the sidebar to its tests, and again shows all. */
7
+ export function tagButtons(test: ManifestTest): HTMLElement | null {
8
+ const tags = shownTags(test)
9
+
10
+ if (tags.length === 0) {
11
+ return null
12
+ }
13
+
14
+ const buttons = tags.map((tag) => {
15
+ const active = tag === state.tag
16
+
17
+ return el(
18
+ 'button',
19
+ {
20
+ class: `tag${active ? ' active' : ''}`,
21
+ 'aria-pressed': String(active),
22
+ title: active ? 'show every test' : `show only the tests tagged ${tag}`,
23
+ click: () => filterByTag(active ? null : tag),
24
+ },
25
+ tag,
26
+ )
27
+ })
28
+
29
+ return el('div', { class: 'tags' }, ...buttons)
30
+ }
@@ -0,0 +1,24 @@
1
+ import { el } from './el.js'
2
+ import { filterByTag, state } from './state.js'
3
+
4
+ /** The line above the tree while a tag narrows it, with the button that clears the filter. */
5
+ export function tagFilterBar(): HTMLElement[] {
6
+ if (state.tag === null) {
7
+ return []
8
+ }
9
+
10
+ return [
11
+ el('span', { class: 'tag-filter-label' }, 'tagged'),
12
+ el('span', { class: 'tag' }, state.tag),
13
+ el(
14
+ 'button',
15
+ {
16
+ class: 'tag-filter-clear',
17
+ 'aria-label': `Show every test, not only those tagged ${state.tag}`,
18
+ title: 'show every test',
19
+ click: () => filterByTag(null),
20
+ },
21
+ '×',
22
+ ),
23
+ ]
24
+ }
@@ -1,6 +1,7 @@
1
1
  import type { ManifestTest } from '@describe-me/core/types'
2
2
  import { CollapsedSuites } from './collapsed-suites.js'
3
- import { matchesQuery } from './matches-query.js'
3
+ import { filterActive } from './filter-active.js'
4
+ import { matchesFilter } from './matches-filter.js'
4
5
  import { soleSuite } from './sole-suite.js'
5
6
  import { state } from './state.js'
6
7
  import { suiteKey } from './suite-key.js'
@@ -15,7 +16,7 @@ function collect(
15
16
  ): void {
16
17
  for (const child of node.children) {
17
18
  if (!('suites' in child)) {
18
- if (matchesQuery(child, moduleId, state.query)) {
19
+ if (matchesFilter(child, moduleId)) {
19
20
  into.push(child)
20
21
  }
21
22
 
@@ -30,8 +31,8 @@ function collect(
30
31
  }
31
32
 
32
33
  /**
33
- * The tests the sidebar shows, in its order, after the search and the
34
- * collapsed suites. It walks the same tree by the same rules as the sidebar.
34
+ * The tests the sidebar shows, in its order, after the search, the tag filter
35
+ * and the collapsed suites. It walks the same tree by the same rules as the sidebar.
35
36
  */
36
37
  export function visibleTests(): ManifestTest[] {
37
38
  const manifest = state.manifest
@@ -39,8 +40,8 @@ export function visibleTests(): ManifestTest[] {
39
40
  return []
40
41
  }
41
42
 
42
- // A search shows every match, collapsed or not.
43
- const collapsed = state.query.trim() ? null : CollapsedSuites.for(manifest.root)
43
+ // A search or a tag shows every match, collapsed or not.
44
+ const collapsed = filterActive() ? null : CollapsedSuites.for(manifest.root)
44
45
  const tests: ManifestTest[] = []
45
46
 
46
47
  for (const mod of manifest.modules) {