@crvouga/atlas 0.12.0 → 0.14.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/playwright-reporter/src/index.d.ts +11 -0
- package/dist/playwright-reporter/src/index.js +4 -2
- package/dist/reports/src/evidence-types.d.ts +3 -0
- package/dist/reports/src/layer-merge.d.ts +1 -1
- package/dist/reports/src/layer-merge.js +3 -2
- package/dist/reports/src/layer-types.d.ts +3 -0
- package/dist/reports/src/publish-checks.js +1 -0
- package/dist/reports/src/report-cli.d.ts +1 -1
- package/dist/reports/src/report-cli.js +30 -2
- package/dist/reports/src/reporter.d.ts +1 -1
- package/dist/reports/src/test-catalog.d.ts +20 -4
- package/dist/reports/src/test-catalog.js +31 -8
- package/dist/schema/src/api.d.ts +49 -1
- package/dist/schema/src/api.js +7 -3
- package/dist/schema/src/layer.d.ts +26 -0
- package/dist/schema/src/layer.js +17 -0
- package/docs/integrating.md +8 -0
- package/docs/reference/cli.md +7 -0
- package/docs/reference/playwright-reporter.md +6 -6
- package/docs/reference/reports.md +18 -6
- package/docs/reference/schema.md +20 -1
- package/docs/report-contract.md +1 -0
- package/docs/ui.md +1 -1
- package/llms-full.txt +17 -1
- package/package.json +1 -1
- package/packages/schema/schemas/effective-delta.schema.json +30 -0
- package/packages/schema/schemas/effective-evidence.schema.json +30 -0
- package/packages/schema/schemas/evidence-page.schema.json +30 -0
- package/packages/schema/schemas/layer.schema.json +30 -0
- package/packages/schema/schemas/publish-checks.schema.json +30 -0
- package/packages/schema/schemas/report-run.schema.json +17 -1
- package/packages/schema/schemas/report-runs.schema.json +17 -1
- package/packages/server/openapi.json +154 -2
- package/packages/visualizer/src/evidence/check-detail.tsx +10 -4
- package/packages/visualizer/src/evidence/recording-page.tsx +5 -2
- package/packages/visualizer/src/explorer/Explorer.tsx +6 -6
- package/packages/visualizer/src/explorer/RunsView.tsx +25 -6
- package/packages/visualizer/src/explorer/TestDetail.tsx +142 -14
- package/packages/visualizer/src/explorer/parts.tsx +2 -1
- package/packages/visualizer/src/explorer/pinned.ts +91 -0
- package/packages/visualizer/src/explorer/search.ts +5 -2
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Pinned result
|
|
3
|
+
*
|
|
4
|
+
* Opening a test from a run, or choosing an earlier run in its history, pins the detail to one exact layer: the
|
|
5
|
+
* `layer` parameter of the explorer's address. This finds that layer's own record of the open check, wherever the
|
|
6
|
+
* viewer holds it: in the evidence it already read, or, on a server that sends only the winners, from the layer
|
|
7
|
+
* itself. A layer that cannot be found is reported as missing, never replaced by another result.
|
|
8
|
+
*/
|
|
9
|
+
import { checkHistory, type HistoryEntry } from '@crvouga/atlas/reports/evidence-view';
|
|
10
|
+
import type { EvidencePage, LayerHeader, LayerRows } from '@crvouga/atlas/reports/evidence-types';
|
|
11
|
+
import { isDiagnosticKind, slotKey } from '@crvouga/atlas/reports/layer-merge';
|
|
12
|
+
import { useQuery } from '@tanstack/react-query';
|
|
13
|
+
import { useMemo } from 'react';
|
|
14
|
+
|
|
15
|
+
import { reportJson } from '../evidence/api';
|
|
16
|
+
import { useEvidence } from '../evidence/evidence-context';
|
|
17
|
+
|
|
18
|
+
/** What the viewer knows of the pinned result. `none` when nothing is pinned. */
|
|
19
|
+
export type PinnedResult =
|
|
20
|
+
| { readonly state: 'none' }
|
|
21
|
+
| { readonly state: 'loading' }
|
|
22
|
+
| { readonly state: 'missing'; readonly reason: string }
|
|
23
|
+
| { readonly state: 'ready'; readonly entry: HistoryEntry };
|
|
24
|
+
|
|
25
|
+
/** The most layer-header pages read looking for a pinned layer on a server. */
|
|
26
|
+
const MAX_PAGES = 200;
|
|
27
|
+
|
|
28
|
+
/** A test key (`TestSummary.key`) as the spec path and check id it names, or null when it is not one. */
|
|
29
|
+
export function checkOfKey(key: string): { path: string | null; id: string } | null {
|
|
30
|
+
try {
|
|
31
|
+
const parsed: unknown = JSON.parse(key);
|
|
32
|
+
if (Array.isArray(parsed) && typeof parsed[0] === 'string' && typeof parsed[1] === 'string') return { path: parsed[0] || null, id: parsed[1] };
|
|
33
|
+
} catch {
|
|
34
|
+
/* not a check key */
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One layer's record of one check, preferring a verdict over a timing, as `checkHistory` does. */
|
|
40
|
+
function recordOf(layer: LayerHeader, rows: LayerRows, key: string): HistoryEntry | null {
|
|
41
|
+
const own = rows.rows.filter((row) => slotKey(row.specPath ?? '', row.id) === key);
|
|
42
|
+
const row = own.find((candidate) => !isDiagnosticKind(candidate.kind)) ?? own[0];
|
|
43
|
+
return row ? { layer, row } : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readLayer(layerId: string, key: string, signal: AbortSignal): Promise<HistoryEntry> {
|
|
47
|
+
let cursor: string | null = null;
|
|
48
|
+
let header: LayerHeader | undefined;
|
|
49
|
+
for (let page = 0; page < MAX_PAGES && !header; page++) {
|
|
50
|
+
const read: EvidencePage = await reportJson<EvidencePage>(`/evidence${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ''}`, signal);
|
|
51
|
+
header = read.layers.find((layer) => layer.id === layerId);
|
|
52
|
+
cursor = read.cursor;
|
|
53
|
+
if (!cursor) break;
|
|
54
|
+
}
|
|
55
|
+
if (!header) throw new Error('That run is not in the report any more. Its source may be switched off or have expired.');
|
|
56
|
+
const rows = await reportJson<LayerRows>(`/evidence/rows/${encodeURIComponent(header.backend)}/${encodeURIComponent(header.id)}`, signal);
|
|
57
|
+
const entry = recordOf(header, rows, key);
|
|
58
|
+
if (!entry) throw new Error('That run no longer holds a result for this test.');
|
|
59
|
+
return entry;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The record of the check `testKey` made by the layer `layerId`.
|
|
64
|
+
*
|
|
65
|
+
* @param layerId - The pinned layer (the `layer` address parameter), or undefined for the latest combined evidence.
|
|
66
|
+
* @param testKey - The open test's key.
|
|
67
|
+
* @returns `none` without a layer, `loading` while it is being found, `ready` with its entry, or `missing` with why.
|
|
68
|
+
*/
|
|
69
|
+
export function usePinnedResult(layerId: string | undefined, testKey: string): PinnedResult {
|
|
70
|
+
const { evidence, effective, ready, pending, unavailable } = useEvidence();
|
|
71
|
+
const check = useMemo(() => checkOfKey(testKey), [testKey]);
|
|
72
|
+
const local = useMemo(
|
|
73
|
+
() => (layerId && check ? checkHistory(evidence, check.path, check.id).find((entry) => entry.layer.id === layerId) : undefined),
|
|
74
|
+
[evidence, layerId, check]
|
|
75
|
+
);
|
|
76
|
+
const remote = useQuery({
|
|
77
|
+
queryKey: ['pinned-result', layerId ?? '', testKey],
|
|
78
|
+
queryFn: ({ signal }) => readLayer(layerId!, testKey, signal),
|
|
79
|
+
enabled: !!layerId && !local && effective && ready,
|
|
80
|
+
staleTime: 5 * 60_000,
|
|
81
|
+
retry: false
|
|
82
|
+
});
|
|
83
|
+
if (!layerId) return { state: 'none' };
|
|
84
|
+
if (local) return { state: 'ready', entry: local };
|
|
85
|
+
if (unavailable) return { state: 'missing', reason: unavailable };
|
|
86
|
+
if (!ready) return { state: 'loading' };
|
|
87
|
+
if (!effective) return pending > 0 ? { state: 'loading' } : { state: 'missing', reason: 'That run is not in the report any more. Its source may be switched off or have expired.' };
|
|
88
|
+
if (remote.data) return { state: 'ready', entry: remote.data };
|
|
89
|
+
if (remote.isError) return { state: 'missing', reason: remote.error.message };
|
|
90
|
+
return { state: 'loading' };
|
|
91
|
+
}
|
|
@@ -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,
|
|
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
|
|
6
6
|
* take several values are comma-separated (`status=failed,flaky`). Anything unreadable is dropped, never an error.
|
|
7
7
|
*/
|
|
8
8
|
import { TEST_SORTS, type TestQuery, type TestSort } from '@crvouga/atlas/reports/test-catalog';
|
|
@@ -36,7 +36,9 @@ export const ExplorerSearchFields = {
|
|
|
36
36
|
sort: z.enum(TEST_SORTS).optional().catch(undefined),
|
|
37
37
|
tab: z.enum(['runs']).optional().catch(undefined),
|
|
38
38
|
/** The run opened in the Runs tab. */
|
|
39
|
-
openRun: text
|
|
39
|
+
openRun: text,
|
|
40
|
+
/** Pins the open test's detail to one exact layer (a run's record of it), instead of the latest combined evidence. */
|
|
41
|
+
layer: text
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
/** The explorer's search parameters as the route holds them. */
|
|
@@ -54,6 +56,7 @@ export type ExplorerSearch = {
|
|
|
54
56
|
readonly sort?: TestSort;
|
|
55
57
|
readonly tab?: 'runs';
|
|
56
58
|
readonly openRun?: string;
|
|
59
|
+
readonly layer?: string;
|
|
57
60
|
};
|
|
58
61
|
|
|
59
62
|
/** The values of a comma-separated filter, empty when it is not set. */
|