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 +2 -2
- package/src/filter-active.ts +6 -0
- package/src/inspector.ts +8 -2
- package/src/keyboard.ts +4 -2
- package/src/matches-filter.ts +12 -0
- package/src/matches-query.ts +7 -3
- package/src/regions.ts +5 -2
- package/src/render-tree.ts +7 -4
- package/src/shown-tags.ts +11 -0
- package/src/sidebar.ts +39 -12
- package/src/state-hash.ts +8 -1
- package/src/state.ts +11 -0
- package/src/style.css +57 -0
- package/src/sync-selection.ts +16 -3
- package/src/tag-buttons.ts +30 -0
- package/src/tag-filter-bar.ts +24 -0
- package/src/visible-tests.ts +7 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "describe-me",
|
|
3
|
-
"version": "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.
|
|
48
|
+
"@describe-me/core": "^0.7.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
|
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,
|
|
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
|
|
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 (
|
|
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
|
+
}
|
package/src/matches-query.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { ManifestTest } from '@describe-me/core/types'
|
|
2
|
+
import { shownTags } from './shown-tags.js'
|
|
2
3
|
|
|
3
|
-
/**
|
|
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(
|
|
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
|
}
|
package/src/render-tree.ts
CHANGED
|
@@ -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
|
|
24
|
-
* module and suites opened and its row scrolled into
|
|
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 {
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
166
|
-
const collapsed =
|
|
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 &&
|
|
172
|
-
return [el('div', { class: 'empty' },
|
|
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
|
-
/**
|
|
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);
|
package/src/sync-selection.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
+
}
|
package/src/visible-tests.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ManifestTest } from '@describe-me/core/types'
|
|
2
2
|
import { CollapsedSuites } from './collapsed-suites.js'
|
|
3
|
-
import {
|
|
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 (
|
|
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
|
|
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 =
|
|
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) {
|