@crvouga/atlas 1.3.0 → 1.5.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 +12 -0
- package/dist/reports/src/evidence-detail.d.ts +23 -0
- package/dist/reports/src/evidence-detail.js +19 -1
- package/dist/reports/src/export-evidence.js +1 -1
- package/dist/reports/src/layers-route.d.ts +2 -1
- package/dist/reports/src/layers-route.js +8 -4
- package/dist/reports/src/test-catalog.d.ts +27 -0
- package/dist/reports/src/test-catalog.js +8 -2
- package/dist/schema/src/api.d.ts +22 -0
- package/dist/schema/src/api.js +10 -1
- package/dist/server/src/openapi.js +2 -2
- package/docs/reference/http-api.md +4 -4
- package/docs/reference/reports.md +64 -7
- package/docs/rich-evidence.md +23 -0
- package/docs/ui.md +22 -2
- package/llms-full.txt +49 -6
- package/package.json +1 -1
- package/packages/schema/schemas/check-detail.schema.json +24 -0
- package/packages/schema/schemas/report-run.schema.json +85 -0
- package/packages/schema/schemas/report-runs.schema.json +6 -0
- package/packages/server/openapi.json +119 -4
- package/packages/visualizer/src/evidence/artifacts.tsx +69 -18
- package/packages/visualizer/src/evidence/behavior-panel.tsx +291 -0
- package/packages/visualizer/src/evidence/behavior.test.ts +99 -0
- package/packages/visualizer/src/evidence/behavior.ts +268 -0
- package/packages/visualizer/src/evidence/check-detail.tsx +4 -0
- package/packages/visualizer/src/evidence/feature-page.tsx +82 -36
- package/packages/visualizer/src/evidence/gherkin.ts +31 -1
- package/packages/visualizer/src/evidence/spec-index.tsx +48 -15
- package/packages/visualizer/src/explorer/Explorer.tsx +9 -5
- package/packages/visualizer/src/explorer/RunArtifacts.tsx +246 -0
- package/packages/visualizer/src/explorer/RunsView.tsx +40 -9
- package/packages/visualizer/src/explorer/search.ts +5 -2
|
@@ -7,7 +7,8 @@ import { Alert, Anchor, Box, Button, Code, Flex, Group, List, Stack, Table, Text
|
|
|
7
7
|
* tests and how they went, how long it took, whether it has recordings to watch, and a link to the run. Opening one lists
|
|
8
8
|
* every test it ran, filterable, with a way to show exactly those tests in the explorer. Videos are counted as
|
|
9
9
|
* references (every mention) and as distinct playable recordings (identical bytes are one), and a reference whose file
|
|
10
|
-
* was never stored is shown as unavailable, never as a recording.
|
|
10
|
+
* was never stored is shown as unavailable, never as a recording. An opened run lists its artifacts first (see
|
|
11
|
+
* `RunArtifacts`), and a run of no checks says so instead of reading as passed.
|
|
11
12
|
*/
|
|
12
13
|
import { videoCounts } from '@crvouga/atlas/reports/evidence-detail';
|
|
13
14
|
import type { VideoCounts, VideoRef } from '@crvouga/atlas/reports/evidence-types';
|
|
@@ -19,6 +20,7 @@ import { useMemo, useState } from 'react';
|
|
|
19
20
|
import { ago, exactTime, plural, shortSha, statusWord, useNow } from '../evidence/format';
|
|
20
21
|
import { Meter, StatusMark } from '../evidence/marks';
|
|
21
22
|
import { useRunDetail, useRunList } from './catalog';
|
|
23
|
+
import { RunArtifacts } from './RunArtifacts';
|
|
22
24
|
import { FilterBar } from './filters';
|
|
23
25
|
import { patched, useExplorerNavigate, useExplorerSearch } from './navigate';
|
|
24
26
|
import { runName } from './parts';
|
|
@@ -44,6 +46,13 @@ function Freshness({ run }: { run: RunSummary }) {
|
|
|
44
46
|
);
|
|
45
47
|
}
|
|
46
48
|
function Counts({ run }: { run: RunSummary }) {
|
|
49
|
+
// A run of no checks (only files) has not passed anything: say so, rather than "0 passed".
|
|
50
|
+
if (run.tests === 0 && Object.values(run.counts).every((count) => count === 0))
|
|
51
|
+
return (
|
|
52
|
+
<Text span inherit c="dimmed" data-status="none">
|
|
53
|
+
No checks recorded
|
|
54
|
+
</Text>
|
|
55
|
+
);
|
|
47
56
|
return (
|
|
48
57
|
<Text span inherit>
|
|
49
58
|
{run.counts.failed > 0 && (
|
|
@@ -88,14 +97,35 @@ function Unavailable({ videos }: { videos: VideoCounts }) {
|
|
|
88
97
|
/** A run's videos in its row: a link to the recordings, how many references they come from, and what cannot be played. */
|
|
89
98
|
function RunVideos({ run }: { run: RunSummary }) {
|
|
90
99
|
const videos = run.videos;
|
|
91
|
-
|
|
100
|
+
const files = run.attachments ? (
|
|
101
|
+
<Anchor
|
|
102
|
+
aria-label={`Open the ${plural(run.attachments, 'file', 'files')} of run ${runName(run.key)}`}
|
|
103
|
+
renderRoot={(props) => (
|
|
104
|
+
<Link {...props} to="/" search={patched({ openRun: run.key, layer: undefined, artifact: undefined, video: undefined })} resetScroll={false} activeOptions={{ exact: true }} />
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
<Text span inherit data-run-files>
|
|
108
|
+
{plural(run.attachments, 'run file', 'run files')}
|
|
109
|
+
</Text>
|
|
110
|
+
</Anchor>
|
|
111
|
+
) : null;
|
|
112
|
+
if (!videos)
|
|
113
|
+
return (
|
|
114
|
+
<Stack gap={0}>
|
|
115
|
+
<Text span inherit c="dimmed" data-video="none">
|
|
116
|
+
No video
|
|
117
|
+
</Text>
|
|
118
|
+
{files}
|
|
119
|
+
</Stack>
|
|
120
|
+
);
|
|
92
121
|
return (
|
|
93
122
|
<Stack gap={0} data-video={videos.recordings > 0 ? 'available' : 'unavailable'}>
|
|
123
|
+
{files}
|
|
94
124
|
{videos.recordings > 0 && (
|
|
95
125
|
<Anchor
|
|
96
126
|
aria-label={`Watch ${plural(videos.recordings, 'recording', 'recordings')} in run ${runName(run.key)}`}
|
|
97
127
|
renderRoot={(props) => (
|
|
98
|
-
<Link {...props} to="/" search={patched({ openRun: run.key, video: true })} resetScroll={false} activeOptions={{ exact: true }} />
|
|
128
|
+
<Link {...props} to="/" search={patched({ openRun: run.key, layer: undefined, artifact: undefined, video: true })} resetScroll={false} activeOptions={{ exact: true }} />
|
|
99
129
|
)}
|
|
100
130
|
>
|
|
101
131
|
<IconVideo size={16} aria-hidden="true" /> {plural(videos.recordings, 'video', 'videos')}
|
|
@@ -204,12 +234,12 @@ function RunTable() {
|
|
|
204
234
|
</Table.Thead>
|
|
205
235
|
<Table.Tbody>
|
|
206
236
|
{list.slice(0, shown).map((run) => (
|
|
207
|
-
<Table.Tr key={run.key} data-status={run.counts.failed ? 'failed' : 'passed'}>
|
|
237
|
+
<Table.Tr key={run.key} data-status={run.counts.failed ? 'failed' : run.tests === 0 ? 'not-run' : 'passed'}>
|
|
208
238
|
<Table.Td title={exactTime(run.endedAtMs)}>
|
|
209
239
|
<Anchor
|
|
210
240
|
aria-label={`Open run ${runLabel(run)}`}
|
|
211
241
|
renderRoot={(props) => (
|
|
212
|
-
<Link {...props} to="/" search={patched({ openRun: run.key, video: undefined })} resetScroll={false} activeOptions={{ exact: true }} />
|
|
242
|
+
<Link {...props} to="/" search={patched({ openRun: run.key, layer: undefined, artifact: undefined, video: undefined })} resetScroll={false} activeOptions={{ exact: true }} />
|
|
213
243
|
)}
|
|
214
244
|
>
|
|
215
245
|
{ago(run.endedAtMs, now)}
|
|
@@ -319,7 +349,7 @@ function RunDetailView({ runKey }: { runKey: string }) {
|
|
|
319
349
|
const back = (
|
|
320
350
|
<Anchor
|
|
321
351
|
renderRoot={(props) => (
|
|
322
|
-
<Link {...props} to="/" search={patched({ openRun: undefined })} resetScroll={false} activeOptions={{ exact: true }} />
|
|
352
|
+
<Link {...props} to="/" search={patched({ openRun: undefined, layer: undefined, artifact: undefined })} resetScroll={false} activeOptions={{ exact: true }} />
|
|
323
353
|
)}
|
|
324
354
|
>
|
|
325
355
|
<Text aria-hidden="true" span inherit>
|
|
@@ -395,7 +425,7 @@ function RunDetailView({ runKey }: { runKey: string }) {
|
|
|
395
425
|
<Meter counts={run.counts} />
|
|
396
426
|
</Text>
|
|
397
427
|
<Text span inherit>
|
|
398
|
-
{plural(run.tests, 'test', 'tests')}
|
|
428
|
+
{run.tests === 0 ? 'No tests' : plural(run.tests, 'test', 'tests')}
|
|
399
429
|
</Text>
|
|
400
430
|
<Counts run={run} />
|
|
401
431
|
</Stack>
|
|
@@ -406,13 +436,14 @@ function RunDetailView({ runKey }: { runKey: string }) {
|
|
|
406
436
|
<Unavailable videos={run.videos} />
|
|
407
437
|
</Text>
|
|
408
438
|
)}
|
|
439
|
+
<RunArtifacts detail={detail.data} />
|
|
409
440
|
<Flex gap="xs" align="center" wrap="wrap">
|
|
410
441
|
<Anchor
|
|
411
442
|
renderRoot={(props) => (
|
|
412
443
|
<Link
|
|
413
444
|
{...props}
|
|
414
445
|
to="/"
|
|
415
|
-
search={patched({ tab: undefined, openRun: undefined, inRun: run.key, test: undefined, layer: undefined })}
|
|
446
|
+
search={patched({ tab: undefined, openRun: undefined, inRun: run.key, test: undefined, layer: undefined, artifact: undefined })}
|
|
416
447
|
resetScroll={false}
|
|
417
448
|
activeOptions={{ exact: true }}
|
|
418
449
|
/>
|
|
@@ -488,7 +519,7 @@ function RunDetailView({ runKey }: { runKey: string }) {
|
|
|
488
519
|
<Link
|
|
489
520
|
{...props}
|
|
490
521
|
to="/"
|
|
491
|
-
search={patched({ tab: undefined, openRun: undefined, test: result.key, layer: result.layer })}
|
|
522
|
+
search={patched({ tab: undefined, openRun: undefined, artifact: undefined, test: result.key, layer: result.layer })}
|
|
492
523
|
resetScroll={false}
|
|
493
524
|
activeOptions={{ exact: true }}
|
|
494
525
|
/>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @module Explorer address
|
|
3
3
|
*
|
|
4
4
|
* Everything the test explorer shows is in the address of the `/` route, so a link reproduces the view: the open test,
|
|
5
|
-
* the words searched, the filters, the grouping, the order, the Runs tab with the run opened in it,
|
|
5
|
+
* the words searched, the filters, the grouping, the order, the Runs tab with the run opened in it, the exact layer a test's detail is pinned to, and the one file of a run's layer its Artifacts select. Filters that
|
|
6
6
|
* take several values are comma-separated (`status=failed,flaky`); a comma or backslash inside a value is written `\,`
|
|
7
7
|
* or `\\`, and the router encodes `#` and spaces. Anything unreadable is dropped, never an error.
|
|
8
8
|
*
|
|
@@ -60,8 +60,10 @@ export const ExplorerSearchFields = {
|
|
|
60
60
|
tab: z.enum(['runs', 'artifacts']).optional().catch(undefined),
|
|
61
61
|
/** The run opened in the Runs tab. */
|
|
62
62
|
openRun: text,
|
|
63
|
-
/** Pins the open test's detail to one exact layer (a run's record of it), instead of the latest combined evidence. */
|
|
63
|
+
/** Pins the open test's detail to one exact layer (a run's record of it), instead of the latest combined evidence. With `openRun` and `artifact` it names the layer the file is in. */
|
|
64
64
|
layer: text,
|
|
65
|
+
/** With `openRun` and `layer`: the one file of that layer (an `artifactRef`: `id:…`, `key:…` or `name:…`) the run's Artifacts select. */
|
|
66
|
+
artifact: text,
|
|
65
67
|
/** Only the runs, and the results of an opened run, that have a playable video recording. */
|
|
66
68
|
video: z.preprocess((value) => (value === 'true' ? true : value), z.literal(true)).optional().catch(undefined)
|
|
67
69
|
};
|
|
@@ -91,6 +93,7 @@ export type ExplorerSearch = {
|
|
|
91
93
|
readonly tab?: 'runs' | 'artifacts';
|
|
92
94
|
readonly openRun?: string;
|
|
93
95
|
readonly layer?: string;
|
|
96
|
+
readonly artifact?: string;
|
|
94
97
|
readonly video?: true;
|
|
95
98
|
};
|
|
96
99
|
|