@crvouga/atlas 1.12.0 → 1.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 +13 -1
- package/dist/critics/src/contract.d.ts +113 -0
- package/dist/critics/src/contract.js +48 -0
- package/dist/critics/src/credentials.d.ts +72 -0
- package/dist/critics/src/credentials.js +136 -0
- package/dist/critics/src/critique.d.ts +128 -0
- package/dist/critics/src/critique.js +163 -0
- package/dist/critics/src/index.d.ts +21 -0
- package/dist/critics/src/index.js +21 -0
- package/dist/critics/src/judging.d.ts +153 -0
- package/dist/critics/src/judging.js +145 -0
- package/dist/critics/src/publish.d.ts +23 -0
- package/dist/critics/src/publish.js +21 -0
- package/dist/critics/src/runner.d.ts +104 -0
- package/dist/critics/src/runner.js +202 -0
- package/dist/critics/src/scope.d.ts +8 -0
- package/dist/critics/src/scope.js +26 -0
- package/dist/playwright/src/index.d.ts +4 -0
- package/dist/playwright/src/index.js +4 -0
- package/dist/playwright/src/ui-catalog.d.ts +51 -0
- package/dist/playwright/src/ui-catalog.js +44 -0
- package/dist/reports/src/compose.d.ts +2 -2
- package/dist/reports/src/compose.js +6 -5
- package/dist/reports/src/evidence-types.d.ts +3 -0
- package/dist/reports/src/evidence-view.d.ts +2 -1
- package/dist/reports/src/evidence-view.js +5 -2
- package/dist/reports/src/layer-merge.d.ts +16 -4
- package/dist/reports/src/layer-merge.js +23 -5
- package/dist/reports/src/report-index.js +9 -5
- package/dist/reports/src/report-status.d.ts +1 -1
- package/dist/reports/src/report-status.js +6 -4
- package/dist/reports/src/ui-catalog-publish.d.ts +92 -0
- package/dist/reports/src/ui-catalog-publish.js +68 -0
- package/dist/reports/src/ui-catalog.d.ts +229 -0
- package/dist/reports/src/ui-catalog.js +273 -0
- package/dist/reports/src/ui-state.d.ts +70 -0
- package/dist/reports/src/ui-state.js +66 -0
- package/dist/schema/src/api.d.ts +351 -0
- package/dist/schema/src/api.js +72 -1
- package/dist/schema/src/critique.d.ts +144 -0
- package/dist/schema/src/critique.js +96 -0
- package/dist/schema/src/index.d.ts +2 -1
- package/dist/schema/src/index.js +2 -1
- package/dist/schema/src/server-config.d.ts +7 -1
- package/dist/schema/src/server-config.js +6 -3
- package/dist/server/src/app.js +15 -0
- package/dist/server/src/auth.d.ts +1 -1
- package/dist/server/src/auth.js +2 -2
- package/dist/server/src/config.js +2 -2
- package/dist/server/src/openapi.d.ts +3 -3
- package/dist/server/src/openapi.js +29 -6
- package/dist/server/src/store-api.js +39 -1
- package/docs/agents.md +1 -0
- package/docs/critics.md +190 -0
- package/docs/extending.md +1 -0
- package/docs/reference/critics.md +840 -0
- package/docs/reference/http-api.md +52 -2
- package/docs/reference/playwright.md +63 -0
- package/docs/reference/reports.md +608 -8
- package/docs/reference/schema.md +152 -2
- package/docs/reference/server.md +3 -3
- package/docs/report-contract.md +6 -2
- package/docs/rich-evidence.md +5 -0
- package/docs/server.md +8 -4
- package/docs/ui-catalog.md +173 -0
- package/docs/ui.md +15 -0
- package/llms-full.txt +457 -8
- package/llms.txt +3 -0
- package/package.json +8 -2
- package/packages/critics/README.md +18 -0
- package/packages/schema/schemas/critique.schema.json +254 -0
- package/packages/schema/schemas/effective-delta.schema.json +73 -0
- package/packages/schema/schemas/effective-evidence.schema.json +73 -0
- package/packages/schema/schemas/layer-rows.schema.json +73 -0
- package/packages/schema/schemas/server-config.schema.json +2 -1
- package/packages/schema/schemas/server-info.schema.json +1 -0
- package/packages/server/openapi.json +1443 -80
- package/packages/visualizer/src/app/AppShell.tsx +36 -30
- package/packages/visualizer/src/app/router.tsx +10 -1
- package/packages/visualizer/src/catalog/UiCatalogFilters.tsx +65 -0
- package/packages/visualizer/src/catalog/UiCatalogPage.tsx +342 -0
- package/packages/visualizer/src/catalog/provider.tsx +25 -0
- package/packages/visualizer/src/catalog/search.ts +68 -0
- package/packages/visualizer/src/catalog/words.ts +26 -0
- package/packages/visualizer/src/components/Header.tsx +17 -4
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { REPORT_STATUS_SCHEMA_VERSION } from '../../schema/src/layer.js';
|
|
12
12
|
import { countResults } from './compose.js';
|
|
13
13
|
import { normalizeEnvironment } from './layer-axes.js';
|
|
14
|
-
import { isDiagnosticKind } from './layer-merge.js';
|
|
14
|
+
import { isDiagnosticKind, isOpinionKind } from './layer-merge.js';
|
|
15
15
|
import { recordingCompleteness } from './recording-receipt.js';
|
|
16
16
|
/** Total string order by code unit. */
|
|
17
17
|
const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
@@ -23,7 +23,7 @@ const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
|
23
23
|
* checks (or none completed), failed checks, running checks, specs without results (only with `requireAllSpecs`),
|
|
24
24
|
* each required environment with no checks (the name is normalised, so `iOS` means `ios`), then every notice of the
|
|
25
25
|
* report (an unreadable layer or backend). `failing` lists each failed or running winner, in a fixed order that does
|
|
26
|
-
* not depend on read order; diagnostic `performance` checks are left out unless nothing else was recorded.
|
|
26
|
+
* not depend on read order; diagnostic `performance` checks are left out unless nothing else was recorded, and `critique` opinions are always left out.
|
|
27
27
|
* `recordings` reports recording completeness of the same results apart from `ok`: an incomplete or unknown
|
|
28
28
|
* completeness never changes the gate (use `atlas report publish --require-recordings` to fail on a missing video).
|
|
29
29
|
*
|
|
@@ -34,7 +34,8 @@ const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
|
34
34
|
* @see docs/report-contract.md#atlas-report-status
|
|
35
35
|
*/
|
|
36
36
|
export function reportStatus(report, options = {}) {
|
|
37
|
-
|
|
37
|
+
// A critique is an opinion: a low score or a crashed critic never fails the gate (see `isOpinionKind`).
|
|
38
|
+
const results = [...report.combined.specs.flatMap(entry => entry.results), ...report.combined.unassigned].filter(result => !isOpinionKind(result.row.kind));
|
|
38
39
|
const outcomes = results.some(result => !isDiagnosticKind(result.row.kind)) ? results.filter(result => !isDiagnosticKind(result.row.kind)) : results;
|
|
39
40
|
const failing = outcomes
|
|
40
41
|
.filter(result => result.row.status === 'failed' || result.row.status === 'running')
|
|
@@ -98,7 +99,8 @@ export function reportStatus(report, options = {}) {
|
|
|
98
99
|
}
|
|
99
100
|
/** `spec › check`, unless the check's id already starts with its spec. */
|
|
100
101
|
const label = (item) => (item.spec && !item.check.startsWith(item.spec) ? `${item.spec} › ${item.check}` : item.check);
|
|
101
|
-
function specOutcome(path, title, kind,
|
|
102
|
+
function specOutcome(path, title, kind, all) {
|
|
103
|
+
const results = all.filter(result => !isOpinionKind(result.row.kind));
|
|
102
104
|
const environments = new Map();
|
|
103
105
|
for (const result of results) {
|
|
104
106
|
const key = result.layer.environment ?? 'unspecified';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Writing a UI catalog from any collector
|
|
3
|
+
*
|
|
4
|
+
* Two small builders that turn what a collector captured, or what a planner expects, into the input of `publishChecks`
|
|
5
|
+
* (and of `atlas report publish --checks`), following the convention in {@link ./ui-state}. They are plain data in and plain
|
|
6
|
+
* data out: no browser, no test framework, no file system. A Playwright test, a Cypress run, a mobile device lab, a Storybook
|
|
7
|
+
* crawler or a shell script that writes the same JSON all produce the same catalog.
|
|
8
|
+
*
|
|
9
|
+
* Shards need no coordination: each shard calls {@link uiStateLayer} for the states it captured (and optionally
|
|
10
|
+
* {@link uiPlanLayer} for the states it was given), with its own `shard` label, and publishes. A planner that knows the whole
|
|
11
|
+
* product publishes the plan once.
|
|
12
|
+
*
|
|
13
|
+
* @see docs/ui-catalog.md
|
|
14
|
+
*/
|
|
15
|
+
import type { PublishChecksInput } from '../../schema/src/layer.js';
|
|
16
|
+
import { type UiStateFacts } from './ui-state.js';
|
|
17
|
+
/** A picture to store with a capture: its bytes (`data`, `base64`), or where it already is (`uri`). */
|
|
18
|
+
export type UiPicture = {
|
|
19
|
+
/** The file name; default `<id>.png`. */
|
|
20
|
+
readonly name?: string;
|
|
21
|
+
readonly data?: Uint8Array;
|
|
22
|
+
readonly base64?: string;
|
|
23
|
+
readonly uri?: string;
|
|
24
|
+
/** Default `image/png`. */
|
|
25
|
+
readonly contentType?: string;
|
|
26
|
+
readonly width?: number;
|
|
27
|
+
readonly height?: number;
|
|
28
|
+
};
|
|
29
|
+
/** One captured state. `id` must be stable across runs: it is how a new capture replaces an old one. */
|
|
30
|
+
export type UiStateCapture = UiStateFacts & {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
/** For a person; default the id. */
|
|
33
|
+
readonly title?: string;
|
|
34
|
+
/** `passed` (default): captured. `failed`: the capture was attempted and did not work (say why in `detail`). `running`: being taken. `not-run`: skipped. */
|
|
35
|
+
readonly status?: 'passed' | 'failed' | 'running' | 'not-run';
|
|
36
|
+
/** Why it failed or was skipped: a readiness check that never became true, a console error. */
|
|
37
|
+
readonly detail?: string;
|
|
38
|
+
readonly screenshot?: UiPicture;
|
|
39
|
+
readonly durationMs?: number;
|
|
40
|
+
/** More facts about the capture that never split it (`browser`, `os`). */
|
|
41
|
+
readonly labels?: Readonly<Record<string, string>>;
|
|
42
|
+
readonly tags?: readonly string[];
|
|
43
|
+
};
|
|
44
|
+
/** One expected state, or (with `inventory`) one surface of the product's inventory. */
|
|
45
|
+
export type UiStatePlan = UiStateFacts & {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly title?: string;
|
|
48
|
+
};
|
|
49
|
+
/** What every layer of a catalog says about where and when it was recorded. */
|
|
50
|
+
export type UiLayerOrigin = {
|
|
51
|
+
/** The suite or tool that makes the layer. Part of the slot; a planner and a collector normally differ. */
|
|
52
|
+
readonly producer: string;
|
|
53
|
+
/** When the captures were taken (or the plan was made), epoch milliseconds. Take it from the run, not from the moment of publishing. */
|
|
54
|
+
readonly recordedAtMs: number;
|
|
55
|
+
/** The client: `desktop-web`, `mobile-web`, `ios`, `android`… Part of the slot. */
|
|
56
|
+
readonly environment?: string;
|
|
57
|
+
/** The rest of the profile, such as `{ viewport: '390x844', scheme: 'dark' }`. Each combination is its own result. Part of the slot. */
|
|
58
|
+
readonly profile?: Readonly<Record<string, string>>;
|
|
59
|
+
readonly sha?: string | null;
|
|
60
|
+
readonly branch?: string | null;
|
|
61
|
+
readonly runUrl?: string | null;
|
|
62
|
+
/** Which shard this is (`2/4`, `checkout`, a machine name). Shown with each capture; never part of the slot. */
|
|
63
|
+
readonly shard?: string;
|
|
64
|
+
/** The execution these layers belong to, so shards of one run stay one run. */
|
|
65
|
+
readonly run?: {
|
|
66
|
+
readonly id: string;
|
|
67
|
+
readonly title?: string;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* The input of `publishChecks` for the states one collector (or one shard) captured under one profile. Throws when a state id
|
|
72
|
+
* repeats, because the second would silently replace the first. A state without a `status` is `passed`: it was captured.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* await publishChecks(store, uiStateLayer({
|
|
77
|
+
* producer: 'web-capture', recordedAtMs: Date.now(), environment: 'mobile-web', profile: { viewport: '390x844', scheme: 'dark' }, shard: '1/4',
|
|
78
|
+
* states: [{ id: 'checkout/payment', title: 'Payment', route: '/checkout', covers: ['payment-form'], screenshot: { data: png } }]
|
|
79
|
+
* }));
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function uiStateLayer(input: UiLayerOrigin & {
|
|
83
|
+
readonly states: readonly UiStateCapture[];
|
|
84
|
+
}): PublishChecksInput;
|
|
85
|
+
/**
|
|
86
|
+
* The input of `publishChecks` for what is expected: states that should be captured under this profile and, with `inventory`,
|
|
87
|
+
* surfaces the product is known to have. Every entry is `not-run`, so a plan never counts as a pass, and it fills a slot of
|
|
88
|
+
* its own (kind `ui-plan`), so a capture never erases it and it never hides a capture. Throws when an id repeats.
|
|
89
|
+
*/
|
|
90
|
+
export declare function uiPlanLayer(input: UiLayerOrigin & {
|
|
91
|
+
readonly states: readonly UiStatePlan[];
|
|
92
|
+
}): PublishChecksInput;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { UI_PLAN_KIND, UI_STATE_EXTENSION, UI_STATE_KIND } from './ui-state.js';
|
|
2
|
+
function common(origin, kind, title) {
|
|
3
|
+
return {
|
|
4
|
+
producer: origin.producer, kind, title, recordedAtMs: origin.recordedAtMs,
|
|
5
|
+
...(origin.environment ? { environment: origin.environment } : {}),
|
|
6
|
+
parameters: { ...origin.profile },
|
|
7
|
+
...(origin.sha !== undefined ? { sha: origin.sha } : {}), ...(origin.branch !== undefined ? { branch: origin.branch } : {}), ...(origin.runUrl !== undefined ? { runUrl: origin.runUrl } : {}),
|
|
8
|
+
...(origin.run ? { run: origin.run } : {})
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function facts(source) {
|
|
12
|
+
const { route, fixture, cohort, area, flags, covers, inventory } = source;
|
|
13
|
+
return { ...(route ? { route } : {}), ...(fixture ? { fixture } : {}), ...(cohort ? { cohort } : {}), ...(area ? { area } : {}), ...(flags?.length ? { flags } : {}), ...(covers?.length ? { covers } : {}), ...(inventory ? { inventory } : {}) };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The input of `publishChecks` for the states one collector (or one shard) captured under one profile. Throws when a state id
|
|
17
|
+
* repeats, because the second would silently replace the first. A state without a `status` is `passed`: it was captured.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* await publishChecks(store, uiStateLayer({
|
|
22
|
+
* producer: 'web-capture', recordedAtMs: Date.now(), environment: 'mobile-web', profile: { viewport: '390x844', scheme: 'dark' }, shard: '1/4',
|
|
23
|
+
* states: [{ id: 'checkout/payment', title: 'Payment', route: '/checkout', covers: ['payment-form'], screenshot: { data: png } }]
|
|
24
|
+
* }));
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function uiStateLayer(input) {
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
const checks = input.states.map(state => {
|
|
30
|
+
if (seen.has(state.id))
|
|
31
|
+
throw new Error(`uiStateLayer: the state "${state.id}" appears twice; give each state of one profile its own id`);
|
|
32
|
+
seen.add(state.id);
|
|
33
|
+
const shot = state.screenshot;
|
|
34
|
+
const attachments = shot ? [{
|
|
35
|
+
name: shot.name ?? `${state.id.replace(/[^\w.-]+/g, '-')}.png`, role: 'screenshot', contentType: shot.contentType ?? 'image/png',
|
|
36
|
+
...(shot.data ? { data: shot.data } : {}), ...(shot.base64 ? { base64: shot.base64 } : {}), ...(shot.uri ? { uri: shot.uri } : {}),
|
|
37
|
+
...(shot.width !== undefined ? { width: shot.width } : {}), ...(shot.height !== undefined ? { height: shot.height } : {})
|
|
38
|
+
}] : undefined;
|
|
39
|
+
const labels = { ...state.labels, ...(input.shard ? { shard: input.shard } : {}) };
|
|
40
|
+
return {
|
|
41
|
+
id: state.id, title: state.title ?? state.id, status: state.status ?? 'passed', detail: state.detail ?? '',
|
|
42
|
+
...(state.durationMs !== undefined ? { durationMs: state.durationMs } : {}),
|
|
43
|
+
...(attachments ? { attachments } : {}),
|
|
44
|
+
...(Object.keys(labels).length ? { labels } : {}), ...(state.tags?.length ? { tags: [...state.tags] } : {}),
|
|
45
|
+
extensions: { [UI_STATE_EXTENSION]: facts(state) }
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
return { ...common(input, UI_STATE_KIND, 'UI states'), checks };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The input of `publishChecks` for what is expected: states that should be captured under this profile and, with `inventory`,
|
|
52
|
+
* surfaces the product is known to have. Every entry is `not-run`, so a plan never counts as a pass, and it fills a slot of
|
|
53
|
+
* its own (kind `ui-plan`), so a capture never erases it and it never hides a capture. Throws when an id repeats.
|
|
54
|
+
*/
|
|
55
|
+
export function uiPlanLayer(input) {
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const checks = input.states.map(state => {
|
|
58
|
+
if (seen.has(state.id))
|
|
59
|
+
throw new Error(`uiPlanLayer: "${state.id}" appears twice`);
|
|
60
|
+
seen.add(state.id);
|
|
61
|
+
return {
|
|
62
|
+
id: state.id, title: state.title ?? state.id, status: 'not-run', detail: state.inventory ? 'In the product inventory' : 'Expected, not captured yet',
|
|
63
|
+
...(input.shard ? { labels: { shard: input.shard } } : {}),
|
|
64
|
+
extensions: { [UI_STATE_EXTENSION]: facts(state) }
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
return { ...common(input, UI_PLAN_KIND, 'UI states expected'), checks };
|
|
68
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module UI catalog
|
|
3
|
+
*
|
|
4
|
+
* A browsable inventory of a product's user interface, computed from layers and nothing else: each **state** (a screen, or
|
|
5
|
+
* one state of a screen) captured once per **profile** (viewport, color scheme, device), with the facts needed to find and
|
|
6
|
+
* trust it, and what should exist but has not been captured yet. `uiCatalog` is a pure function of the winning results of
|
|
7
|
+
* the layers it is given, so it inherits the report contract's laws: the same layers in any order, split any way across
|
|
8
|
+
* backends, shards and machines, give the same catalog; a layer changes only the cells it fills; an unreadable layer is
|
|
9
|
+
* absent, never captured. It imports no collection framework and knows nothing about how a picture was taken.
|
|
10
|
+
*
|
|
11
|
+
* How the convention maps onto the layer format is in {@link ./ui-state}: a state is a check of a `ui-state` part, a profile
|
|
12
|
+
* is the layer's `environment` and `parameters`, the expected set is a `ui-plan` part. The viewer computes the catalog from
|
|
13
|
+
* the report it already streams (so it fills in as results arrive), and the server answers the same document at
|
|
14
|
+
* `GET /api/reports/ui-catalog`.
|
|
15
|
+
*
|
|
16
|
+
* @see docs/ui-catalog.md
|
|
17
|
+
* @see docs/report-contract.md#how-layers-compose
|
|
18
|
+
*/
|
|
19
|
+
import type { BackendInfo, LayerEvidence, LayerHeader } from './evidence-types.js';
|
|
20
|
+
import type { EvidenceStatus } from './explorer-types.js';
|
|
21
|
+
import { type UiScreenshot } from './ui-state.js';
|
|
22
|
+
/** The `schemaVersion` of the catalog document. */
|
|
23
|
+
export declare const UI_CATALOG_VERSION = 1;
|
|
24
|
+
/** One profile: the client and the choices a state was captured under. Its `key` identifies it across layers. */
|
|
25
|
+
export type UiProfile = {
|
|
26
|
+
/** Stable identity: the environment and the sorted parameters. */
|
|
27
|
+
readonly key: string;
|
|
28
|
+
/** The client (`desktop-web`, `ios`…), or null when the layer named none. */
|
|
29
|
+
readonly environment: string | null;
|
|
30
|
+
/** The layer's other choices, such as `{ viewport: '390x844', scheme: 'dark' }`. */
|
|
31
|
+
readonly parameters: Readonly<Record<string, string>>;
|
|
32
|
+
/** For a person: the client and each parameter's value. */
|
|
33
|
+
readonly label: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* How one state stands in one profile. `captured`: a capture passed. `failed`: it was attempted and failed (its `detail`
|
|
37
|
+
* says why). `capturing`: it is being taken now. `skipped`: a producer recorded it as not run. `missing`: expected, and
|
|
38
|
+
* nothing has been recorded. Only `captured` is ever shown as captured; every other value means there is no picture to trust.
|
|
39
|
+
*/
|
|
40
|
+
export type UiCoverage = 'captured' | 'failed' | 'capturing' | 'skipped' | 'missing';
|
|
41
|
+
/** The values of {@link UiCoverage}, in the order a person reads them. */
|
|
42
|
+
export declare const UI_COVERAGE: readonly ["captured", "failed", "capturing", "skipped", "missing"];
|
|
43
|
+
/** A picture, with the address a browser loads it from. */
|
|
44
|
+
export type UiImage = UiScreenshot & {
|
|
45
|
+
/** Where the stored bytes are, relative to the reports API root (`/layers/<layer>/blobs/attachments/<sha256>`). Absent for an address-only or omitted picture. */
|
|
46
|
+
readonly path?: string;
|
|
47
|
+
};
|
|
48
|
+
/** Where a capture came from: the layer, the backend that holds it, and the provenance the layer recorded. */
|
|
49
|
+
export type UiOrigin = {
|
|
50
|
+
readonly layer: string;
|
|
51
|
+
readonly backend: string;
|
|
52
|
+
readonly producer: string;
|
|
53
|
+
readonly recordedAtMs: number;
|
|
54
|
+
readonly sha: string | null;
|
|
55
|
+
readonly branch: string | null;
|
|
56
|
+
readonly runUrl: string | null;
|
|
57
|
+
readonly freshness?: 'unknown' | 'modified';
|
|
58
|
+
};
|
|
59
|
+
/** The winning capture of one cell. */
|
|
60
|
+
export type UiCapture = {
|
|
61
|
+
readonly status: EvidenceStatus;
|
|
62
|
+
/** Why it failed or was skipped; empty otherwise. */
|
|
63
|
+
readonly detail: string;
|
|
64
|
+
readonly durationMs: number | null;
|
|
65
|
+
/** The picture, or null when the capture has none (or it was left out: see `omitted`). */
|
|
66
|
+
readonly image: UiImage | null;
|
|
67
|
+
/** The check's labels: what describes the capture without splitting it (a `shard`, a `browser`). */
|
|
68
|
+
readonly labels: Readonly<Record<string, string>>;
|
|
69
|
+
readonly origin: UiOrigin;
|
|
70
|
+
/** How many other captures of this cell exist from other producers. The winner is the one the report contract picks. */
|
|
71
|
+
readonly alternatives: number;
|
|
72
|
+
};
|
|
73
|
+
/** One state in one profile. */
|
|
74
|
+
export type UiCell = {
|
|
75
|
+
readonly profile: UiProfile;
|
|
76
|
+
readonly coverage: UiCoverage;
|
|
77
|
+
/** Whether a plan expects this state in this profile: `declared`; `undeclared` when plans exist and none names it; `unknown` when no plan was published at all. */
|
|
78
|
+
readonly expected: 'declared' | 'undeclared' | 'unknown';
|
|
79
|
+
readonly capture: UiCapture | null;
|
|
80
|
+
/** The layer of the plan that expects it, when one does. */
|
|
81
|
+
readonly planned: {
|
|
82
|
+
readonly layer: string;
|
|
83
|
+
readonly backend: string;
|
|
84
|
+
} | null;
|
|
85
|
+
};
|
|
86
|
+
/** One state of the product's interface, across its profiles. */
|
|
87
|
+
export type UiCatalogState = {
|
|
88
|
+
/** The check id the producers gave it. */
|
|
89
|
+
readonly id: string;
|
|
90
|
+
readonly title: string;
|
|
91
|
+
readonly route: string | null;
|
|
92
|
+
readonly fixture: string | null;
|
|
93
|
+
readonly cohort: string | null;
|
|
94
|
+
readonly area: string | null;
|
|
95
|
+
readonly flags: readonly string[];
|
|
96
|
+
/** The surfaces it covers. */
|
|
97
|
+
readonly covers: readonly string[];
|
|
98
|
+
/** The producers that captured or planned it. */
|
|
99
|
+
readonly producers: readonly string[];
|
|
100
|
+
/** One cell per profile, ordered by the profile's label. */
|
|
101
|
+
readonly cells: readonly UiCell[];
|
|
102
|
+
};
|
|
103
|
+
/** One surface of the product: an inventory entry, or something a state says it covers. */
|
|
104
|
+
export type UiSurface = {
|
|
105
|
+
readonly id: string;
|
|
106
|
+
/** True when a `ui-plan` inventory entry names it: the product is known to have it. */
|
|
107
|
+
readonly inventoried: boolean;
|
|
108
|
+
/** States that cover it. */
|
|
109
|
+
readonly states: number;
|
|
110
|
+
/** Of those, states with at least one captured profile. */
|
|
111
|
+
readonly captured: number;
|
|
112
|
+
/** `covered`: some covering state is captured. `uncaptured`: states cover it and none is captured yet. `uncovered`: no state covers it. */
|
|
113
|
+
readonly status: 'covered' | 'uncaptured' | 'uncovered';
|
|
114
|
+
};
|
|
115
|
+
/** A value of a facet and how many states have it. */
|
|
116
|
+
export type UiFacetValue = {
|
|
117
|
+
readonly value: string;
|
|
118
|
+
readonly count: number;
|
|
119
|
+
readonly label?: string;
|
|
120
|
+
};
|
|
121
|
+
/** The values each filter offers, over every state of the catalog. */
|
|
122
|
+
export type UiCatalogFacets = {
|
|
123
|
+
readonly profiles: readonly UiFacetValue[];
|
|
124
|
+
readonly coverage: readonly UiFacetValue[];
|
|
125
|
+
readonly areas: readonly UiFacetValue[];
|
|
126
|
+
readonly routes: readonly UiFacetValue[];
|
|
127
|
+
readonly cohorts: readonly UiFacetValue[];
|
|
128
|
+
readonly flags: readonly UiFacetValue[];
|
|
129
|
+
readonly surfaces: readonly UiFacetValue[];
|
|
130
|
+
readonly producers: readonly UiFacetValue[];
|
|
131
|
+
};
|
|
132
|
+
/** The catalog in numbers. Cells count a state once per profile. */
|
|
133
|
+
export type UiCatalogSummary = {
|
|
134
|
+
readonly states: number;
|
|
135
|
+
readonly profiles: number;
|
|
136
|
+
readonly cells: number;
|
|
137
|
+
readonly coverage: Readonly<Record<UiCoverage, number>>;
|
|
138
|
+
/** Captured or failed cells no plan expects. Zero when no plan was published, because then nothing is known to be unexpected. */
|
|
139
|
+
readonly undeclared: number;
|
|
140
|
+
/** Whether any plan (an expected set) was published. Without one, `missing` can only ever be zero. */
|
|
141
|
+
readonly planned: boolean;
|
|
142
|
+
readonly surfaces: number;
|
|
143
|
+
readonly uncoveredSurfaces: number;
|
|
144
|
+
};
|
|
145
|
+
/** The catalog, as `GET /api/reports/ui-catalog` answers it (`version` is the report's, when the caller knows it). */
|
|
146
|
+
export type UiCatalog = {
|
|
147
|
+
readonly schemaVersion: typeof UI_CATALOG_VERSION;
|
|
148
|
+
readonly version?: string;
|
|
149
|
+
readonly summary: UiCatalogSummary;
|
|
150
|
+
readonly profiles: readonly UiProfile[];
|
|
151
|
+
readonly states: readonly UiCatalogState[];
|
|
152
|
+
readonly surfaces: readonly UiSurface[];
|
|
153
|
+
readonly facets: UiCatalogFacets;
|
|
154
|
+
/** What could not be read. A state that depended on it is not in the catalog as captured. Filled by the caller who read the backends. */
|
|
155
|
+
readonly sources: {
|
|
156
|
+
readonly layers: number;
|
|
157
|
+
readonly backends: readonly BackendInfo[];
|
|
158
|
+
readonly notices: readonly string[];
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
/** What a caller may say about how the layers were read. */
|
|
162
|
+
export type UiCatalogOptions = {
|
|
163
|
+
/** The report's version, repeated in the document. */
|
|
164
|
+
readonly version?: string;
|
|
165
|
+
/** The backends, with whether each could be read. */
|
|
166
|
+
readonly backends?: readonly BackendInfo[];
|
|
167
|
+
/** What could not be read. */
|
|
168
|
+
readonly notices?: readonly string[];
|
|
169
|
+
};
|
|
170
|
+
/** The profile of a layer: its environment and its other parameters. */
|
|
171
|
+
export declare function uiProfile(layer: Pick<LayerHeader, 'environment' | 'parameters'>): UiProfile;
|
|
172
|
+
/**
|
|
173
|
+
* The UI catalog of some layers (as the effective report holds them, or any set of {@link LayerEvidence}).
|
|
174
|
+
*
|
|
175
|
+
* Only rows of `ui-state` and `ui-plan` parts take part. The winner of every slot is decided by the report contract
|
|
176
|
+
* (`combineEvidence`), so the result does not depend on the order of `layers` or on how they are divided among backends. A
|
|
177
|
+
* state is the set of cells that share a check `id`; a cell is one profile of it. A cell is `captured` only when the winning
|
|
178
|
+
* capture passed; a plan that nothing has captured is `missing`. When no plan exists nothing is `missing` and nothing is
|
|
179
|
+
* `undeclared`: what is expected is then unknown, and the catalog says so (`summary.planned`).
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```ts
|
|
183
|
+
* const catalog = uiCatalog(effective.layers, { version: effective.version, backends: effective.backends });
|
|
184
|
+
* catalog.states.map(state => [state.id, state.cells.map(cell => cell.coverage)]);
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export declare function uiCatalog(layers: readonly LayerEvidence[], options?: UiCatalogOptions): UiCatalog;
|
|
188
|
+
/** What a person narrows the catalog by. Every field is optional; nothing asked is everything. Values within a field are alternatives, fields combine with AND. */
|
|
189
|
+
export type UiCatalogQuery = {
|
|
190
|
+
/** Words (all of them) found in the id, title, route, fixture, cohort, area, flags, surfaces or producers. */
|
|
191
|
+
readonly q?: string;
|
|
192
|
+
readonly coverage?: readonly UiCoverage[];
|
|
193
|
+
/** Profile keys, as {@link UiProfile.key}. */
|
|
194
|
+
readonly profile?: readonly string[];
|
|
195
|
+
readonly area?: readonly string[];
|
|
196
|
+
readonly route?: readonly string[];
|
|
197
|
+
readonly cohort?: readonly string[];
|
|
198
|
+
readonly flag?: readonly string[];
|
|
199
|
+
/** Surfaces a state covers. */
|
|
200
|
+
readonly surface?: readonly string[];
|
|
201
|
+
readonly producer?: readonly string[];
|
|
202
|
+
};
|
|
203
|
+
/** Reads a query from URL search parameters (`q`, and the comma-separated {@link UiCatalogQuery} lists). Unknown coverage values are left out, never an error. */
|
|
204
|
+
export declare function parseUiCatalogQuery(params: URLSearchParams): UiCatalogQuery;
|
|
205
|
+
/** A query as URL search parameters, the form {@link parseUiCatalogQuery} reads back. */
|
|
206
|
+
export declare function uiCatalogParams(query: UiCatalogQuery): URLSearchParams;
|
|
207
|
+
/** Whether a query narrows anything. */
|
|
208
|
+
export declare function isUiCatalogFiltered(query: UiCatalogQuery): boolean;
|
|
209
|
+
/**
|
|
210
|
+
* The part of a catalog a query asks for. A state is kept when it matches the words and the fact filters; its cells are
|
|
211
|
+
* kept when they match the `profile` and `coverage` filters, and a state left with no cell is dropped. The summary is
|
|
212
|
+
* recomputed for what is kept; the profiles, surfaces, facets and sources stay those of the whole catalog, so the
|
|
213
|
+
* controls keep offering every choice.
|
|
214
|
+
*/
|
|
215
|
+
export declare function filterUiCatalog(catalog: UiCatalog, query: UiCatalogQuery): UiCatalog;
|
|
216
|
+
/** A page of a catalog's states, for a document that would otherwise be large. The summary, facets and sources are the whole catalog's. */
|
|
217
|
+
export declare function pageUiCatalog(catalog: UiCatalog, page: {
|
|
218
|
+
readonly limit?: number;
|
|
219
|
+
readonly offset?: number;
|
|
220
|
+
}): UiCatalog & {
|
|
221
|
+
readonly total: number;
|
|
222
|
+
readonly offset: number;
|
|
223
|
+
readonly limit: number;
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* The catalog as Markdown for a person or an agent reading text: the summary in words, the surfaces that are not covered, and
|
|
227
|
+
* one line per state with its coverage in each profile. Only `captured` is called captured.
|
|
228
|
+
*/
|
|
229
|
+
export declare function uiCatalogMarkdown(catalog: UiCatalog): string;
|