@crvouga/atlas 1.5.0 → 1.6.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/CHANGELOG.md +7 -1
- package/docs/ui.md +11 -1
- package/llms-full.txt +11 -1
- package/package.json +1 -1
- package/packages/visualizer/README.md +28 -0
- package/packages/visualizer/src/explorer/Explorer.tsx +105 -25
- package/packages/visualizer/src/explorer/RunsView.tsx +173 -35
- package/packages/visualizer/src/explorer/catalog.tsx +9 -5
- package/packages/visualizer/src/explorer/search-notes.tsx +74 -0
- package/packages/visualizer/src/explorer/search.ts +16 -6
- package/packages/visualizer/src/find/Finder.tsx +127 -81
- package/packages/visualizer/src/find/behaviors.ts +205 -0
- package/packages/visualizer/src/find/corpus.ts +21 -7
- package/packages/visualizer/src/find/doc-search.ts +66 -0
- package/packages/visualizer/src/find/query.ts +247 -0
- package/packages/visualizer/src/find/rank.ts +195 -47
- package/packages/visualizer/src/find/search.test.ts +322 -0
- package/packages/visualizer/src/find/semantic-search.ts +153 -0
- package/packages/visualizer/src/find/semantic.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# Changelog: @crvouga/atlas
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1.6.0 (2026-10-29)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- unify fuzzy semantic search with complete filterable sortable results ([3b0b90e](https://github.com/crvouga/atlas/commit/3b0b90e4960dcbcbcfee924f33aca4ce02beaaec))
|
|
8
|
+
|
|
9
|
+
## 1.5.0 (2026-10-28)
|
|
4
10
|
|
|
5
11
|
### Features
|
|
6
12
|
|
package/docs/ui.md
CHANGED
|
@@ -101,6 +101,16 @@ visible notation label; marks are decorative. Sources, licenses and the DMN nota
|
|
|
101
101
|
and update this mapping when adding a format. Brand artwork preserves upstream colors independently of
|
|
102
102
|
Atlas's theme. SVG data plots and statechart arrow markers are geometry, not handcrafted UI icons.
|
|
103
103
|
|
|
104
|
+
## Search
|
|
105
|
+
|
|
106
|
+
All search boxes (finder, test list, Runs tab, an opened run) use `src/find/query.ts` for the query language and
|
|
107
|
+
`src/find/rank.ts` for matching; do not add a second matcher or filter on `includes`. Build documents from
|
|
108
|
+
`src/find/behaviors.ts` so a behavior has the same fields and facets everywhere. Match first, then count, filter and sort
|
|
109
|
+
the whole set, then page. Show filters typed in the box as removable chips, show what was not understood
|
|
110
|
+
(`SearchNotes`), show why a result matched and mark exact ones, and say when matching is words only (no embedder, or
|
|
111
|
+
a failed one). A bounded list says it is bounded and offers all results. Selection follows result identity, never a
|
|
112
|
+
position, when results change. The query, filters and sort stay in the address.
|
|
113
|
+
|
|
104
114
|
## Accessibility and responsive behavior
|
|
105
115
|
|
|
106
116
|
The shell collapses its index below Mantine's `md` breakpoint; its named Burger toggles mobile navigation.
|
|
@@ -151,7 +161,7 @@ The narrowly scoped exceptions are:
|
|
|
151
161
|
| Statecharts | Graph, screen/event/journey details, developer metadata, chart test results | Mantine cards/navigation and product compositions; graph exception above |
|
|
152
162
|
| Evidence | Steps, retries, annotations, facts, unknown extensions, safe downloads and previews | List, Badge, Accordion, Code, Image, native media |
|
|
153
163
|
| Recordings | Pictures/video/timeline, actor selection, moments, failure jumps, timing, model-state differences | Tabs, Slider, Button, ScrollArea, Table, Progress |
|
|
154
|
-
| Search/help | Ranked and semantic search, shortcut help, recording shortcuts | Spotlight, Modal, Kbd |
|
|
164
|
+
| Search/help | Ranked, filterable and semantic search across behaviors, runs and results; complete results; shortcut help, recording shortcuts | Spotlight, TextInput, NativeSelect, Button chips, Alert, Modal, Kbd |
|
|
155
165
|
| Administration | Token access, S3 storage notice, garbage collection policy (saved in S3), live timeouts, report rules, limits, raw JSON, conflicts | Same provider/theme, Fieldset, Mantine inputs, Accordion, Alert |
|
|
156
166
|
|
|
157
167
|
The hash routes (`/`, `/chart/$chartId`, `/specs/$kind`), query contracts, project scoping, streaming,
|
package/llms-full.txt
CHANGED
|
@@ -3933,6 +3933,16 @@ visible notation label; marks are decorative. Sources, licenses and the DMN nota
|
|
|
3933
3933
|
and update this mapping when adding a format. Brand artwork preserves upstream colors independently of
|
|
3934
3934
|
Atlas's theme. SVG data plots and statechart arrow markers are geometry, not handcrafted UI icons.
|
|
3935
3935
|
|
|
3936
|
+
## Search
|
|
3937
|
+
|
|
3938
|
+
All search boxes (finder, test list, Runs tab, an opened run) use `src/find/query.ts` for the query language and
|
|
3939
|
+
`src/find/rank.ts` for matching; do not add a second matcher or filter on `includes`. Build documents from
|
|
3940
|
+
`src/find/behaviors.ts` so a behavior has the same fields and facets everywhere. Match first, then count, filter and sort
|
|
3941
|
+
the whole set, then page. Show filters typed in the box as removable chips, show what was not understood
|
|
3942
|
+
(`SearchNotes`), show why a result matched and mark exact ones, and say when matching is words only (no embedder, or
|
|
3943
|
+
a failed one). A bounded list says it is bounded and offers all results. Selection follows result identity, never a
|
|
3944
|
+
position, when results change. The query, filters and sort stay in the address.
|
|
3945
|
+
|
|
3936
3946
|
## Accessibility and responsive behavior
|
|
3937
3947
|
|
|
3938
3948
|
The shell collapses its index below Mantine's `md` breakpoint; its named Burger toggles mobile navigation.
|
|
@@ -3983,7 +3993,7 @@ The narrowly scoped exceptions are:
|
|
|
3983
3993
|
| Statecharts | Graph, screen/event/journey details, developer metadata, chart test results | Mantine cards/navigation and product compositions; graph exception above |
|
|
3984
3994
|
| Evidence | Steps, retries, annotations, facts, unknown extensions, safe downloads and previews | List, Badge, Accordion, Code, Image, native media |
|
|
3985
3995
|
| Recordings | Pictures/video/timeline, actor selection, moments, failure jumps, timing, model-state differences | Tabs, Slider, Button, ScrollArea, Table, Progress |
|
|
3986
|
-
| Search/help | Ranked and semantic search, shortcut help, recording shortcuts | Spotlight, Modal, Kbd |
|
|
3996
|
+
| Search/help | Ranked, filterable and semantic search across behaviors, runs and results; complete results; shortcut help, recording shortcuts | Spotlight, TextInput, NativeSelect, Button chips, Alert, Modal, Kbd |
|
|
3987
3997
|
| Administration | Token access, S3 storage notice, garbage collection policy (saved in S3), live timeouts, report rules, limits, raw JSON, conflicts | Same provider/theme, Fieldset, Mantine inputs, Accordion, Alert |
|
|
3988
3998
|
|
|
3989
3999
|
The hash routes (`/`, `/chart/$chartId`, `/specs/$kind`), query contracts, project scoping, streaming,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crvouga/atlas",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Atlas: specs of any kind, reported as layered evidence. Publish checks from any runner (JUnit, CTRF, a script), combine them across nodes, branches, environments and backends, gate CI on the result, and run statecharts against the real app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -154,6 +154,34 @@ Components read only the view model (`AtlasView`), never raw files.
|
|
|
154
154
|
its own worker. Positions are cached under a hash of the chart's structure only, so new results
|
|
155
155
|
never move anything.
|
|
156
156
|
|
|
157
|
+
## Searching behaviors, runs and recordings
|
|
158
|
+
|
|
159
|
+
Every search box (the finder opened with `Ctrl+K` or `/`, the test list, the Runs tab and the results of an opened
|
|
160
|
+
run) reads one query language and matches with one engine (`src/find/query.ts`, `src/find/rank.ts`), so the same words
|
|
161
|
+
make the same behaviors eligible everywhere.
|
|
162
|
+
|
|
163
|
+
- **Words** match titles, steps, descriptions, specs, tags, environments and failure messages by stem, everyday synonym
|
|
164
|
+
and close spelling (`savde answer` finds "Keep a saved answer"), and the start of a longer word while it is typed.
|
|
165
|
+
Every distinctive word must be found; a word present in most behaviors is optional. When an embedder is available the
|
|
166
|
+
results are also re-ranked by meaning and a paraphrase with no word in common is added (`Similar meaning`); when none
|
|
167
|
+
is, or it fails, the box says it is matching words and spellings only.
|
|
168
|
+
- **Exact matches.** `"a quoted phrase"` and identifiers with `/`, `_` or `.` (`memory/keep`) must appear as written, are
|
|
169
|
+
marked `Exact` and are listed first.
|
|
170
|
+
- **Filters** narrow by structure instead of matching words: `status:failed` (`passed`, `failed`, `flaky`, `not-run`,
|
|
171
|
+
`running`), `env:ios` (also `platform:`, `environment:`, `device:`), `producer:`, `domain:`, `spec:`, `tag:`, `branch:`
|
|
172
|
+
and `has:video` (a playable recording, not the word). Values of one field are alternatives (`status:failed,flaky`);
|
|
173
|
+
different fields all hold. Each shows as a removable chip, and the status, environment and Has video buttons are the
|
|
174
|
+
same filters without the syntax.
|
|
175
|
+
- **Nothing is dropped silently.** An unknown operator, an unknown status, an unclosed quote, or a filter a list does not
|
|
176
|
+
have (`domain:` on runs) is listed under the box with what to type instead.
|
|
177
|
+
- **Complete results.** Counts, filters and sorting (including Best match, offered with words to match) apply to the whole
|
|
178
|
+
dataset before anything is shown in pages. The finder shows its first 40 results, says so, and opens all of them
|
|
179
|
+
(`Shift+Enter`, or the explorer's list for the same query). The query, filters and sort are in the address, so a link
|
|
180
|
+
and the back button restore them. Results that change under the cursor keep the highlighted result highlighted by
|
|
181
|
+
identity, and say so when it is no longer listed.
|
|
182
|
+
|
|
183
|
+
The standing rules are in [the UI conventions](../../docs/ui.md#search).
|
|
184
|
+
|
|
157
185
|
## Fixtures
|
|
158
186
|
|
|
159
187
|
`fixtures/` holds seven scenarios generated by `scripts/make-fixtures.ts` from `example/specs`, a
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SpecLogo } from '../components/SpecLogo';
|
|
2
|
-
import { IconX } from '@tabler/icons-react';
|
|
2
|
+
import { IconVideo, IconX } from '@tabler/icons-react';
|
|
3
3
|
import {
|
|
4
4
|
Accordion,
|
|
5
5
|
Alert,
|
|
@@ -37,11 +37,9 @@ import {
|
|
|
37
37
|
durationStats,
|
|
38
38
|
formatMs,
|
|
39
39
|
matchesQuery,
|
|
40
|
-
TEST_SORTS,
|
|
41
40
|
testCatalog,
|
|
42
41
|
type CatalogModel,
|
|
43
42
|
type TestQuery,
|
|
44
|
-
type TestSort,
|
|
45
43
|
type TestSummary
|
|
46
44
|
} from '@crvouga/atlas/reports/test-catalog';
|
|
47
45
|
import { Link } from '@tanstack/react-router';
|
|
@@ -50,13 +48,18 @@ import { useEvidence } from '../evidence/evidence-context';
|
|
|
50
48
|
import { ago, plural, useNow } from '../evidence/format';
|
|
51
49
|
import { Meter, StatusMark } from '../evidence/marks';
|
|
52
50
|
import { SPEC_KINDS } from '../spec-kind';
|
|
51
|
+
import { behaviorDocs, behaviorId, describeHit, testHasVideo } from '../find/behaviors';
|
|
52
|
+
import { useDocSearch } from '../find/doc-search';
|
|
53
|
+
import { hasFilters, passesFilters, withoutToken, type FilterKey, type ParsedSearch } from '../find/query';
|
|
54
|
+
import type { FindDoc, Ranked } from '../find/rank';
|
|
53
55
|
import { useRunDetail, useTestCatalog } from './catalog';
|
|
54
56
|
import { groupTests, tally, type TestGroupView } from './groups';
|
|
55
57
|
import { patched, useExplorerNavigate, useExplorerSearch } from './navigate';
|
|
56
58
|
import { Badges, EnvironmentChips, projectTitle, runName } from './parts';
|
|
57
59
|
import { ArtifactsView } from './ArtifactsView';
|
|
58
60
|
import { RunsView } from './RunsView';
|
|
59
|
-
import {
|
|
61
|
+
import { SearchNotes, TypedFilters } from './search-notes';
|
|
62
|
+
import { EXPLORER_SORTS, joined, narrowed, queryOf, sortOf, TEST_GROUPS, toggled, values, type ExplorerSearch, type ExplorerSort, type TestGroup } from './search';
|
|
60
63
|
import { TestDetail } from './TestDetail';
|
|
61
64
|
/** Groups start closed when there are more of them than this, unless the list is short. */
|
|
62
65
|
const OPEN_GROUPS = 40;
|
|
@@ -71,7 +74,8 @@ const GROUP_LABEL: Record<TestGroup, string> = {
|
|
|
71
74
|
producer: 'Producer',
|
|
72
75
|
none: 'None'
|
|
73
76
|
};
|
|
74
|
-
const SORT_LABEL: Record<
|
|
77
|
+
const SORT_LABEL: Record<ExplorerSort, string> = {
|
|
78
|
+
relevance: 'Best match',
|
|
75
79
|
spec: 'Spec',
|
|
76
80
|
title: 'Title',
|
|
77
81
|
recent: 'Most recent',
|
|
@@ -91,30 +95,59 @@ const STATUS_FILTERS = [
|
|
|
91
95
|
function percent(rate: number | null): string {
|
|
92
96
|
return rate === null ? '—' : `${Math.round(rate * 100)}%`;
|
|
93
97
|
}
|
|
94
|
-
|
|
98
|
+
const NO_STRUCTURE: ReadonlySet<FilterKey> = new Set();
|
|
99
|
+
const WITHOUT_STATUS_AND_ENVIRONMENT: ReadonlySet<FilterKey> = new Set<FilterKey>(['status', 'env']);
|
|
100
|
+
const WITHOUT_STATUS: ReadonlySet<FilterKey> = new Set<FilterKey>(['status']);
|
|
101
|
+
const WITHOUT_ENVIRONMENT: ReadonlySet<FilterKey> = new Set<FilterKey>(['env']);
|
|
102
|
+
const WITHOUT_VIDEO: ReadonlySet<FilterKey> = new Set<FilterKey>(['video']);
|
|
103
|
+
/**
|
|
104
|
+
* What the explorer lists for an address: the tests shown, and the scope the insight strip and filter counts describe.
|
|
105
|
+
* The words, quoted phrases and filters typed in the search box are read by the shared search (`find/`), over every
|
|
106
|
+
* test, before anything is counted or ordered; nothing is cut to a page here.
|
|
107
|
+
*/
|
|
95
108
|
function useListing(model: CatalogModel | null, search: ExplorerSearch, q: string | undefined) {
|
|
96
109
|
const run = useRunDetail(search.inRun);
|
|
97
110
|
const inRun = useMemo(
|
|
98
111
|
() => (search.inRun ? new Set(run.data?.results.map((result) => result.key) ?? []) : null),
|
|
99
112
|
[search.inRun, run.data]
|
|
100
113
|
);
|
|
101
|
-
|
|
114
|
+
const docs = useMemo(() => ((q || search.video) && model ? behaviorDocs(model.tests) : new Map<string, FindDoc>()), [q, search.video, model?.tests]);
|
|
115
|
+
const list = useMemo(() => [...docs.values()], [docs]);
|
|
116
|
+
const found = useDocSearch(list, q);
|
|
117
|
+
const sort = sortOf(search);
|
|
118
|
+
const listing = useMemo(() => {
|
|
102
119
|
if (!model) return null;
|
|
103
|
-
const query: TestQuery = { ...queryOf(search),
|
|
104
|
-
|
|
120
|
+
const query: TestQuery = { ...queryOf(search), q: undefined };
|
|
121
|
+
// `has:video` typed in the box and the Has video button are the same filter.
|
|
122
|
+
const operator = { ...found.parsed.filters, video: found.parsed.filters.video || search.video === true };
|
|
123
|
+
const structured = hasFilters(operator);
|
|
124
|
+
const hits = found.hits;
|
|
125
|
+
const passes = (test: TestSummary, omit: ReadonlySet<FilterKey>) =>
|
|
126
|
+
(!hits || hits.has(behaviorId(test.key))) && (!structured || passesFilters(docs.get(test.key)?.facets, operator, omit));
|
|
127
|
+
let shown = testCatalog(model, { ...query, limit: 20000 }).tests.filter((test) => (!inRun || inRun.has(test.key)) && passes(test, NO_STRUCTURE));
|
|
128
|
+
if (sort === 'relevance' && hits) {
|
|
129
|
+
const order = new Map([...hits.keys()].map((id, at) => [id, at] as const));
|
|
130
|
+
shown = [...shown].sort((a, b) => (order.get(behaviorId(a.key)) ?? 0) - (order.get(behaviorId(b.key)) ?? 0));
|
|
131
|
+
}
|
|
105
132
|
// The strip and the status and environment counts describe what the other filters leave.
|
|
106
|
-
const scope = model.tests.filter((test) => matchesQuery(model, test, { ...query, status: [], environment: [] }, inRun));
|
|
107
|
-
const byEnvironment = scope.filter((test) => matchesQuery(model, test, { environment: query.environment }));
|
|
108
|
-
const byStatus = scope.filter((test) => matchesQuery(model, test, { status: query.status }));
|
|
133
|
+
const scope = model.tests.filter((test) => matchesQuery(model, test, { ...query, status: [], environment: [] }, inRun) && passes(test, WITHOUT_STATUS_AND_ENVIRONMENT));
|
|
134
|
+
const byEnvironment = scope.filter((test) => matchesQuery(model, test, { environment: query.environment }) && passes(test, WITHOUT_STATUS));
|
|
135
|
+
const byStatus = scope.filter((test) => matchesQuery(model, test, { status: query.status }) && passes(test, WITHOUT_ENVIRONMENT));
|
|
136
|
+
const videoScope = model.tests.filter((test) => matchesQuery(model, test, query, inRun) && passes(test, WITHOUT_VIDEO));
|
|
109
137
|
return {
|
|
110
138
|
shown,
|
|
139
|
+
withVideo: videoScope.filter((test) => docs.get(test.key)?.facets?.video ?? testHasVideo(test)).length,
|
|
111
140
|
scope,
|
|
112
141
|
byEnvironment,
|
|
113
142
|
byStatus,
|
|
143
|
+
hits: hits as ReadonlyMap<string, Ranked> | null,
|
|
144
|
+
parsed: found.parsed,
|
|
145
|
+
semantic: found.status,
|
|
114
146
|
runPending: !!search.inRun && run.isPending,
|
|
115
147
|
runMissing: !!search.inRun && run.isSuccess && run.data === null
|
|
116
148
|
};
|
|
117
|
-
}, [model, search,
|
|
149
|
+
}, [model, search, found, docs, sort, inRun, run.isPending, run.isSuccess, run.data]);
|
|
150
|
+
return listing;
|
|
118
151
|
}
|
|
119
152
|
/** The project home: the published spec kinds, the insight strip, the toolbar, the grouped list and the open test. */
|
|
120
153
|
export function Explorer() {
|
|
@@ -252,7 +285,7 @@ export function Explorer() {
|
|
|
252
285
|
That run is not in the report any more.
|
|
253
286
|
</Alert>
|
|
254
287
|
)}
|
|
255
|
-
<TestList tests={listing.shown} search={search} total={model.tests.length} />
|
|
288
|
+
<TestList tests={listing.shown} search={search} total={model.tests.length} hits={listing.hits} sort={search.sort ?? (listing.hits ? 'relevance' : 'spec')} />
|
|
256
289
|
</>
|
|
257
290
|
))}
|
|
258
291
|
</Box>
|
|
@@ -496,6 +529,9 @@ function Toolbar({
|
|
|
496
529
|
onSearch: (value: string) => void;
|
|
497
530
|
}) {
|
|
498
531
|
const go = useExplorerNavigate();
|
|
532
|
+
// Best match is the order of a search with words; without words the list is in spec order.
|
|
533
|
+
const defaultSort: ExplorerSort = listing.hits ? 'relevance' : 'spec';
|
|
534
|
+
const sort: ExplorerSort = search.sort ?? defaultSort;
|
|
499
535
|
const [draft, setDraft] = useState(search.q ?? '');
|
|
500
536
|
// The address may change from elsewhere (a link, the index, back): follow it.
|
|
501
537
|
useEffect(() => setDraft((current) => (current.trim() === (search.q ?? '').trim() ? current : (search.q ?? ''))), [search.q]);
|
|
@@ -527,7 +563,7 @@ function Toolbar({
|
|
|
527
563
|
miw={{ base: '100%', sm: '15rem' }}
|
|
528
564
|
type="search"
|
|
529
565
|
aria-label="Search tests"
|
|
530
|
-
placeholder=
|
|
566
|
+
placeholder='Search titles, specs, tags… or filter: status:failed has:video "exact phrase"'
|
|
531
567
|
value={draft}
|
|
532
568
|
onChange={(event) => {
|
|
533
569
|
setDraft(event.target.value);
|
|
@@ -556,12 +592,12 @@ function Toolbar({
|
|
|
556
592
|
</Text>
|
|
557
593
|
<NativeSelect
|
|
558
594
|
aria-label="Sort"
|
|
559
|
-
value={
|
|
560
|
-
onChange={(event) => go({ sort: event.target.value ===
|
|
595
|
+
value={sort}
|
|
596
|
+
onChange={(event) => go({ sort: event.target.value === defaultSort ? undefined : (event.target.value as ExplorerSort) })}
|
|
561
597
|
>
|
|
562
|
-
{
|
|
563
|
-
<option key={
|
|
564
|
-
{SORT_LABEL[
|
|
598
|
+
{EXPLORER_SORTS.filter((option) => option !== 'relevance' || listing.hits !== null || sort === 'relevance').map((option) => (
|
|
599
|
+
<option key={option} value={option}>
|
|
600
|
+
{SORT_LABEL[option]}
|
|
565
601
|
</option>
|
|
566
602
|
))}
|
|
567
603
|
</NativeSelect>
|
|
@@ -608,6 +644,26 @@ function Toolbar({
|
|
|
608
644
|
))}
|
|
609
645
|
</Flex>
|
|
610
646
|
)}
|
|
647
|
+
<Button
|
|
648
|
+
type="button"
|
|
649
|
+
aria-pressed={search.video === true || listing.parsed.filters.video}
|
|
650
|
+
aria-label={`Has video: ${plural(listing.withVideo, 'test', 'tests')}`}
|
|
651
|
+
onClick={() => {
|
|
652
|
+
if (listing.parsed.filters.video) {
|
|
653
|
+
const token = listing.parsed.tokens.find((item) => item.key === 'video')!;
|
|
654
|
+
const next = withoutToken(draft, token);
|
|
655
|
+
setDraft(next);
|
|
656
|
+
onSearch(next);
|
|
657
|
+
} else go({ video: search.video ? undefined : true });
|
|
658
|
+
}}
|
|
659
|
+
variant={search.video === true || listing.parsed.filters.video ? 'light' : 'subtle'}
|
|
660
|
+
leftSection={<IconVideo size={16} aria-hidden="true" />}
|
|
661
|
+
>
|
|
662
|
+
Has video{' '}
|
|
663
|
+
<Text span inherit>
|
|
664
|
+
{listing.withVideo.toLocaleString()}
|
|
665
|
+
</Text>
|
|
666
|
+
</Button>
|
|
611
667
|
{model.facets.producers.length > 1 && (
|
|
612
668
|
<Box component="label">
|
|
613
669
|
<VisuallyHidden>Producer</VisuallyHidden>
|
|
@@ -683,6 +739,14 @@ function Toolbar({
|
|
|
683
739
|
? plural(model.tests.length, 'test', 'tests')
|
|
684
740
|
: `${listing.shown.length.toLocaleString()} of ${plural(model.tests.length, 'test', 'tests')}`}
|
|
685
741
|
</Text>
|
|
742
|
+
<TypedFilters
|
|
743
|
+
parsed={listing.parsed}
|
|
744
|
+
query={draft}
|
|
745
|
+
onChange={(next) => {
|
|
746
|
+
setDraft(next);
|
|
747
|
+
onSearch(next);
|
|
748
|
+
}}
|
|
749
|
+
/>
|
|
686
750
|
{chips.map((chip) => (
|
|
687
751
|
<Button key={chip.label} type="button" onClick={() => go(chip.clear)} aria-label={`Remove ${chip.label}`} variant="light">
|
|
688
752
|
{chip.label}{' '}
|
|
@@ -707,7 +771,8 @@ function Toolbar({
|
|
|
707
771
|
scope: undefined,
|
|
708
772
|
artifactTag: undefined,
|
|
709
773
|
inherited: undefined,
|
|
710
|
-
inRun: undefined
|
|
774
|
+
inRun: undefined,
|
|
775
|
+
video: undefined
|
|
711
776
|
});
|
|
712
777
|
}}
|
|
713
778
|
variant="subtle"
|
|
@@ -716,12 +781,13 @@ function Toolbar({
|
|
|
716
781
|
</Button>
|
|
717
782
|
)}
|
|
718
783
|
</Flex>
|
|
784
|
+
<SearchNotes issues={listing.parsed.issues} semantic={listing.semantic} />
|
|
719
785
|
</Box>
|
|
720
786
|
);
|
|
721
787
|
}
|
|
722
|
-
function TestList({ tests, search, total }: { tests: readonly TestSummary[]; search: ExplorerSearch; total: number }) {
|
|
788
|
+
function TestList({ tests, search, total, hits, sort }: { tests: readonly TestSummary[]; search: ExplorerSearch; total: number; hits: ReadonlyMap<string, Ranked> | null; sort: ExplorerSort }) {
|
|
723
789
|
const by = search.group ?? 'spec';
|
|
724
|
-
const groups = useMemo(() => groupTests(tests, by,
|
|
790
|
+
const groups = useMemo(() => groupTests(tests, by, sort === 'spec'), [tests, by, sort]);
|
|
725
791
|
const [overrides, setOverrides] = useState<ReadonlyMap<string, boolean>>(new Map());
|
|
726
792
|
const defaultOpen = groups.length <= OPEN_GROUPS || tests.length <= OPEN_TESTS;
|
|
727
793
|
const isOpen = (group: TestGroupView) => by === 'none' || (overrides.get(`${by}|${group.key}`) ?? defaultOpen);
|
|
@@ -781,6 +847,7 @@ function TestList({ tests, search, total }: { tests: readonly TestSummary[]; sea
|
|
|
781
847
|
by={by}
|
|
782
848
|
open={isOpen(group)}
|
|
783
849
|
selected={search.test}
|
|
850
|
+
hits={hits}
|
|
784
851
|
onToggle={() => setOverrides(new Map(overrides).set(`${by}|${group.key}`, !isOpen(group)))}
|
|
785
852
|
/>
|
|
786
853
|
</Stack>
|
|
@@ -794,12 +861,14 @@ function TestGroupSection({
|
|
|
794
861
|
by,
|
|
795
862
|
open,
|
|
796
863
|
selected,
|
|
864
|
+
hits,
|
|
797
865
|
onToggle
|
|
798
866
|
}: {
|
|
799
867
|
group: TestGroupView;
|
|
800
868
|
by: TestGroup;
|
|
801
869
|
open: boolean;
|
|
802
870
|
selected: string | undefined;
|
|
871
|
+
hits: ReadonlyMap<string, Ranked> | null;
|
|
803
872
|
onToggle: () => void;
|
|
804
873
|
}) {
|
|
805
874
|
const [shown, setShown] = useState(by === 'none' ? 100 : GROUP_ROWS);
|
|
@@ -807,7 +876,7 @@ function TestGroupSection({
|
|
|
807
876
|
const rows = (
|
|
808
877
|
<List spacing="xs" listStyleType="none">
|
|
809
878
|
{group.tests.slice(0, shown).map((test) => (
|
|
810
|
-
<TestRow key={test.key} test={test} selected={selected === test.key} now={now} />
|
|
879
|
+
<TestRow key={test.key} test={test} selected={selected === test.key} now={now} hit={hits?.get(behaviorId(test.key))} />
|
|
811
880
|
))}
|
|
812
881
|
{group.tests.length > shown && (
|
|
813
882
|
<List.Item>
|
|
@@ -852,7 +921,8 @@ function TestGroupSection({
|
|
|
852
921
|
</Box>
|
|
853
922
|
);
|
|
854
923
|
}
|
|
855
|
-
function TestRow({ test, selected, now }: { test: TestSummary; selected: boolean; now: number }) {
|
|
924
|
+
function TestRow({ test, selected, now, hit }: { test: TestSummary; selected: boolean; now: number; hit: Ranked | undefined }) {
|
|
925
|
+
const why = hit ? describeHit(hit) : '';
|
|
856
926
|
const failure = test.status === 'failed' ? test.outcomes.find((outcome) => outcome.status === 'failed')?.detail.split('\n')[0] : '';
|
|
857
927
|
return (
|
|
858
928
|
<List.Item data-status={test.status}>
|
|
@@ -886,6 +956,16 @@ function TestRow({ test, selected, now }: { test: TestSummary; selected: boolean
|
|
|
886
956
|
{failure}
|
|
887
957
|
</Text>
|
|
888
958
|
)}
|
|
959
|
+
{why && (
|
|
960
|
+
<Text size="xs" c="dimmed" data-why>
|
|
961
|
+
{hit?.exact && (
|
|
962
|
+
<Badge component="span" color="blue" mr="xs">
|
|
963
|
+
Exact
|
|
964
|
+
</Badge>
|
|
965
|
+
)}
|
|
966
|
+
{why}
|
|
967
|
+
</Text>
|
|
968
|
+
)}
|
|
889
969
|
</Stack>
|
|
890
970
|
<Group gap="md" visibleFrom="sm">
|
|
891
971
|
<Text title="Average duration" size="xs" c="dimmed">
|