@crvouga/atlas 1.13.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 +7 -1
- 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/evidence-types.d.ts +3 -0
- package/dist/reports/src/layer-merge.d.ts +5 -1
- package/dist/reports/src/layer-merge.js +9 -2
- package/dist/reports/src/report-index.js +9 -5
- 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 +350 -0
- package/dist/schema/src/api.js +71 -0
- package/dist/server/src/app.js +15 -0
- package/dist/server/src/openapi.js +22 -2
- package/docs/reference/http-api.md +46 -0
- package/docs/reference/playwright.md +63 -0
- package/docs/reference/reports.md +588 -1
- package/docs/reference/schema.md +33 -0
- package/docs/report-contract.md +2 -2
- package/docs/rich-evidence.md +5 -0
- package/docs/server.md +2 -0
- package/docs/ui-catalog.md +173 -0
- package/docs/ui.md +15 -0
- package/llms-full.txt +246 -2
- package/llms.txt +1 -0
- package/package.json +2 -1
- 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/server/openapi.json +1441 -79
- 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
|
@@ -104,7 +104,10 @@ recognise screens and tap or type (`screen`, `recognize`, `locate`, the signal c
|
|
|
104
104
|
the media tools (`captureScreen`, `ScreencastRecorder`, `ffmpeg`, `PHONE` and the pacing presets). Needs the
|
|
105
105
|
`playwright` package; clips and thumbnails need FFmpeg on the PATH.
|
|
106
106
|
|
|
107
|
+
Also `captureUiState`, the optional Playwright adapter of the UI catalog (it needs no Playwright import itself).
|
|
108
|
+
|
|
107
109
|
See docs/statecharts.md
|
|
110
|
+
See docs/ui-catalog.md
|
|
108
111
|
|
|
109
112
|
### Playwright screenshots, clips and paced actions
|
|
110
113
|
|
|
@@ -301,3 +304,63 @@ const states = {
|
|
|
301
304
|
'No todos': screen({ all: [text('Nothing to do yet')], none: [role('listitem')] })
|
|
302
305
|
};
|
|
303
306
|
```
|
|
307
|
+
|
|
308
|
+
### Playwright adapter for the UI catalog
|
|
309
|
+
|
|
310
|
+
Source: `packages/playwright/src/ui-catalog.ts`
|
|
311
|
+
|
|
312
|
+
The optional Playwright side of the UI catalog: take a page to a state, wait until it is ready, photograph it, and hand the
|
|
313
|
+
result to `uiStateLayer` (`@crvouga/atlas/reports/ui-catalog-publish`) as a capture. This is the only Playwright-specific code
|
|
314
|
+
of the feature; reading, composing and exploring the catalog know nothing about it, and any other program that writes the
|
|
315
|
+
same layer produces the same catalog. The page is typed by the three methods used, so no `playwright` import is needed here
|
|
316
|
+
and a Playwright Test `page`, a `Page` of the driver, or a stub all fit.
|
|
317
|
+
|
|
318
|
+
See docs/ui-catalog.md
|
|
319
|
+
|
|
320
|
+
#### `CapturePage`
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
export type CapturePage = {
|
|
324
|
+
screenshot(options?: { readonly fullPage?: boolean; readonly animations?: 'disabled' | 'allow'; readonly type?: 'png' }): Promise<Uint8Array>;
|
|
325
|
+
viewportSize(): { readonly width: number; readonly height: number } | null;
|
|
326
|
+
};
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
The part of a Playwright `Page` that capturing uses.
|
|
330
|
+
|
|
331
|
+
#### `CaptureUiStateOptions`
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
export type CaptureUiStateOptions = UiStateFacts & {
|
|
335
|
+
/** The state's stable id: how a later capture replaces this one. */
|
|
336
|
+
readonly id: string;
|
|
337
|
+
readonly title?: string;
|
|
338
|
+
/** Resolves when the page shows the state (a locator is visible, the network is idle). A rejection or a timeout makes the capture `failed`, with the reason as its detail. */
|
|
339
|
+
readonly ready?: (page: CapturePage) => Promise<void>;
|
|
340
|
+
/** How long to wait for `ready`; default 15 seconds. */
|
|
341
|
+
readonly readyTimeoutMs?: number;
|
|
342
|
+
/** Photograph the whole scrollable page rather than the viewport. */
|
|
343
|
+
readonly fullPage?: boolean;
|
|
344
|
+
/** Extra labels for the capture (`browser`, `os`), which never split it. */
|
|
345
|
+
readonly labels?: Readonly<Record<string, string>>;
|
|
346
|
+
};
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
What to capture and how to know the page is ready.
|
|
350
|
+
|
|
351
|
+
#### `captureUiState`
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
export async function captureUiState(page: CapturePage, options: CaptureUiStateOptions): Promise<UiStateCapture>
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Wait for the page to be ready, photograph it, and return the capture to publish. Never throws for the page's sake: a page
|
|
358
|
+
that never became ready, or could not be photographed, is a `failed` capture that says why, so a broken state shows in the
|
|
359
|
+
catalog instead of ending the run. A readiness failure still attaches a picture of what the page showed, for diagnosis.
|
|
360
|
+
|
|
361
|
+
**Example**
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
const shot = await captureUiState(page, { id: 'checkout/payment', route: '/checkout', ready: p => expectVisible(p, '#total') });
|
|
365
|
+
await publishChecks(store, uiStateLayer({ producer: 'playwright', recordedAtMs: Date.now(), environment: 'desktop-web', profile: { viewport: '1280x800' }, states: [shot] }));
|
|
366
|
+
```
|
|
@@ -2252,6 +2252,8 @@ export type LayerRow = {
|
|
|
2252
2252
|
readonly durationMs: number | null;
|
|
2253
2253
|
/** What this result carries beyond its outcome. Absent when nothing. */
|
|
2254
2254
|
readonly evidence?: EvidenceSummary;
|
|
2255
|
+
/** For a check of a `ui-state` or `ui-plan` part: its picture and declared facts, so a catalog is listed without another request (see `ui-state.ts`). Absent for every other kind. */
|
|
2256
|
+
readonly state?: UiStateFact;
|
|
2255
2257
|
/** What became of this check's expected recording: `uploaded`, `referenced`, `skipped`, `unresolved` or `not-applicable`, with a reason when it is not uploaded. Absent when the check declared no expectation. Independent of `status`. */
|
|
2256
2258
|
readonly recording?: { readonly state: string; readonly reason?: string };
|
|
2257
2259
|
};
|
|
@@ -4581,7 +4583,9 @@ Serialises parameters as JSON key/value pairs sorted by key, so equal parameter
|
|
|
4581
4583
|
export function isDiagnosticKind(kind: string): boolean
|
|
4582
4584
|
```
|
|
4583
4585
|
|
|
4584
|
-
Whether a part `kind` only qualifies an outcome
|
|
4586
|
+
Whether a part `kind` only qualifies an outcome and so is not counted as a check when anything else is recorded: a
|
|
4587
|
+
`performance` timing, or a `ui-plan` entry (what a UI catalog expects, which a capture of the same state replaces in the
|
|
4588
|
+
tests' view). It never decides a slot: both still fill their own.
|
|
4585
4589
|
|
|
4586
4590
|
#### `isOpinionKind`
|
|
4587
4591
|
|
|
@@ -9982,6 +9986,589 @@ export type ReportData = HtmlReportBundle & {
|
|
|
9982
9986
|
|
|
9983
9987
|
A run's tests (`HtmlReportBundle`) plus the document title shown for them, for the standalone / Solid report viewer.
|
|
9984
9988
|
|
|
9989
|
+
### Writing a UI catalog from any collector
|
|
9990
|
+
|
|
9991
|
+
Source: `packages/reports/src/ui-catalog-publish.ts`. Import: `@crvouga/atlas/reports/ui-catalog-publish`
|
|
9992
|
+
|
|
9993
|
+
Two small builders that turn what a collector captured, or what a planner expects, into the input of `publishChecks`
|
|
9994
|
+
(and of `atlas report publish --checks`), following the convention in {@link ./ui-state}. They are plain data in and plain
|
|
9995
|
+
data out: no browser, no test framework, no file system. A Playwright test, a Cypress run, a mobile device lab, a Storybook
|
|
9996
|
+
crawler or a shell script that writes the same JSON all produce the same catalog.
|
|
9997
|
+
|
|
9998
|
+
Shards need no coordination: each shard calls {@link uiStateLayer} for the states it captured (and optionally
|
|
9999
|
+
{@link uiPlanLayer} for the states it was given), with its own `shard` label, and publishes. A planner that knows the whole
|
|
10000
|
+
product publishes the plan once.
|
|
10001
|
+
|
|
10002
|
+
See docs/ui-catalog.md
|
|
10003
|
+
|
|
10004
|
+
#### `UiPicture`
|
|
10005
|
+
|
|
10006
|
+
```ts
|
|
10007
|
+
export type UiPicture = {
|
|
10008
|
+
/** The file name; default `<id>.png`. */
|
|
10009
|
+
readonly name?: string;
|
|
10010
|
+
readonly data?: Uint8Array;
|
|
10011
|
+
readonly base64?: string;
|
|
10012
|
+
readonly uri?: string;
|
|
10013
|
+
/** Default `image/png`. */
|
|
10014
|
+
readonly contentType?: string;
|
|
10015
|
+
readonly width?: number;
|
|
10016
|
+
readonly height?: number;
|
|
10017
|
+
};
|
|
10018
|
+
```
|
|
10019
|
+
|
|
10020
|
+
A picture to store with a capture: its bytes (`data`, `base64`), or where it already is (`uri`).
|
|
10021
|
+
|
|
10022
|
+
#### `UiStateCapture`
|
|
10023
|
+
|
|
10024
|
+
```ts
|
|
10025
|
+
export type UiStateCapture = UiStateFacts & {
|
|
10026
|
+
readonly id: string;
|
|
10027
|
+
/** For a person; default the id. */
|
|
10028
|
+
readonly title?: string;
|
|
10029
|
+
/** `passed` (default): captured. `failed`: the capture was attempted and did not work (say why in `detail`). `running`: being taken. `not-run`: skipped. */
|
|
10030
|
+
readonly status?: 'passed' | 'failed' | 'running' | 'not-run';
|
|
10031
|
+
/** Why it failed or was skipped: a readiness check that never became true, a console error. */
|
|
10032
|
+
readonly detail?: string;
|
|
10033
|
+
readonly screenshot?: UiPicture;
|
|
10034
|
+
readonly durationMs?: number;
|
|
10035
|
+
/** More facts about the capture that never split it (`browser`, `os`). */
|
|
10036
|
+
readonly labels?: Readonly<Record<string, string>>;
|
|
10037
|
+
readonly tags?: readonly string[];
|
|
10038
|
+
};
|
|
10039
|
+
```
|
|
10040
|
+
|
|
10041
|
+
One captured state. `id` must be stable across runs: it is how a new capture replaces an old one.
|
|
10042
|
+
|
|
10043
|
+
#### `UiStatePlan`
|
|
10044
|
+
|
|
10045
|
+
```ts
|
|
10046
|
+
export type UiStatePlan = UiStateFacts & {
|
|
10047
|
+
readonly id: string;
|
|
10048
|
+
readonly title?: string;
|
|
10049
|
+
};
|
|
10050
|
+
```
|
|
10051
|
+
|
|
10052
|
+
One expected state, or (with `inventory`) one surface of the product's inventory.
|
|
10053
|
+
|
|
10054
|
+
#### `UiLayerOrigin`
|
|
10055
|
+
|
|
10056
|
+
```ts
|
|
10057
|
+
export type UiLayerOrigin = {
|
|
10058
|
+
/** The suite or tool that makes the layer. Part of the slot; a planner and a collector normally differ. */
|
|
10059
|
+
readonly producer: string;
|
|
10060
|
+
/** When the captures were taken (or the plan was made), epoch milliseconds. Take it from the run, not from the moment of publishing. */
|
|
10061
|
+
readonly recordedAtMs: number;
|
|
10062
|
+
/** The client: `desktop-web`, `mobile-web`, `ios`, `android`… Part of the slot. */
|
|
10063
|
+
readonly environment?: string;
|
|
10064
|
+
/** The rest of the profile, such as `{ viewport: '390x844', scheme: 'dark' }`. Each combination is its own result. Part of the slot. */
|
|
10065
|
+
readonly profile?: Readonly<Record<string, string>>;
|
|
10066
|
+
readonly sha?: string | null;
|
|
10067
|
+
readonly branch?: string | null;
|
|
10068
|
+
readonly runUrl?: string | null;
|
|
10069
|
+
/** Which shard this is (`2/4`, `checkout`, a machine name). Shown with each capture; never part of the slot. */
|
|
10070
|
+
readonly shard?: string;
|
|
10071
|
+
/** The execution these layers belong to, so shards of one run stay one run. */
|
|
10072
|
+
readonly run?: { readonly id: string; readonly title?: string };
|
|
10073
|
+
};
|
|
10074
|
+
```
|
|
10075
|
+
|
|
10076
|
+
What every layer of a catalog says about where and when it was recorded.
|
|
10077
|
+
|
|
10078
|
+
#### `uiStateLayer`
|
|
10079
|
+
|
|
10080
|
+
```ts
|
|
10081
|
+
export function uiStateLayer(input: UiLayerOrigin & { readonly states: readonly UiStateCapture[] }): PublishChecksInput
|
|
10082
|
+
```
|
|
10083
|
+
|
|
10084
|
+
The input of `publishChecks` for the states one collector (or one shard) captured under one profile. Throws when a state id
|
|
10085
|
+
repeats, because the second would silently replace the first. A state without a `status` is `passed`: it was captured.
|
|
10086
|
+
|
|
10087
|
+
**Example**
|
|
10088
|
+
|
|
10089
|
+
```ts
|
|
10090
|
+
await publishChecks(store, uiStateLayer({
|
|
10091
|
+
producer: 'web-capture', recordedAtMs: Date.now(), environment: 'mobile-web', profile: { viewport: '390x844', scheme: 'dark' }, shard: '1/4',
|
|
10092
|
+
states: [{ id: 'checkout/payment', title: 'Payment', route: '/checkout', covers: ['payment-form'], screenshot: { data: png } }]
|
|
10093
|
+
}));
|
|
10094
|
+
```
|
|
10095
|
+
|
|
10096
|
+
#### `uiPlanLayer`
|
|
10097
|
+
|
|
10098
|
+
```ts
|
|
10099
|
+
export function uiPlanLayer(input: UiLayerOrigin & { readonly states: readonly UiStatePlan[] }): PublishChecksInput
|
|
10100
|
+
```
|
|
10101
|
+
|
|
10102
|
+
The input of `publishChecks` for what is expected: states that should be captured under this profile and, with `inventory`,
|
|
10103
|
+
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
|
|
10104
|
+
its own (kind `ui-plan`), so a capture never erases it and it never hides a capture. Throws when an id repeats.
|
|
10105
|
+
|
|
10106
|
+
### UI catalog
|
|
10107
|
+
|
|
10108
|
+
Source: `packages/reports/src/ui-catalog.ts`. Import: `@crvouga/atlas/reports/ui-catalog`
|
|
10109
|
+
|
|
10110
|
+
A browsable inventory of a product's user interface, computed from layers and nothing else: each **state** (a screen, or
|
|
10111
|
+
one state of a screen) captured once per **profile** (viewport, color scheme, device), with the facts needed to find and
|
|
10112
|
+
trust it, and what should exist but has not been captured yet. `uiCatalog` is a pure function of the winning results of
|
|
10113
|
+
the layers it is given, so it inherits the report contract's laws: the same layers in any order, split any way across
|
|
10114
|
+
backends, shards and machines, give the same catalog; a layer changes only the cells it fills; an unreadable layer is
|
|
10115
|
+
absent, never captured. It imports no collection framework and knows nothing about how a picture was taken.
|
|
10116
|
+
|
|
10117
|
+
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
|
|
10118
|
+
is the layer's `environment` and `parameters`, the expected set is a `ui-plan` part. The viewer computes the catalog from
|
|
10119
|
+
the report it already streams (so it fills in as results arrive), and the server answers the same document at
|
|
10120
|
+
`GET /api/reports/ui-catalog`.
|
|
10121
|
+
|
|
10122
|
+
See docs/ui-catalog.md
|
|
10123
|
+
See docs/report-contract.md#how-layers-compose
|
|
10124
|
+
|
|
10125
|
+
#### `UI_CATALOG_VERSION`
|
|
10126
|
+
|
|
10127
|
+
```ts
|
|
10128
|
+
export const UI_CATALOG_VERSION = …
|
|
10129
|
+
```
|
|
10130
|
+
|
|
10131
|
+
The `schemaVersion` of the catalog document.
|
|
10132
|
+
|
|
10133
|
+
#### `UiProfile`
|
|
10134
|
+
|
|
10135
|
+
```ts
|
|
10136
|
+
export type UiProfile = {
|
|
10137
|
+
/** Stable identity: the environment and the sorted parameters. */
|
|
10138
|
+
readonly key: string;
|
|
10139
|
+
/** The client (`desktop-web`, `ios`…), or null when the layer named none. */
|
|
10140
|
+
readonly environment: string | null;
|
|
10141
|
+
/** The layer's other choices, such as `{ viewport: '390x844', scheme: 'dark' }`. */
|
|
10142
|
+
readonly parameters: Readonly<Record<string, string>>;
|
|
10143
|
+
/** For a person: the client and each parameter's value. */
|
|
10144
|
+
readonly label: string;
|
|
10145
|
+
};
|
|
10146
|
+
```
|
|
10147
|
+
|
|
10148
|
+
One profile: the client and the choices a state was captured under. Its `key` identifies it across layers.
|
|
10149
|
+
|
|
10150
|
+
#### `UiCoverage`
|
|
10151
|
+
|
|
10152
|
+
```ts
|
|
10153
|
+
export type UiCoverage = 'captured' | 'failed' | 'capturing' | 'skipped' | 'missing';
|
|
10154
|
+
```
|
|
10155
|
+
|
|
10156
|
+
How one state stands in one profile. `captured`: a capture passed. `failed`: it was attempted and failed (its `detail`
|
|
10157
|
+
says why). `capturing`: it is being taken now. `skipped`: a producer recorded it as not run. `missing`: expected, and
|
|
10158
|
+
nothing has been recorded. Only `captured` is ever shown as captured; every other value means there is no picture to trust.
|
|
10159
|
+
|
|
10160
|
+
#### `UI_COVERAGE`
|
|
10161
|
+
|
|
10162
|
+
```ts
|
|
10163
|
+
export const UI_COVERAGE = …
|
|
10164
|
+
```
|
|
10165
|
+
|
|
10166
|
+
The values of {@link UiCoverage}, in the order a person reads them.
|
|
10167
|
+
|
|
10168
|
+
#### `UiImage`
|
|
10169
|
+
|
|
10170
|
+
```ts
|
|
10171
|
+
export type UiImage = UiScreenshot & {
|
|
10172
|
+
/** Where the stored bytes are, relative to the reports API root (`/layers/<layer>/blobs/attachments/<sha256>`). Absent for an address-only or omitted picture. */
|
|
10173
|
+
readonly path?: string;
|
|
10174
|
+
};
|
|
10175
|
+
```
|
|
10176
|
+
|
|
10177
|
+
A picture, with the address a browser loads it from.
|
|
10178
|
+
|
|
10179
|
+
#### `UiOrigin`
|
|
10180
|
+
|
|
10181
|
+
```ts
|
|
10182
|
+
export type UiOrigin = {
|
|
10183
|
+
readonly layer: string;
|
|
10184
|
+
readonly backend: string;
|
|
10185
|
+
readonly producer: string;
|
|
10186
|
+
readonly recordedAtMs: number;
|
|
10187
|
+
readonly sha: string | null;
|
|
10188
|
+
readonly branch: string | null;
|
|
10189
|
+
readonly runUrl: string | null;
|
|
10190
|
+
readonly freshness?: 'unknown' | 'modified';
|
|
10191
|
+
};
|
|
10192
|
+
```
|
|
10193
|
+
|
|
10194
|
+
Where a capture came from: the layer, the backend that holds it, and the provenance the layer recorded.
|
|
10195
|
+
|
|
10196
|
+
#### `UiCapture`
|
|
10197
|
+
|
|
10198
|
+
```ts
|
|
10199
|
+
export type UiCapture = {
|
|
10200
|
+
readonly status: EvidenceStatus;
|
|
10201
|
+
/** Why it failed or was skipped; empty otherwise. */
|
|
10202
|
+
readonly detail: string;
|
|
10203
|
+
readonly durationMs: number | null;
|
|
10204
|
+
/** The picture, or null when the capture has none (or it was left out: see `omitted`). */
|
|
10205
|
+
readonly image: UiImage | null;
|
|
10206
|
+
/** The check's labels: what describes the capture without splitting it (a `shard`, a `browser`). */
|
|
10207
|
+
readonly labels: Readonly<Record<string, string>>;
|
|
10208
|
+
readonly origin: UiOrigin;
|
|
10209
|
+
/** How many other captures of this cell exist from other producers. The winner is the one the report contract picks. */
|
|
10210
|
+
readonly alternatives: number;
|
|
10211
|
+
};
|
|
10212
|
+
```
|
|
10213
|
+
|
|
10214
|
+
The winning capture of one cell.
|
|
10215
|
+
|
|
10216
|
+
#### `UiCell`
|
|
10217
|
+
|
|
10218
|
+
```ts
|
|
10219
|
+
export type UiCell = {
|
|
10220
|
+
readonly profile: UiProfile;
|
|
10221
|
+
readonly coverage: UiCoverage;
|
|
10222
|
+
/** 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. */
|
|
10223
|
+
readonly expected: 'declared' | 'undeclared' | 'unknown';
|
|
10224
|
+
readonly capture: UiCapture | null;
|
|
10225
|
+
/** The layer of the plan that expects it, when one does. */
|
|
10226
|
+
readonly planned: { readonly layer: string; readonly backend: string } | null;
|
|
10227
|
+
};
|
|
10228
|
+
```
|
|
10229
|
+
|
|
10230
|
+
One state in one profile.
|
|
10231
|
+
|
|
10232
|
+
#### `UiCatalogState`
|
|
10233
|
+
|
|
10234
|
+
```ts
|
|
10235
|
+
export type UiCatalogState = {
|
|
10236
|
+
/** The check id the producers gave it. */
|
|
10237
|
+
readonly id: string;
|
|
10238
|
+
readonly title: string;
|
|
10239
|
+
readonly route: string | null;
|
|
10240
|
+
readonly fixture: string | null;
|
|
10241
|
+
readonly cohort: string | null;
|
|
10242
|
+
readonly area: string | null;
|
|
10243
|
+
readonly flags: readonly string[];
|
|
10244
|
+
/** The surfaces it covers. */
|
|
10245
|
+
readonly covers: readonly string[];
|
|
10246
|
+
/** The producers that captured or planned it. */
|
|
10247
|
+
readonly producers: readonly string[];
|
|
10248
|
+
/** One cell per profile, ordered by the profile's label. */
|
|
10249
|
+
readonly cells: readonly UiCell[];
|
|
10250
|
+
};
|
|
10251
|
+
```
|
|
10252
|
+
|
|
10253
|
+
One state of the product's interface, across its profiles.
|
|
10254
|
+
|
|
10255
|
+
#### `UiSurface`
|
|
10256
|
+
|
|
10257
|
+
```ts
|
|
10258
|
+
export type UiSurface = {
|
|
10259
|
+
readonly id: string;
|
|
10260
|
+
/** True when a `ui-plan` inventory entry names it: the product is known to have it. */
|
|
10261
|
+
readonly inventoried: boolean;
|
|
10262
|
+
/** States that cover it. */
|
|
10263
|
+
readonly states: number;
|
|
10264
|
+
/** Of those, states with at least one captured profile. */
|
|
10265
|
+
readonly captured: number;
|
|
10266
|
+
/** `covered`: some covering state is captured. `uncaptured`: states cover it and none is captured yet. `uncovered`: no state covers it. */
|
|
10267
|
+
readonly status: 'covered' | 'uncaptured' | 'uncovered';
|
|
10268
|
+
};
|
|
10269
|
+
```
|
|
10270
|
+
|
|
10271
|
+
One surface of the product: an inventory entry, or something a state says it covers.
|
|
10272
|
+
|
|
10273
|
+
#### `UiFacetValue`
|
|
10274
|
+
|
|
10275
|
+
```ts
|
|
10276
|
+
export type UiFacetValue = { readonly value: string; readonly count: number; readonly label?: string };
|
|
10277
|
+
```
|
|
10278
|
+
|
|
10279
|
+
A value of a facet and how many states have it.
|
|
10280
|
+
|
|
10281
|
+
#### `UiCatalogFacets`
|
|
10282
|
+
|
|
10283
|
+
```ts
|
|
10284
|
+
export type UiCatalogFacets = {
|
|
10285
|
+
readonly profiles: readonly UiFacetValue[];
|
|
10286
|
+
readonly coverage: readonly UiFacetValue[];
|
|
10287
|
+
readonly areas: readonly UiFacetValue[];
|
|
10288
|
+
readonly routes: readonly UiFacetValue[];
|
|
10289
|
+
readonly cohorts: readonly UiFacetValue[];
|
|
10290
|
+
readonly flags: readonly UiFacetValue[];
|
|
10291
|
+
readonly surfaces: readonly UiFacetValue[];
|
|
10292
|
+
readonly producers: readonly UiFacetValue[];
|
|
10293
|
+
};
|
|
10294
|
+
```
|
|
10295
|
+
|
|
10296
|
+
The values each filter offers, over every state of the catalog.
|
|
10297
|
+
|
|
10298
|
+
#### `UiCatalogSummary`
|
|
10299
|
+
|
|
10300
|
+
```ts
|
|
10301
|
+
export type UiCatalogSummary = {
|
|
10302
|
+
readonly states: number;
|
|
10303
|
+
readonly profiles: number;
|
|
10304
|
+
readonly cells: number;
|
|
10305
|
+
readonly coverage: Readonly<Record<UiCoverage, number>>;
|
|
10306
|
+
/** Captured or failed cells no plan expects. Zero when no plan was published, because then nothing is known to be unexpected. */
|
|
10307
|
+
readonly undeclared: number;
|
|
10308
|
+
/** Whether any plan (an expected set) was published. Without one, `missing` can only ever be zero. */
|
|
10309
|
+
readonly planned: boolean;
|
|
10310
|
+
readonly surfaces: number;
|
|
10311
|
+
readonly uncoveredSurfaces: number;
|
|
10312
|
+
};
|
|
10313
|
+
```
|
|
10314
|
+
|
|
10315
|
+
The catalog in numbers. Cells count a state once per profile.
|
|
10316
|
+
|
|
10317
|
+
#### `UiCatalog`
|
|
10318
|
+
|
|
10319
|
+
```ts
|
|
10320
|
+
export type UiCatalog = {
|
|
10321
|
+
readonly schemaVersion: typeof UI_CATALOG_VERSION;
|
|
10322
|
+
readonly version?: string;
|
|
10323
|
+
readonly summary: UiCatalogSummary;
|
|
10324
|
+
readonly profiles: readonly UiProfile[];
|
|
10325
|
+
readonly states: readonly UiCatalogState[];
|
|
10326
|
+
readonly surfaces: readonly UiSurface[];
|
|
10327
|
+
readonly facets: UiCatalogFacets;
|
|
10328
|
+
/** 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. */
|
|
10329
|
+
readonly sources: { readonly layers: number; readonly backends: readonly BackendInfo[]; readonly notices: readonly string[] };
|
|
10330
|
+
};
|
|
10331
|
+
```
|
|
10332
|
+
|
|
10333
|
+
The catalog, as `GET /api/reports/ui-catalog` answers it (`version` is the report's, when the caller knows it).
|
|
10334
|
+
|
|
10335
|
+
#### `UiCatalogOptions`
|
|
10336
|
+
|
|
10337
|
+
```ts
|
|
10338
|
+
export type UiCatalogOptions = {
|
|
10339
|
+
/** The report's version, repeated in the document. */
|
|
10340
|
+
readonly version?: string;
|
|
10341
|
+
/** The backends, with whether each could be read. */
|
|
10342
|
+
readonly backends?: readonly BackendInfo[];
|
|
10343
|
+
/** What could not be read. */
|
|
10344
|
+
readonly notices?: readonly string[];
|
|
10345
|
+
};
|
|
10346
|
+
```
|
|
10347
|
+
|
|
10348
|
+
What a caller may say about how the layers were read.
|
|
10349
|
+
|
|
10350
|
+
#### `uiProfile`
|
|
10351
|
+
|
|
10352
|
+
```ts
|
|
10353
|
+
export function uiProfile(layer: Pick<LayerHeader, 'environment' | 'parameters'>): UiProfile
|
|
10354
|
+
```
|
|
10355
|
+
|
|
10356
|
+
The profile of a layer: its environment and its other parameters.
|
|
10357
|
+
|
|
10358
|
+
#### `uiCatalog`
|
|
10359
|
+
|
|
10360
|
+
```ts
|
|
10361
|
+
export function uiCatalog(layers: readonly LayerEvidence[], options: UiCatalogOptions = {}): UiCatalog
|
|
10362
|
+
```
|
|
10363
|
+
|
|
10364
|
+
The UI catalog of some layers (as the effective report holds them, or any set of {@link LayerEvidence}).
|
|
10365
|
+
|
|
10366
|
+
Only rows of `ui-state` and `ui-plan` parts take part. The winner of every slot is decided by the report contract
|
|
10367
|
+
(`combineEvidence`), so the result does not depend on the order of `layers` or on how they are divided among backends. A
|
|
10368
|
+
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
|
|
10369
|
+
capture passed; a plan that nothing has captured is `missing`. When no plan exists nothing is `missing` and nothing is
|
|
10370
|
+
`undeclared`: what is expected is then unknown, and the catalog says so (`summary.planned`).
|
|
10371
|
+
|
|
10372
|
+
**Example**
|
|
10373
|
+
|
|
10374
|
+
```ts
|
|
10375
|
+
const catalog = uiCatalog(effective.layers, { version: effective.version, backends: effective.backends });
|
|
10376
|
+
catalog.states.map(state => [state.id, state.cells.map(cell => cell.coverage)]);
|
|
10377
|
+
```
|
|
10378
|
+
|
|
10379
|
+
#### `UiCatalogQuery`
|
|
10380
|
+
|
|
10381
|
+
```ts
|
|
10382
|
+
export type UiCatalogQuery = {
|
|
10383
|
+
/** Words (all of them) found in the id, title, route, fixture, cohort, area, flags, surfaces or producers. */
|
|
10384
|
+
readonly q?: string;
|
|
10385
|
+
readonly coverage?: readonly UiCoverage[];
|
|
10386
|
+
/** Profile keys, as {@link UiProfile.key}. */
|
|
10387
|
+
readonly profile?: readonly string[];
|
|
10388
|
+
readonly area?: readonly string[];
|
|
10389
|
+
readonly route?: readonly string[];
|
|
10390
|
+
readonly cohort?: readonly string[];
|
|
10391
|
+
readonly flag?: readonly string[];
|
|
10392
|
+
/** Surfaces a state covers. */
|
|
10393
|
+
readonly surface?: readonly string[];
|
|
10394
|
+
readonly producer?: readonly string[];
|
|
10395
|
+
};
|
|
10396
|
+
```
|
|
10397
|
+
|
|
10398
|
+
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.
|
|
10399
|
+
|
|
10400
|
+
#### `parseUiCatalogQuery`
|
|
10401
|
+
|
|
10402
|
+
```ts
|
|
10403
|
+
export function parseUiCatalogQuery(params: URLSearchParams): UiCatalogQuery
|
|
10404
|
+
```
|
|
10405
|
+
|
|
10406
|
+
Reads a query from URL search parameters (`q`, and the comma-separated {@link UiCatalogQuery} lists). Unknown coverage values are left out, never an error.
|
|
10407
|
+
|
|
10408
|
+
#### `uiCatalogParams`
|
|
10409
|
+
|
|
10410
|
+
```ts
|
|
10411
|
+
export function uiCatalogParams(query: UiCatalogQuery): URLSearchParams
|
|
10412
|
+
```
|
|
10413
|
+
|
|
10414
|
+
A query as URL search parameters, the form {@link parseUiCatalogQuery} reads back.
|
|
10415
|
+
|
|
10416
|
+
#### `isUiCatalogFiltered`
|
|
10417
|
+
|
|
10418
|
+
```ts
|
|
10419
|
+
export function isUiCatalogFiltered(query: UiCatalogQuery): boolean
|
|
10420
|
+
```
|
|
10421
|
+
|
|
10422
|
+
Whether a query narrows anything.
|
|
10423
|
+
|
|
10424
|
+
#### `filterUiCatalog`
|
|
10425
|
+
|
|
10426
|
+
```ts
|
|
10427
|
+
export function filterUiCatalog(catalog: UiCatalog, query: UiCatalogQuery): UiCatalog
|
|
10428
|
+
```
|
|
10429
|
+
|
|
10430
|
+
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
|
|
10431
|
+
kept when they match the `profile` and `coverage` filters, and a state left with no cell is dropped. The summary is
|
|
10432
|
+
recomputed for what is kept; the profiles, surfaces, facets and sources stay those of the whole catalog, so the
|
|
10433
|
+
controls keep offering every choice.
|
|
10434
|
+
|
|
10435
|
+
#### `pageUiCatalog`
|
|
10436
|
+
|
|
10437
|
+
```ts
|
|
10438
|
+
export function pageUiCatalog(catalog: UiCatalog, page: { readonly limit?: number; readonly offset?: number }): UiCatalog & { readonly total: number; readonly offset: number; readonly limit: number }
|
|
10439
|
+
```
|
|
10440
|
+
|
|
10441
|
+
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.
|
|
10442
|
+
|
|
10443
|
+
#### `uiCatalogMarkdown`
|
|
10444
|
+
|
|
10445
|
+
```ts
|
|
10446
|
+
export function uiCatalogMarkdown(catalog: UiCatalog): string
|
|
10447
|
+
```
|
|
10448
|
+
|
|
10449
|
+
The catalog as Markdown for a person or an agent reading text: the summary in words, the surfaces that are not covered, and
|
|
10450
|
+
one line per state with its coverage in each profile. Only `captured` is called captured.
|
|
10451
|
+
|
|
10452
|
+
### UI state conventions
|
|
10453
|
+
|
|
10454
|
+
Source: `packages/reports/src/ui-state.ts`. Import: `@crvouga/atlas/reports/ui-state`
|
|
10455
|
+
|
|
10456
|
+
How a captured UI state is written in a layer, using nothing but what the layer format already has. A **state** is a
|
|
10457
|
+
check in a part of kind `ui-state`: its `id` names the state, its `status` says how the capture went, a `screenshot`
|
|
10458
|
+
attachment is the picture, and the facts the producer declares about it (route, fixture, cohort, flags, the surfaces it
|
|
10459
|
+
covers) sit under the `atlas.ui-state` extension. A **profile** (viewport, color scheme, device) is the layer's
|
|
10460
|
+
`environment` and `parameters`, so a state captured at another viewport is another result, side by side. What is
|
|
10461
|
+
**expected** is a layer of kind `ui-plan` with the same ids and profiles, every check `not-run`.
|
|
10462
|
+
|
|
10463
|
+
Nothing here decides a result: a status, a slot and a winner are what the report contract says they are. This module
|
|
10464
|
+
only reads the convention into a small optional row field ({@link UiStateFact}) so a viewer needs no second request to
|
|
10465
|
+
list a catalog. A producer that writes something else is still read; what cannot be read is left out, never rejected.
|
|
10466
|
+
|
|
10467
|
+
See docs/ui-catalog.md
|
|
10468
|
+
|
|
10469
|
+
#### `UI_STATE_KIND`
|
|
10470
|
+
|
|
10471
|
+
```ts
|
|
10472
|
+
export const UI_STATE_KIND = …
|
|
10473
|
+
```
|
|
10474
|
+
|
|
10475
|
+
The part kind of captured states.
|
|
10476
|
+
|
|
10477
|
+
#### `UI_PLAN_KIND`
|
|
10478
|
+
|
|
10479
|
+
```ts
|
|
10480
|
+
export const UI_PLAN_KIND = …
|
|
10481
|
+
```
|
|
10482
|
+
|
|
10483
|
+
The part kind of the expected set: states (and surfaces) that should exist, whether or not they were captured.
|
|
10484
|
+
|
|
10485
|
+
#### `UI_STATE_EXTENSION`
|
|
10486
|
+
|
|
10487
|
+
```ts
|
|
10488
|
+
export const UI_STATE_EXTENSION = …
|
|
10489
|
+
```
|
|
10490
|
+
|
|
10491
|
+
The check extension that carries the facts a producer declares about a state.
|
|
10492
|
+
|
|
10493
|
+
#### `isUiKind`
|
|
10494
|
+
|
|
10495
|
+
```ts
|
|
10496
|
+
export function isUiKind(kind: string): boolean
|
|
10497
|
+
```
|
|
10498
|
+
|
|
10499
|
+
Whether a part kind belongs to the UI catalog (`ui-state` or `ui-plan`).
|
|
10500
|
+
|
|
10501
|
+
#### `UiScreenshot`
|
|
10502
|
+
|
|
10503
|
+
```ts
|
|
10504
|
+
export type UiScreenshot = {
|
|
10505
|
+
readonly name: string;
|
|
10506
|
+
readonly bundleKey?: string;
|
|
10507
|
+
readonly uri?: string;
|
|
10508
|
+
readonly contentType?: string;
|
|
10509
|
+
readonly width?: number;
|
|
10510
|
+
readonly height?: number;
|
|
10511
|
+
readonly bytes?: number;
|
|
10512
|
+
readonly sha256?: string;
|
|
10513
|
+
/** Why the producer left the bytes out (`too-large`, `policy`…): a picture that was never taken is told apart from one left out. */
|
|
10514
|
+
readonly omitted?: string;
|
|
10515
|
+
};
|
|
10516
|
+
```
|
|
10517
|
+
|
|
10518
|
+
The picture of a captured state: a stored file (`bundleKey`), or an address (`uri`). Everything but `name` is optional.
|
|
10519
|
+
|
|
10520
|
+
#### `UiStateFacts`
|
|
10521
|
+
|
|
10522
|
+
```ts
|
|
10523
|
+
export type UiStateFacts = {
|
|
10524
|
+
/** The address or screen the state is of (`/checkout/payment`). */
|
|
10525
|
+
readonly route?: string;
|
|
10526
|
+
/** The test data it was captured with. */
|
|
10527
|
+
readonly fixture?: string;
|
|
10528
|
+
/** The audience or account kind it shows (`new-customer`). */
|
|
10529
|
+
readonly cohort?: string;
|
|
10530
|
+
/** Feature flags that were on. */
|
|
10531
|
+
readonly flags?: readonly string[];
|
|
10532
|
+
/** The product surfaces (screens, components) this state covers, for coverage against an inventory. */
|
|
10533
|
+
readonly covers?: readonly string[];
|
|
10534
|
+
/** A grouping a person navigates by (`checkout`). */
|
|
10535
|
+
readonly area?: string;
|
|
10536
|
+
/** On a `ui-plan` check only: this entry is a surface of the product's inventory, not a state. Nothing is captured for it. */
|
|
10537
|
+
readonly inventory?: boolean;
|
|
10538
|
+
};
|
|
10539
|
+
```
|
|
10540
|
+
|
|
10541
|
+
The facts a producer declares about a state, under the `atlas.ui-state` extension. All optional, all strings or lists of strings.
|
|
10542
|
+
|
|
10543
|
+
#### `UiStateFact`
|
|
10544
|
+
|
|
10545
|
+
```ts
|
|
10546
|
+
export type UiStateFact = UiStateFacts & {
|
|
10547
|
+
/** The check's picture: its `screenshot` attachment, else its first image. */
|
|
10548
|
+
readonly screenshot?: UiScreenshot;
|
|
10549
|
+
};
|
|
10550
|
+
```
|
|
10551
|
+
|
|
10552
|
+
What a row of a `ui-state` or `ui-plan` part carries for the catalog. Rides in the report row, only for those kinds.
|
|
10553
|
+
|
|
10554
|
+
#### `uiStateFacts`
|
|
10555
|
+
|
|
10556
|
+
```ts
|
|
10557
|
+
export function uiStateFacts(check: Pick<Check, 'extensions'>): UiStateFacts
|
|
10558
|
+
```
|
|
10559
|
+
|
|
10560
|
+
The declared facts of a check, read from its `atlas.ui-state` extension. Strings are trimmed and bounded, lists are
|
|
10561
|
+
distinct and bounded, and anything of another shape is dropped; a check without the extension has no facts.
|
|
10562
|
+
|
|
10563
|
+
#### `uiStateOf`
|
|
10564
|
+
|
|
10565
|
+
```ts
|
|
10566
|
+
export function uiStateOf(kind: string, check: Pick<Check, 'extensions' | 'attachments'>): UiStateFact | undefined
|
|
10567
|
+
```
|
|
10568
|
+
|
|
10569
|
+
What a check of a part of `kind` carries for the catalog: its declared facts and, in a `ui-state` part, its picture.
|
|
10570
|
+
`undefined` for any other kind, and for a UI check that carries neither, so ordinary rows stay as small as they were.
|
|
10571
|
+
|
|
9985
10572
|
### Report viewer navigation state
|
|
9986
10573
|
|
|
9987
10574
|
Source: `packages/reports/src/use-report-nav.ts`. Import: `@crvouga/atlas/reports/use-report-nav`
|