@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/reports/src/evidence-detail.d.ts +23 -0
  3. package/dist/reports/src/evidence-detail.js +19 -1
  4. package/dist/reports/src/export-evidence.js +1 -1
  5. package/dist/reports/src/layers-route.d.ts +2 -1
  6. package/dist/reports/src/layers-route.js +8 -4
  7. package/dist/reports/src/test-catalog.d.ts +27 -0
  8. package/dist/reports/src/test-catalog.js +8 -2
  9. package/dist/schema/src/api.d.ts +22 -0
  10. package/dist/schema/src/api.js +10 -1
  11. package/dist/server/src/openapi.js +2 -2
  12. package/docs/reference/http-api.md +4 -4
  13. package/docs/reference/reports.md +64 -7
  14. package/docs/rich-evidence.md +23 -0
  15. package/docs/ui.md +22 -2
  16. package/llms-full.txt +49 -6
  17. package/package.json +1 -1
  18. package/packages/schema/schemas/check-detail.schema.json +24 -0
  19. package/packages/schema/schemas/report-run.schema.json +85 -0
  20. package/packages/schema/schemas/report-runs.schema.json +6 -0
  21. package/packages/server/openapi.json +119 -4
  22. package/packages/visualizer/src/evidence/artifacts.tsx +69 -18
  23. package/packages/visualizer/src/evidence/behavior-panel.tsx +291 -0
  24. package/packages/visualizer/src/evidence/behavior.test.ts +99 -0
  25. package/packages/visualizer/src/evidence/behavior.ts +268 -0
  26. package/packages/visualizer/src/evidence/check-detail.tsx +4 -0
  27. package/packages/visualizer/src/evidence/feature-page.tsx +82 -36
  28. package/packages/visualizer/src/evidence/gherkin.ts +31 -1
  29. package/packages/visualizer/src/evidence/spec-index.tsx +48 -15
  30. package/packages/visualizer/src/explorer/Explorer.tsx +9 -5
  31. package/packages/visualizer/src/explorer/RunArtifacts.tsx +246 -0
  32. package/packages/visualizer/src/explorer/RunsView.tsx +40 -9
  33. 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
- if (!videos) return <Text span inherit c="dimmed" data-video="none">No video</Text>;
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, and the exact layer a test's detail is pinned to. Filters that
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