@crvouga/atlas 1.13.0 → 1.15.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 +14 -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-detail.d.ts +19 -0
- package/dist/reports/src/evidence-detail.js +31 -4
- package/dist/reports/src/evidence-types.d.ts +5 -2
- package/dist/reports/src/layer-merge.d.ts +5 -1
- package/dist/reports/src/layer-merge.js +13 -5
- package/dist/reports/src/layer-store.d.ts +1 -1
- package/dist/reports/src/layer-store.js +4 -2
- package/dist/reports/src/layer-types.d.ts +2 -0
- package/dist/reports/src/report-index.js +9 -5
- package/dist/reports/src/run-compare.d.ts +182 -0
- package/dist/reports/src/run-compare.js +329 -0
- package/dist/reports/src/test-catalog.d.ts +8 -0
- package/dist/reports/src/test-catalog.js +14 -1
- 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 +354 -0
- package/dist/schema/src/api.js +78 -3
- 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 +942 -4
- 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 +45 -1
- package/llms-full.txt +276 -3
- package/llms.txt +1 -0
- package/package.json +2 -1
- package/packages/schema/schemas/effective-delta.schema.json +75 -2
- package/packages/schema/schemas/effective-evidence.schema.json +75 -2
- package/packages/schema/schemas/evidence-page.schema.json +1 -1
- package/packages/schema/schemas/layer-rows.schema.json +74 -1
- package/packages/schema/schemas/report-run.schema.json +25 -3
- package/packages/schema/schemas/report-runs.schema.json +1 -1
- package/packages/schema/schemas/review-collection.schema.json +1 -1
- package/packages/schema/schemas/review-manifest.schema.json +1 -1
- package/packages/schema/schemas/test-catalog.schema.json +1 -1
- package/packages/schema/schemas/test-history.schema.json +2 -2
- package/packages/server/openapi.json +1559 -175
- 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
- package/packages/visualizer/src/explorer/ComparePair.tsx +597 -0
- package/packages/visualizer/src/explorer/CompareView.tsx +340 -0
- package/packages/visualizer/src/explorer/Explorer.tsx +6 -2
- package/packages/visualizer/src/explorer/RunsView.tsx +10 -0
- package/packages/visualizer/src/explorer/playlist.tsx +1 -1
- package/packages/visualizer/src/explorer/search.ts +15 -3
|
@@ -1694,6 +1694,16 @@ Like {@link summarizeEvidence} for the checks of a part: a video that declares n
|
|
|
1694
1694
|
declares none, takes the scope `inherited` from its part or layer (the nearest declaration of the two, see
|
|
1695
1695
|
`effectiveScope`). Without `inherited` it is the same as `summarizeEvidence`.
|
|
1696
1696
|
|
|
1697
|
+
#### `summarizeStoredEvidence`
|
|
1698
|
+
|
|
1699
|
+
```ts
|
|
1700
|
+
export function summarizeStoredEvidence(inherited: EvidenceScope | undefined, unstored: ReadonlySet<string> | undefined, ...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined
|
|
1701
|
+
```
|
|
1702
|
+
|
|
1703
|
+
Like {@link summarizeEvidenceIn}, knowing which bundle keys the layer names but does not hold (`unstored`, see
|
|
1704
|
+
`unstoredKeys`): a video whose only bytes are such a key is `available: false`, never a recording. Without
|
|
1705
|
+
`unstored` it is the same as `summarizeEvidenceIn`.
|
|
1706
|
+
|
|
1697
1707
|
#### `summarizeLayerEvidence`
|
|
1698
1708
|
|
|
1699
1709
|
```ts
|
|
@@ -1703,6 +1713,14 @@ export function summarizeLayerEvidence(...holders: readonly EvidenceHolder[]): E
|
|
|
1703
1713
|
Like {@link summarizeEvidence} for a layer and its parts: the same summary, but a video attached to the layer or a
|
|
1704
1714
|
part is owned by the `layer`, not by a check.
|
|
1705
1715
|
|
|
1716
|
+
#### `summarizeStoredLayerEvidence`
|
|
1717
|
+
|
|
1718
|
+
```ts
|
|
1719
|
+
export function summarizeStoredLayerEvidence(unstored: ReadonlySet<string> | undefined, ...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined
|
|
1720
|
+
```
|
|
1721
|
+
|
|
1722
|
+
Like {@link summarizeLayerEvidence}, knowing which bundle keys the layer names but does not hold (see {@link summarizeStoredEvidence}).
|
|
1723
|
+
|
|
1706
1724
|
#### `isVideoAttachment`
|
|
1707
1725
|
|
|
1708
1726
|
```ts
|
|
@@ -1804,6 +1822,16 @@ export function bundleKeysOf(holder: EvidenceHolder): string[]
|
|
|
1804
1822
|
|
|
1805
1823
|
Every distinct bundle key (`bundleKey`) that the attachments of a holder, its steps and its attempts point to, so a layer's files can be copied. Attachments held by `uri` or inline `text` have none.
|
|
1806
1824
|
|
|
1825
|
+
#### `unstoredKeys`
|
|
1826
|
+
|
|
1827
|
+
```ts
|
|
1828
|
+
export function unstoredKeys(layer: EvidenceHolder & { readonly parts: readonly (EvidenceHolder & { readonly checks: readonly EvidenceHolder[] })[]; readonly bundle: Readonly<Record<string, unknown>> }): string[]
|
|
1829
|
+
```
|
|
1830
|
+
|
|
1831
|
+
The bundle keys that attachments of a layer, its parts and their checks name, but that the layer's `bundle` index does
|
|
1832
|
+
not hold: files the producer referenced and never stored. Empty for a layer whose every file is there. This reads only
|
|
1833
|
+
the index, never a blob, so it is cheap enough to ask of every layer as it is read.
|
|
1834
|
+
|
|
1807
1835
|
### Live evidence from running suites
|
|
1808
1836
|
|
|
1809
1837
|
Source: `packages/reports/src/evidence-live.ts`. Import: `@crvouga/atlas/reports/evidence-live`
|
|
@@ -2109,7 +2137,7 @@ export type VideoRef = {
|
|
|
2109
2137
|
readonly tags?: readonly string[];
|
|
2110
2138
|
/** What identifies the bytes (digest, bundle key or address); references with the same one are the same recording. Absent when `available` is false. */
|
|
2111
2139
|
readonly recording?: string;
|
|
2112
|
-
/** False when the reference names no bytes at all (no bundle key, address or inline text), so it can never be played. */
|
|
2140
|
+
/** False when the reference names no bytes at all (no bundle key, address or inline text), or a bundle key that the layer's own bundle does not hold, so it can never be played. */
|
|
2113
2141
|
readonly available: boolean;
|
|
2114
2142
|
/** What this recording shows, as declared for it: its own `scope`, else its attempt's, check's, part's or layer's. Absent when none declared one: its scope is unknown, not live, physical or audible. */
|
|
2115
2143
|
readonly scope?: EvidenceScope;
|
|
@@ -2128,7 +2156,7 @@ export type VideoCounts = {
|
|
|
2128
2156
|
readonly references: number;
|
|
2129
2157
|
/** Distinct playable recordings: identical bytes referenced twice are one. */
|
|
2130
2158
|
readonly recordings: number;
|
|
2131
|
-
/** References that name no bytes. A report that predates availability metadata has none of these and none of the others. */
|
|
2159
|
+
/** References that name no bytes, or bytes the layer does not hold. A report that predates availability metadata has none of these and none of the others. */
|
|
2132
2160
|
readonly unavailable: number;
|
|
2133
2161
|
};
|
|
2134
2162
|
```
|
|
@@ -2252,6 +2280,8 @@ export type LayerRow = {
|
|
|
2252
2280
|
readonly durationMs: number | null;
|
|
2253
2281
|
/** What this result carries beyond its outcome. Absent when nothing. */
|
|
2254
2282
|
readonly evidence?: EvidenceSummary;
|
|
2283
|
+
/** 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. */
|
|
2284
|
+
readonly state?: UiStateFact;
|
|
2255
2285
|
/** 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
2286
|
readonly recording?: { readonly state: string; readonly reason?: string };
|
|
2257
2287
|
};
|
|
@@ -4581,7 +4611,9 @@ Serialises parameters as JSON key/value pairs sorted by key, so equal parameter
|
|
|
4581
4611
|
export function isDiagnosticKind(kind: string): boolean
|
|
4582
4612
|
```
|
|
4583
4613
|
|
|
4584
|
-
Whether a part `kind` only qualifies an outcome
|
|
4614
|
+
Whether a part `kind` only qualifies an outcome and so is not counted as a check when anything else is recorded: a
|
|
4615
|
+
`performance` timing, or a `ui-plan` entry (what a UI catalog expects, which a capture of the same state replaces in the
|
|
4616
|
+
tests' view). It never decides a slot: both still fill their own.
|
|
4585
4617
|
|
|
4586
4618
|
#### `isOpinionKind`
|
|
4587
4619
|
|
|
@@ -4866,7 +4898,7 @@ Reads one captured file (a screenshot, a frame, a recorded report) of a layer, o
|
|
|
4866
4898
|
export function layerSummary(layer: ReportLayer): LayerSummary
|
|
4867
4899
|
```
|
|
4868
4900
|
|
|
4869
|
-
Drops a layer's bundle index, leaving the summary composition reads.
|
|
4901
|
+
Drops a layer's bundle index, leaving the summary composition reads, plus the bundle keys its attachments name that the bundle does not hold (`unstored`), so a video that was never stored is not counted as a recording.
|
|
4870
4902
|
|
|
4871
4903
|
### Layer types
|
|
4872
4904
|
|
|
@@ -4927,6 +4959,8 @@ export type LayerSummary = LayerExtras & {
|
|
|
4927
4959
|
readonly source: SourceRecord | null;
|
|
4928
4960
|
/** Where the layer's timelines and captured files are read from: its own bundle (`layer`), an older commit-keyed recording (`commit`), or nowhere (`null`). */
|
|
4929
4961
|
readonly recording: { readonly type: 'layer' } | { readonly type: 'commit'; readonly sha: string; readonly source: string } | null;
|
|
4962
|
+
/** Bundle keys that attachments name but the layer's bundle does not hold, read from the layer's index when its summary was made (see `unstoredKeys`). Absent when there are none. In memory only: never part of the layer document. */
|
|
4963
|
+
readonly unstored?: readonly string[];
|
|
4930
4964
|
};
|
|
4931
4965
|
```
|
|
4932
4966
|
|
|
@@ -8158,6 +8192,319 @@ export async function readRunFile(store: ObjectStore, id: string, relative: stri
|
|
|
8158
8192
|
|
|
8159
8193
|
Reads one file of a run, or `null` when it is missing. Also `null`, without reading, for an `id` that is not a plain run name or a `relative` path with an empty, `.`, `..` or backslash segment, so a path can never leave the run.
|
|
8160
8194
|
|
|
8195
|
+
### Run comparison
|
|
8196
|
+
|
|
8197
|
+
Source: `packages/reports/src/run-compare.ts`. Import: `@crvouga/atlas/reports/run-compare`
|
|
8198
|
+
|
|
8199
|
+
Answers "what changed between these two runs?" without claiming more than the evidence supports. A run is a pinned
|
|
8200
|
+
source: the comparison reads two {@link RunDetail}s as they are and never reaches for a newer run. Pure functions,
|
|
8201
|
+
safe in a browser; nothing here changes a status, a slot or a winner.
|
|
8202
|
+
|
|
8203
|
+
The rules, each of which keeps a comparison from looking more certain than it is:
|
|
8204
|
+
|
|
8205
|
+
- **Identity is explicit.** Results are paired by the check they record (`TestSummary.key`: spec path and check id),
|
|
8206
|
+
then by the condition they were recorded under (kind, producer, environment, parameters). A title is never what
|
|
8207
|
+
pairs two results. A result with the same check but another condition is paired as well, marked as recorded under
|
|
8208
|
+
different conditions, and is not framed as the same experiment. Results of one side with no partner are `added`
|
|
8209
|
+
(only the candidate has them) or `removed` (only the baseline has them); a `removed` result is not a regression,
|
|
8210
|
+
because the candidate may not have run, renamed or dropped it, and the comparison cannot tell which.
|
|
8211
|
+
- **Not run is not regressed.** A result that did not run, or is still running, on either side is `unknown`; it is
|
|
8212
|
+
counted as neither a pass nor a failure. Only a completed result against a completed result is a movement.
|
|
8213
|
+
- **A changed promise is not the same experiment.** When the wording of a behavior differs between the two sides, the
|
|
8214
|
+
pair is `not-comparable` and both outcomes are shown beside the changed promise instead of as a regression or a
|
|
8215
|
+
recovery. A different spec-file revision with the same wording is disclosed beside the movement: a fingerprint is
|
|
8216
|
+
of the whole file, so it cannot say that this behavior's own promise changed. A revision that was not recorded is
|
|
8217
|
+
unknown, never the same.
|
|
8218
|
+
- **Figures are compared like for like.** Product latency and reliability are compared only when both sides were
|
|
8219
|
+
recorded under the same conditions and support the figure; otherwise the reason is given and no improvement is
|
|
8220
|
+
computed. A metric only one side has is named, not compared.
|
|
8221
|
+
|
|
8222
|
+
See docs/ui.md
|
|
8223
|
+
|
|
8224
|
+
#### `COMPARE_GROUPS`
|
|
8225
|
+
|
|
8226
|
+
```ts
|
|
8227
|
+
export const COMPARE_GROUPS = …
|
|
8228
|
+
```
|
|
8229
|
+
|
|
8230
|
+
Where a pair of results (or one result with no partner) stands, in the order a review reads them.
|
|
8231
|
+
|
|
8232
|
+
#### `CompareGroup`
|
|
8233
|
+
|
|
8234
|
+
```ts
|
|
8235
|
+
export type CompareGroup = (typeof COMPARE_GROUPS)[number];
|
|
8236
|
+
```
|
|
8237
|
+
|
|
8238
|
+
One of {@link COMPARE_GROUPS}.
|
|
8239
|
+
|
|
8240
|
+
#### `COMPARE_GROUP_TEXT`
|
|
8241
|
+
|
|
8242
|
+
```ts
|
|
8243
|
+
export const COMPARE_GROUP_TEXT: Readonly<Record<CompareGroup, { readonly label: string; readonly meaning: string }>>
|
|
8244
|
+
```
|
|
8245
|
+
|
|
8246
|
+
What each group is called and what it does and does not say.
|
|
8247
|
+
|
|
8248
|
+
#### `PromiseState`
|
|
8249
|
+
|
|
8250
|
+
```ts
|
|
8251
|
+
export type PromiseState = 'same' | 'changed' | 'revision-differs' | 'unknown';
|
|
8252
|
+
```
|
|
8253
|
+
|
|
8254
|
+
Whether the promise (what the behavior says should happen) is the same on both sides: `same` (same wording and the
|
|
8255
|
+
same recorded spec revision), `changed` (the wording differs), `revision-differs` (same wording, another spec-file
|
|
8256
|
+
revision), or `unknown` (a revision was not recorded on a side, so nothing can be said).
|
|
8257
|
+
|
|
8258
|
+
#### `PromiseReading`
|
|
8259
|
+
|
|
8260
|
+
```ts
|
|
8261
|
+
export type PromiseReading = {
|
|
8262
|
+
readonly state: PromiseState;
|
|
8263
|
+
readonly baselineTitle: string | null;
|
|
8264
|
+
readonly candidateTitle: string | null;
|
|
8265
|
+
readonly baselineRevision: string | null;
|
|
8266
|
+
readonly candidateRevision: string | null;
|
|
8267
|
+
/** In words, for a person. */
|
|
8268
|
+
readonly reasons: readonly string[];
|
|
8269
|
+
};
|
|
8270
|
+
```
|
|
8271
|
+
|
|
8272
|
+
What is known about whether the promise changed, and why.
|
|
8273
|
+
|
|
8274
|
+
#### `Condition`
|
|
8275
|
+
|
|
8276
|
+
```ts
|
|
8277
|
+
export type Condition = {
|
|
8278
|
+
readonly kind: string | null;
|
|
8279
|
+
readonly producer: string;
|
|
8280
|
+
readonly environment: string | null;
|
|
8281
|
+
readonly parameters: Readonly<Record<string, string>>;
|
|
8282
|
+
};
|
|
8283
|
+
```
|
|
8284
|
+
|
|
8285
|
+
The condition a result was recorded under: with the check, what makes it its own result.
|
|
8286
|
+
|
|
8287
|
+
#### `conditionOf`
|
|
8288
|
+
|
|
8289
|
+
```ts
|
|
8290
|
+
export function conditionOf(result: Pick<RunResult, 'kind' | 'producer' | 'environment' | 'parameters'>): Condition
|
|
8291
|
+
```
|
|
8292
|
+
|
|
8293
|
+
The condition of a result. A server that does not send `kind` leaves it null: unknown, equal only to another unknown.
|
|
8294
|
+
|
|
8295
|
+
#### `conditionId`
|
|
8296
|
+
|
|
8297
|
+
```ts
|
|
8298
|
+
export function conditionId(condition: Condition): string
|
|
8299
|
+
```
|
|
8300
|
+
|
|
8301
|
+
A stable text for a condition, equal exactly when two conditions are.
|
|
8302
|
+
|
|
8303
|
+
#### `conditionDifferences`
|
|
8304
|
+
|
|
8305
|
+
```ts
|
|
8306
|
+
export function conditionDifferences(baseline: Condition, candidate: Condition): string[]
|
|
8307
|
+
```
|
|
8308
|
+
|
|
8309
|
+
How two conditions differ, in words; empty when they are the same.
|
|
8310
|
+
|
|
8311
|
+
#### `ComparedResult`
|
|
8312
|
+
|
|
8313
|
+
```ts
|
|
8314
|
+
export type ComparedResult = {
|
|
8315
|
+
/** Identifies the pair within one comparison: the check, and the condition of each side. Stable for the same two runs. */
|
|
8316
|
+
readonly id: string;
|
|
8317
|
+
/** The check's identity (`TestSummary.key`). */
|
|
8318
|
+
readonly key: string;
|
|
8319
|
+
/** The check's title: the candidate's when it has one, else the baseline's. */
|
|
8320
|
+
readonly title: string;
|
|
8321
|
+
readonly specPath: string | null;
|
|
8322
|
+
readonly baseline: RunResult | null;
|
|
8323
|
+
readonly candidate: RunResult | null;
|
|
8324
|
+
readonly group: CompareGroup;
|
|
8325
|
+
/** One sentence saying what happened, and what is not known. */
|
|
8326
|
+
readonly summary: string;
|
|
8327
|
+
readonly promise: PromiseReading;
|
|
8328
|
+
/** How the two conditions differ, in words, when both sides have a result and they were recorded under different conditions. */
|
|
8329
|
+
readonly conditions: readonly string[];
|
|
8330
|
+
/** What a reader must not assume about this pair, in words. */
|
|
8331
|
+
readonly caveats: readonly string[];
|
|
8332
|
+
};
|
|
8333
|
+
```
|
|
8334
|
+
|
|
8335
|
+
One behavior's results on the two sides.
|
|
8336
|
+
|
|
8337
|
+
#### `RunComparison`
|
|
8338
|
+
|
|
8339
|
+
```ts
|
|
8340
|
+
export type RunComparison = {
|
|
8341
|
+
readonly baseline: RunSummary;
|
|
8342
|
+
readonly candidate: RunSummary;
|
|
8343
|
+
/** Every pair and every unpaired result, in the order of {@link COMPARE_GROUPS}, then spec and title. */
|
|
8344
|
+
readonly results: readonly ComparedResult[];
|
|
8345
|
+
readonly counts: Readonly<Record<CompareGroup, number>>;
|
|
8346
|
+
/** What a reader must not assume about the two runs as a whole: the same run on both sides, different branches, a candidate older than the baseline. */
|
|
8347
|
+
readonly notes: readonly string[];
|
|
8348
|
+
};
|
|
8349
|
+
```
|
|
8350
|
+
|
|
8351
|
+
The whole comparison of two pinned runs.
|
|
8352
|
+
|
|
8353
|
+
#### `shortRevision`
|
|
8354
|
+
|
|
8355
|
+
```ts
|
|
8356
|
+
export function shortRevision(revision: string | null | undefined): string
|
|
8357
|
+
```
|
|
8358
|
+
|
|
8359
|
+
Short form of a spec fingerprint, to show.
|
|
8360
|
+
|
|
8361
|
+
#### `recordedTitle`
|
|
8362
|
+
|
|
8363
|
+
```ts
|
|
8364
|
+
export function recordedTitle(result: Pick<RunResult, 'title' | 'recordedTitle'>): string
|
|
8365
|
+
```
|
|
8366
|
+
|
|
8367
|
+
The wording a run recorded for a check: what that run's layer called it, else the check's newest title.
|
|
8368
|
+
|
|
8369
|
+
#### `compareRuns`
|
|
8370
|
+
|
|
8371
|
+
```ts
|
|
8372
|
+
export function compareRuns(baseline: RunDetail, candidate: RunDetail): RunComparison
|
|
8373
|
+
```
|
|
8374
|
+
|
|
8375
|
+
Pairs the results of two runs and reads what changed. Results are paired by check identity and then by condition (see
|
|
8376
|
+
the module comment); titles never pair two results. Each side's results are those the run recorded last for each
|
|
8377
|
+
check and condition (`RunDetail.results`).
|
|
8378
|
+
|
|
8379
|
+
- `baseline`: The run to compare against.
|
|
8380
|
+
|
|
8381
|
+
- `candidate`: The run under review. Both are used as given: a newer run never replaces either.
|
|
8382
|
+
|
|
8383
|
+
**Returns** Every result of either run, grouped by what changed, with the reasons.
|
|
8384
|
+
|
|
8385
|
+
**Example**
|
|
8386
|
+
|
|
8387
|
+
```ts
|
|
8388
|
+
const comparison = compareRuns(await runDetail('run:ci-100'), await runDetail('run:ci-101'));
|
|
8389
|
+
comparison.results.filter(item => item.group === 'regressed');
|
|
8390
|
+
```
|
|
8391
|
+
|
|
8392
|
+
#### `MetricComparison`
|
|
8393
|
+
|
|
8394
|
+
```ts
|
|
8395
|
+
export type MetricComparison =
|
|
8396
|
+
| { readonly state: 'compared'; readonly name: string; readonly baseline: LatencySummary; readonly candidate: LatencySummary; readonly change: LatencyChange; readonly reasons: readonly string[] }
|
|
8397
|
+
| { readonly state: 'incompatible'; readonly name: string; readonly baseline: LatencySummary; readonly candidate: LatencySummary; readonly change: LatencyChange | null; readonly reasons: readonly string[] }
|
|
8398
|
+
| { readonly state: 'baseline-only' | 'candidate-only'; readonly name: string; readonly summary: LatencySummary; readonly reasons: readonly string[] };
|
|
8399
|
+
```
|
|
8400
|
+
|
|
8401
|
+
What one product-latency metric shows on the two sides.
|
|
8402
|
+
|
|
8403
|
+
#### `compareProductLatency`
|
|
8404
|
+
|
|
8405
|
+
```ts
|
|
8406
|
+
export function compareProductLatency(baseline: readonly LatencyEntry[], candidate: readonly LatencyEntry[], conditionsDiffer: readonly string[] = []): MetricComparison[]
|
|
8407
|
+
```
|
|
8408
|
+
|
|
8409
|
+
Compares the product latency recorded in two sets of results of one check, one per side. A metric is compared only
|
|
8410
|
+
when it is the same measurement (name, unit, boundaries) and both sides were measured on the same devices, in the
|
|
8411
|
+
same environments, with the same instrumentation and other conditions; otherwise it is `incompatible` and no change
|
|
8412
|
+
is computed, with the reasons. A metric only one side recorded is named as such. A side without any latency is unknown,
|
|
8413
|
+
not fast: it contributes no metric. `conditionsDiffer` names differences of the results' own conditions, which make
|
|
8414
|
+
every metric incompatible.
|
|
8415
|
+
|
|
8416
|
+
#### `ReliabilityComparison`
|
|
8417
|
+
|
|
8418
|
+
```ts
|
|
8419
|
+
export type ReliabilityComparison = {
|
|
8420
|
+
/** True only when both figures are established, were counted in the same scope and against the same recorded spec revision. */
|
|
8421
|
+
readonly comparable: boolean;
|
|
8422
|
+
readonly baseline: ReliabilityRate;
|
|
8423
|
+
readonly candidate: ReliabilityRate;
|
|
8424
|
+
/** Candidate minus baseline, in percentage points, only when comparable. */
|
|
8425
|
+
readonly points?: number;
|
|
8426
|
+
/** `higher` or `lower` only when the two intervals do not overlap; `indistinguishable` when they do. Absent when not comparable. */
|
|
8427
|
+
readonly direction?: 'higher' | 'lower' | 'indistinguishable';
|
|
8428
|
+
/** Why no change is stated, or what qualifies one. */
|
|
8429
|
+
readonly reasons: readonly string[];
|
|
8430
|
+
};
|
|
8431
|
+
```
|
|
8432
|
+
|
|
8433
|
+
How reliable a check was on each side, and whether the two figures can be set against each other.
|
|
8434
|
+
|
|
8435
|
+
#### `reliabilityScopeAt`
|
|
8436
|
+
|
|
8437
|
+
```ts
|
|
8438
|
+
export function reliabilityScopeAt(result: Pick<RunResult, 'environment' | 'producer' | 'parameters' | 'specRevision'>, run: Pick<RunSummary, 'endedAtMs'>): ReliabilityScope
|
|
8439
|
+
```
|
|
8440
|
+
|
|
8441
|
+
The scope that counts the independent executions of one result's check up to the end of its run: the same
|
|
8442
|
+
environment, producer and parameters, and the spec revision that result was recorded against (executions against
|
|
8443
|
+
another revision are other promises). A result that recorded no revision is counted across revisions, which
|
|
8444
|
+
{@link compareReliability} will not compare. Use it for each side, with that side's result and run.
|
|
8445
|
+
|
|
8446
|
+
#### `compareReliability`
|
|
8447
|
+
|
|
8448
|
+
```ts
|
|
8449
|
+
export function compareReliability(baseline: ReliabilityDocument, candidate: ReliabilityDocument, conditionsDiffer: readonly string[] = []): ReliabilityComparison
|
|
8450
|
+
```
|
|
8451
|
+
|
|
8452
|
+
Sets the reliability of a check up to the baseline run against its reliability up to the candidate run. A change is
|
|
8453
|
+
stated only when both samples are established (at least `minimumExecutions` independent executions), both counted the
|
|
8454
|
+
same recorded spec revision (see {@link reliabilityScopeAt}) and the conditions match; whether the difference exceeds the uncertainty is
|
|
8455
|
+
said by `direction`. Otherwise each figure stands alone and the reasons say why.
|
|
8456
|
+
|
|
8457
|
+
#### `StepLike`
|
|
8458
|
+
|
|
8459
|
+
```ts
|
|
8460
|
+
export type StepLike = { readonly name: string; readonly offsetMs?: number | undefined };
|
|
8461
|
+
```
|
|
8462
|
+
|
|
8463
|
+
The part of a recorded step this module reads.
|
|
8464
|
+
|
|
8465
|
+
#### `AlignedStep`
|
|
8466
|
+
|
|
8467
|
+
```ts
|
|
8468
|
+
export type AlignedStep = {
|
|
8469
|
+
readonly name: string;
|
|
8470
|
+
/** Milliseconds after the check began, on each side; absent when that side did not record it or its time. */
|
|
8471
|
+
readonly baselineOffsetMs?: number;
|
|
8472
|
+
readonly candidateOffsetMs?: number;
|
|
8473
|
+
readonly onBaseline: boolean;
|
|
8474
|
+
readonly onCandidate: boolean;
|
|
8475
|
+
};
|
|
8476
|
+
```
|
|
8477
|
+
|
|
8478
|
+
One step, found on one or both sides.
|
|
8479
|
+
|
|
8480
|
+
#### `StepAlignment`
|
|
8481
|
+
|
|
8482
|
+
```ts
|
|
8483
|
+
export type StepAlignment = {
|
|
8484
|
+
/** `aligned`: some step is on both sides with a time on both (a recording can be sought to it); `unaligned`: otherwise. */
|
|
8485
|
+
readonly state: 'aligned' | 'unaligned';
|
|
8486
|
+
readonly steps: readonly AlignedStep[];
|
|
8487
|
+
/** The steps with a time on both sides: where both recordings can be sought together. */
|
|
8488
|
+
readonly seekable: readonly (AlignedStep & { readonly baselineOffsetMs: number; readonly candidateOffsetMs: number })[];
|
|
8489
|
+
/** Whether the two lists of step names differ (the steps themselves are part of the promise). */
|
|
8490
|
+
readonly differs: boolean;
|
|
8491
|
+
/** In words, why the recordings are or are not aligned. */
|
|
8492
|
+
readonly explanation: string;
|
|
8493
|
+
};
|
|
8494
|
+
```
|
|
8495
|
+
|
|
8496
|
+
How the recorded steps of two results line up.
|
|
8497
|
+
|
|
8498
|
+
#### `alignSteps`
|
|
8499
|
+
|
|
8500
|
+
```ts
|
|
8501
|
+
export function alignSteps(baseline: readonly StepLike[], candidate: readonly StepLike[]): StepAlignment
|
|
8502
|
+
```
|
|
8503
|
+
|
|
8504
|
+
Lines up the recorded steps of two results by name (the n-th step of a name on one side with the n-th of that name on
|
|
8505
|
+
the other), keeping each side's own time origin: an offset is milliseconds after that side's check began. Nothing is
|
|
8506
|
+
aligned by position or by fuzzy wording. When nothing lines up, `explanation` says why.
|
|
8507
|
+
|
|
8161
8508
|
### S3 object store
|
|
8162
8509
|
|
|
8163
8510
|
Source: `packages/reports/src/s3-store.ts`. Import: `@crvouga/atlas/reports/s3-store`
|
|
@@ -9616,6 +9963,14 @@ export type RunResult = {
|
|
|
9616
9963
|
readonly detail: string;
|
|
9617
9964
|
/** Every video its evidence references, with who holds it (check, retry attempt or step). Absent when there is none. */
|
|
9618
9965
|
readonly videos?: readonly VideoRef[];
|
|
9966
|
+
/** The kind of the part that held it (`behavior`, `checks`…). With the producer, environment and parameters it is the condition the result was recorded under. Absent from an older server. */
|
|
9967
|
+
readonly kind?: string;
|
|
9968
|
+
/** The other choices that made it its own result (a device size, a locale…). Absent when there are none, and from an older server. */
|
|
9969
|
+
readonly parameters?: Readonly<Record<string, string>>;
|
|
9970
|
+
/** The fingerprint of its spec file the layer recorded: results with different ones were recorded against different revisions of the spec. Absent means not recorded, never "the same". */
|
|
9971
|
+
readonly specRevision?: string;
|
|
9972
|
+
/** The title this run's layer gave the check. `title` is the newest title the check has in the whole history, so a check that was reworded shows its newest wording in both runs; this is the wording as recorded. Absent from an older server. */
|
|
9973
|
+
readonly recordedTitle?: string;
|
|
9619
9974
|
};
|
|
9620
9975
|
```
|
|
9621
9976
|
|
|
@@ -9982,6 +10337,589 @@ export type ReportData = HtmlReportBundle & {
|
|
|
9982
10337
|
|
|
9983
10338
|
A run's tests (`HtmlReportBundle`) plus the document title shown for them, for the standalone / Solid report viewer.
|
|
9984
10339
|
|
|
10340
|
+
### Writing a UI catalog from any collector
|
|
10341
|
+
|
|
10342
|
+
Source: `packages/reports/src/ui-catalog-publish.ts`. Import: `@crvouga/atlas/reports/ui-catalog-publish`
|
|
10343
|
+
|
|
10344
|
+
Two small builders that turn what a collector captured, or what a planner expects, into the input of `publishChecks`
|
|
10345
|
+
(and of `atlas report publish --checks`), following the convention in {@link ./ui-state}. They are plain data in and plain
|
|
10346
|
+
data out: no browser, no test framework, no file system. A Playwright test, a Cypress run, a mobile device lab, a Storybook
|
|
10347
|
+
crawler or a shell script that writes the same JSON all produce the same catalog.
|
|
10348
|
+
|
|
10349
|
+
Shards need no coordination: each shard calls {@link uiStateLayer} for the states it captured (and optionally
|
|
10350
|
+
{@link uiPlanLayer} for the states it was given), with its own `shard` label, and publishes. A planner that knows the whole
|
|
10351
|
+
product publishes the plan once.
|
|
10352
|
+
|
|
10353
|
+
See docs/ui-catalog.md
|
|
10354
|
+
|
|
10355
|
+
#### `UiPicture`
|
|
10356
|
+
|
|
10357
|
+
```ts
|
|
10358
|
+
export type UiPicture = {
|
|
10359
|
+
/** The file name; default `<id>.png`. */
|
|
10360
|
+
readonly name?: string;
|
|
10361
|
+
readonly data?: Uint8Array;
|
|
10362
|
+
readonly base64?: string;
|
|
10363
|
+
readonly uri?: string;
|
|
10364
|
+
/** Default `image/png`. */
|
|
10365
|
+
readonly contentType?: string;
|
|
10366
|
+
readonly width?: number;
|
|
10367
|
+
readonly height?: number;
|
|
10368
|
+
};
|
|
10369
|
+
```
|
|
10370
|
+
|
|
10371
|
+
A picture to store with a capture: its bytes (`data`, `base64`), or where it already is (`uri`).
|
|
10372
|
+
|
|
10373
|
+
#### `UiStateCapture`
|
|
10374
|
+
|
|
10375
|
+
```ts
|
|
10376
|
+
export type UiStateCapture = UiStateFacts & {
|
|
10377
|
+
readonly id: string;
|
|
10378
|
+
/** For a person; default the id. */
|
|
10379
|
+
readonly title?: string;
|
|
10380
|
+
/** `passed` (default): captured. `failed`: the capture was attempted and did not work (say why in `detail`). `running`: being taken. `not-run`: skipped. */
|
|
10381
|
+
readonly status?: 'passed' | 'failed' | 'running' | 'not-run';
|
|
10382
|
+
/** Why it failed or was skipped: a readiness check that never became true, a console error. */
|
|
10383
|
+
readonly detail?: string;
|
|
10384
|
+
readonly screenshot?: UiPicture;
|
|
10385
|
+
readonly durationMs?: number;
|
|
10386
|
+
/** More facts about the capture that never split it (`browser`, `os`). */
|
|
10387
|
+
readonly labels?: Readonly<Record<string, string>>;
|
|
10388
|
+
readonly tags?: readonly string[];
|
|
10389
|
+
};
|
|
10390
|
+
```
|
|
10391
|
+
|
|
10392
|
+
One captured state. `id` must be stable across runs: it is how a new capture replaces an old one.
|
|
10393
|
+
|
|
10394
|
+
#### `UiStatePlan`
|
|
10395
|
+
|
|
10396
|
+
```ts
|
|
10397
|
+
export type UiStatePlan = UiStateFacts & {
|
|
10398
|
+
readonly id: string;
|
|
10399
|
+
readonly title?: string;
|
|
10400
|
+
};
|
|
10401
|
+
```
|
|
10402
|
+
|
|
10403
|
+
One expected state, or (with `inventory`) one surface of the product's inventory.
|
|
10404
|
+
|
|
10405
|
+
#### `UiLayerOrigin`
|
|
10406
|
+
|
|
10407
|
+
```ts
|
|
10408
|
+
export type UiLayerOrigin = {
|
|
10409
|
+
/** The suite or tool that makes the layer. Part of the slot; a planner and a collector normally differ. */
|
|
10410
|
+
readonly producer: string;
|
|
10411
|
+
/** When the captures were taken (or the plan was made), epoch milliseconds. Take it from the run, not from the moment of publishing. */
|
|
10412
|
+
readonly recordedAtMs: number;
|
|
10413
|
+
/** The client: `desktop-web`, `mobile-web`, `ios`, `android`… Part of the slot. */
|
|
10414
|
+
readonly environment?: string;
|
|
10415
|
+
/** The rest of the profile, such as `{ viewport: '390x844', scheme: 'dark' }`. Each combination is its own result. Part of the slot. */
|
|
10416
|
+
readonly profile?: Readonly<Record<string, string>>;
|
|
10417
|
+
readonly sha?: string | null;
|
|
10418
|
+
readonly branch?: string | null;
|
|
10419
|
+
readonly runUrl?: string | null;
|
|
10420
|
+
/** Which shard this is (`2/4`, `checkout`, a machine name). Shown with each capture; never part of the slot. */
|
|
10421
|
+
readonly shard?: string;
|
|
10422
|
+
/** The execution these layers belong to, so shards of one run stay one run. */
|
|
10423
|
+
readonly run?: { readonly id: string; readonly title?: string };
|
|
10424
|
+
};
|
|
10425
|
+
```
|
|
10426
|
+
|
|
10427
|
+
What every layer of a catalog says about where and when it was recorded.
|
|
10428
|
+
|
|
10429
|
+
#### `uiStateLayer`
|
|
10430
|
+
|
|
10431
|
+
```ts
|
|
10432
|
+
export function uiStateLayer(input: UiLayerOrigin & { readonly states: readonly UiStateCapture[] }): PublishChecksInput
|
|
10433
|
+
```
|
|
10434
|
+
|
|
10435
|
+
The input of `publishChecks` for the states one collector (or one shard) captured under one profile. Throws when a state id
|
|
10436
|
+
repeats, because the second would silently replace the first. A state without a `status` is `passed`: it was captured.
|
|
10437
|
+
|
|
10438
|
+
**Example**
|
|
10439
|
+
|
|
10440
|
+
```ts
|
|
10441
|
+
await publishChecks(store, uiStateLayer({
|
|
10442
|
+
producer: 'web-capture', recordedAtMs: Date.now(), environment: 'mobile-web', profile: { viewport: '390x844', scheme: 'dark' }, shard: '1/4',
|
|
10443
|
+
states: [{ id: 'checkout/payment', title: 'Payment', route: '/checkout', covers: ['payment-form'], screenshot: { data: png } }]
|
|
10444
|
+
}));
|
|
10445
|
+
```
|
|
10446
|
+
|
|
10447
|
+
#### `uiPlanLayer`
|
|
10448
|
+
|
|
10449
|
+
```ts
|
|
10450
|
+
export function uiPlanLayer(input: UiLayerOrigin & { readonly states: readonly UiStatePlan[] }): PublishChecksInput
|
|
10451
|
+
```
|
|
10452
|
+
|
|
10453
|
+
The input of `publishChecks` for what is expected: states that should be captured under this profile and, with `inventory`,
|
|
10454
|
+
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
|
|
10455
|
+
its own (kind `ui-plan`), so a capture never erases it and it never hides a capture. Throws when an id repeats.
|
|
10456
|
+
|
|
10457
|
+
### UI catalog
|
|
10458
|
+
|
|
10459
|
+
Source: `packages/reports/src/ui-catalog.ts`. Import: `@crvouga/atlas/reports/ui-catalog`
|
|
10460
|
+
|
|
10461
|
+
A browsable inventory of a product's user interface, computed from layers and nothing else: each **state** (a screen, or
|
|
10462
|
+
one state of a screen) captured once per **profile** (viewport, color scheme, device), with the facts needed to find and
|
|
10463
|
+
trust it, and what should exist but has not been captured yet. `uiCatalog` is a pure function of the winning results of
|
|
10464
|
+
the layers it is given, so it inherits the report contract's laws: the same layers in any order, split any way across
|
|
10465
|
+
backends, shards and machines, give the same catalog; a layer changes only the cells it fills; an unreadable layer is
|
|
10466
|
+
absent, never captured. It imports no collection framework and knows nothing about how a picture was taken.
|
|
10467
|
+
|
|
10468
|
+
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
|
|
10469
|
+
is the layer's `environment` and `parameters`, the expected set is a `ui-plan` part. The viewer computes the catalog from
|
|
10470
|
+
the report it already streams (so it fills in as results arrive), and the server answers the same document at
|
|
10471
|
+
`GET /api/reports/ui-catalog`.
|
|
10472
|
+
|
|
10473
|
+
See docs/ui-catalog.md
|
|
10474
|
+
See docs/report-contract.md#how-layers-compose
|
|
10475
|
+
|
|
10476
|
+
#### `UI_CATALOG_VERSION`
|
|
10477
|
+
|
|
10478
|
+
```ts
|
|
10479
|
+
export const UI_CATALOG_VERSION = …
|
|
10480
|
+
```
|
|
10481
|
+
|
|
10482
|
+
The `schemaVersion` of the catalog document.
|
|
10483
|
+
|
|
10484
|
+
#### `UiProfile`
|
|
10485
|
+
|
|
10486
|
+
```ts
|
|
10487
|
+
export type UiProfile = {
|
|
10488
|
+
/** Stable identity: the environment and the sorted parameters. */
|
|
10489
|
+
readonly key: string;
|
|
10490
|
+
/** The client (`desktop-web`, `ios`…), or null when the layer named none. */
|
|
10491
|
+
readonly environment: string | null;
|
|
10492
|
+
/** The layer's other choices, such as `{ viewport: '390x844', scheme: 'dark' }`. */
|
|
10493
|
+
readonly parameters: Readonly<Record<string, string>>;
|
|
10494
|
+
/** For a person: the client and each parameter's value. */
|
|
10495
|
+
readonly label: string;
|
|
10496
|
+
};
|
|
10497
|
+
```
|
|
10498
|
+
|
|
10499
|
+
One profile: the client and the choices a state was captured under. Its `key` identifies it across layers.
|
|
10500
|
+
|
|
10501
|
+
#### `UiCoverage`
|
|
10502
|
+
|
|
10503
|
+
```ts
|
|
10504
|
+
export type UiCoverage = 'captured' | 'failed' | 'capturing' | 'skipped' | 'missing';
|
|
10505
|
+
```
|
|
10506
|
+
|
|
10507
|
+
How one state stands in one profile. `captured`: a capture passed. `failed`: it was attempted and failed (its `detail`
|
|
10508
|
+
says why). `capturing`: it is being taken now. `skipped`: a producer recorded it as not run. `missing`: expected, and
|
|
10509
|
+
nothing has been recorded. Only `captured` is ever shown as captured; every other value means there is no picture to trust.
|
|
10510
|
+
|
|
10511
|
+
#### `UI_COVERAGE`
|
|
10512
|
+
|
|
10513
|
+
```ts
|
|
10514
|
+
export const UI_COVERAGE = …
|
|
10515
|
+
```
|
|
10516
|
+
|
|
10517
|
+
The values of {@link UiCoverage}, in the order a person reads them.
|
|
10518
|
+
|
|
10519
|
+
#### `UiImage`
|
|
10520
|
+
|
|
10521
|
+
```ts
|
|
10522
|
+
export type UiImage = UiScreenshot & {
|
|
10523
|
+
/** Where the stored bytes are, relative to the reports API root (`/layers/<layer>/blobs/attachments/<sha256>`). Absent for an address-only or omitted picture. */
|
|
10524
|
+
readonly path?: string;
|
|
10525
|
+
};
|
|
10526
|
+
```
|
|
10527
|
+
|
|
10528
|
+
A picture, with the address a browser loads it from.
|
|
10529
|
+
|
|
10530
|
+
#### `UiOrigin`
|
|
10531
|
+
|
|
10532
|
+
```ts
|
|
10533
|
+
export type UiOrigin = {
|
|
10534
|
+
readonly layer: string;
|
|
10535
|
+
readonly backend: string;
|
|
10536
|
+
readonly producer: string;
|
|
10537
|
+
readonly recordedAtMs: number;
|
|
10538
|
+
readonly sha: string | null;
|
|
10539
|
+
readonly branch: string | null;
|
|
10540
|
+
readonly runUrl: string | null;
|
|
10541
|
+
readonly freshness?: 'unknown' | 'modified';
|
|
10542
|
+
};
|
|
10543
|
+
```
|
|
10544
|
+
|
|
10545
|
+
Where a capture came from: the layer, the backend that holds it, and the provenance the layer recorded.
|
|
10546
|
+
|
|
10547
|
+
#### `UiCapture`
|
|
10548
|
+
|
|
10549
|
+
```ts
|
|
10550
|
+
export type UiCapture = {
|
|
10551
|
+
readonly status: EvidenceStatus;
|
|
10552
|
+
/** Why it failed or was skipped; empty otherwise. */
|
|
10553
|
+
readonly detail: string;
|
|
10554
|
+
readonly durationMs: number | null;
|
|
10555
|
+
/** The picture, or null when the capture has none (or it was left out: see `omitted`). */
|
|
10556
|
+
readonly image: UiImage | null;
|
|
10557
|
+
/** The check's labels: what describes the capture without splitting it (a `shard`, a `browser`). */
|
|
10558
|
+
readonly labels: Readonly<Record<string, string>>;
|
|
10559
|
+
readonly origin: UiOrigin;
|
|
10560
|
+
/** How many other captures of this cell exist from other producers. The winner is the one the report contract picks. */
|
|
10561
|
+
readonly alternatives: number;
|
|
10562
|
+
};
|
|
10563
|
+
```
|
|
10564
|
+
|
|
10565
|
+
The winning capture of one cell.
|
|
10566
|
+
|
|
10567
|
+
#### `UiCell`
|
|
10568
|
+
|
|
10569
|
+
```ts
|
|
10570
|
+
export type UiCell = {
|
|
10571
|
+
readonly profile: UiProfile;
|
|
10572
|
+
readonly coverage: UiCoverage;
|
|
10573
|
+
/** 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. */
|
|
10574
|
+
readonly expected: 'declared' | 'undeclared' | 'unknown';
|
|
10575
|
+
readonly capture: UiCapture | null;
|
|
10576
|
+
/** The layer of the plan that expects it, when one does. */
|
|
10577
|
+
readonly planned: { readonly layer: string; readonly backend: string } | null;
|
|
10578
|
+
};
|
|
10579
|
+
```
|
|
10580
|
+
|
|
10581
|
+
One state in one profile.
|
|
10582
|
+
|
|
10583
|
+
#### `UiCatalogState`
|
|
10584
|
+
|
|
10585
|
+
```ts
|
|
10586
|
+
export type UiCatalogState = {
|
|
10587
|
+
/** The check id the producers gave it. */
|
|
10588
|
+
readonly id: string;
|
|
10589
|
+
readonly title: string;
|
|
10590
|
+
readonly route: string | null;
|
|
10591
|
+
readonly fixture: string | null;
|
|
10592
|
+
readonly cohort: string | null;
|
|
10593
|
+
readonly area: string | null;
|
|
10594
|
+
readonly flags: readonly string[];
|
|
10595
|
+
/** The surfaces it covers. */
|
|
10596
|
+
readonly covers: readonly string[];
|
|
10597
|
+
/** The producers that captured or planned it. */
|
|
10598
|
+
readonly producers: readonly string[];
|
|
10599
|
+
/** One cell per profile, ordered by the profile's label. */
|
|
10600
|
+
readonly cells: readonly UiCell[];
|
|
10601
|
+
};
|
|
10602
|
+
```
|
|
10603
|
+
|
|
10604
|
+
One state of the product's interface, across its profiles.
|
|
10605
|
+
|
|
10606
|
+
#### `UiSurface`
|
|
10607
|
+
|
|
10608
|
+
```ts
|
|
10609
|
+
export type UiSurface = {
|
|
10610
|
+
readonly id: string;
|
|
10611
|
+
/** True when a `ui-plan` inventory entry names it: the product is known to have it. */
|
|
10612
|
+
readonly inventoried: boolean;
|
|
10613
|
+
/** States that cover it. */
|
|
10614
|
+
readonly states: number;
|
|
10615
|
+
/** Of those, states with at least one captured profile. */
|
|
10616
|
+
readonly captured: number;
|
|
10617
|
+
/** `covered`: some covering state is captured. `uncaptured`: states cover it and none is captured yet. `uncovered`: no state covers it. */
|
|
10618
|
+
readonly status: 'covered' | 'uncaptured' | 'uncovered';
|
|
10619
|
+
};
|
|
10620
|
+
```
|
|
10621
|
+
|
|
10622
|
+
One surface of the product: an inventory entry, or something a state says it covers.
|
|
10623
|
+
|
|
10624
|
+
#### `UiFacetValue`
|
|
10625
|
+
|
|
10626
|
+
```ts
|
|
10627
|
+
export type UiFacetValue = { readonly value: string; readonly count: number; readonly label?: string };
|
|
10628
|
+
```
|
|
10629
|
+
|
|
10630
|
+
A value of a facet and how many states have it.
|
|
10631
|
+
|
|
10632
|
+
#### `UiCatalogFacets`
|
|
10633
|
+
|
|
10634
|
+
```ts
|
|
10635
|
+
export type UiCatalogFacets = {
|
|
10636
|
+
readonly profiles: readonly UiFacetValue[];
|
|
10637
|
+
readonly coverage: readonly UiFacetValue[];
|
|
10638
|
+
readonly areas: readonly UiFacetValue[];
|
|
10639
|
+
readonly routes: readonly UiFacetValue[];
|
|
10640
|
+
readonly cohorts: readonly UiFacetValue[];
|
|
10641
|
+
readonly flags: readonly UiFacetValue[];
|
|
10642
|
+
readonly surfaces: readonly UiFacetValue[];
|
|
10643
|
+
readonly producers: readonly UiFacetValue[];
|
|
10644
|
+
};
|
|
10645
|
+
```
|
|
10646
|
+
|
|
10647
|
+
The values each filter offers, over every state of the catalog.
|
|
10648
|
+
|
|
10649
|
+
#### `UiCatalogSummary`
|
|
10650
|
+
|
|
10651
|
+
```ts
|
|
10652
|
+
export type UiCatalogSummary = {
|
|
10653
|
+
readonly states: number;
|
|
10654
|
+
readonly profiles: number;
|
|
10655
|
+
readonly cells: number;
|
|
10656
|
+
readonly coverage: Readonly<Record<UiCoverage, number>>;
|
|
10657
|
+
/** Captured or failed cells no plan expects. Zero when no plan was published, because then nothing is known to be unexpected. */
|
|
10658
|
+
readonly undeclared: number;
|
|
10659
|
+
/** Whether any plan (an expected set) was published. Without one, `missing` can only ever be zero. */
|
|
10660
|
+
readonly planned: boolean;
|
|
10661
|
+
readonly surfaces: number;
|
|
10662
|
+
readonly uncoveredSurfaces: number;
|
|
10663
|
+
};
|
|
10664
|
+
```
|
|
10665
|
+
|
|
10666
|
+
The catalog in numbers. Cells count a state once per profile.
|
|
10667
|
+
|
|
10668
|
+
#### `UiCatalog`
|
|
10669
|
+
|
|
10670
|
+
```ts
|
|
10671
|
+
export type UiCatalog = {
|
|
10672
|
+
readonly schemaVersion: typeof UI_CATALOG_VERSION;
|
|
10673
|
+
readonly version?: string;
|
|
10674
|
+
readonly summary: UiCatalogSummary;
|
|
10675
|
+
readonly profiles: readonly UiProfile[];
|
|
10676
|
+
readonly states: readonly UiCatalogState[];
|
|
10677
|
+
readonly surfaces: readonly UiSurface[];
|
|
10678
|
+
readonly facets: UiCatalogFacets;
|
|
10679
|
+
/** 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. */
|
|
10680
|
+
readonly sources: { readonly layers: number; readonly backends: readonly BackendInfo[]; readonly notices: readonly string[] };
|
|
10681
|
+
};
|
|
10682
|
+
```
|
|
10683
|
+
|
|
10684
|
+
The catalog, as `GET /api/reports/ui-catalog` answers it (`version` is the report's, when the caller knows it).
|
|
10685
|
+
|
|
10686
|
+
#### `UiCatalogOptions`
|
|
10687
|
+
|
|
10688
|
+
```ts
|
|
10689
|
+
export type UiCatalogOptions = {
|
|
10690
|
+
/** The report's version, repeated in the document. */
|
|
10691
|
+
readonly version?: string;
|
|
10692
|
+
/** The backends, with whether each could be read. */
|
|
10693
|
+
readonly backends?: readonly BackendInfo[];
|
|
10694
|
+
/** What could not be read. */
|
|
10695
|
+
readonly notices?: readonly string[];
|
|
10696
|
+
};
|
|
10697
|
+
```
|
|
10698
|
+
|
|
10699
|
+
What a caller may say about how the layers were read.
|
|
10700
|
+
|
|
10701
|
+
#### `uiProfile`
|
|
10702
|
+
|
|
10703
|
+
```ts
|
|
10704
|
+
export function uiProfile(layer: Pick<LayerHeader, 'environment' | 'parameters'>): UiProfile
|
|
10705
|
+
```
|
|
10706
|
+
|
|
10707
|
+
The profile of a layer: its environment and its other parameters.
|
|
10708
|
+
|
|
10709
|
+
#### `uiCatalog`
|
|
10710
|
+
|
|
10711
|
+
```ts
|
|
10712
|
+
export function uiCatalog(layers: readonly LayerEvidence[], options: UiCatalogOptions = {}): UiCatalog
|
|
10713
|
+
```
|
|
10714
|
+
|
|
10715
|
+
The UI catalog of some layers (as the effective report holds them, or any set of {@link LayerEvidence}).
|
|
10716
|
+
|
|
10717
|
+
Only rows of `ui-state` and `ui-plan` parts take part. The winner of every slot is decided by the report contract
|
|
10718
|
+
(`combineEvidence`), so the result does not depend on the order of `layers` or on how they are divided among backends. A
|
|
10719
|
+
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
|
|
10720
|
+
capture passed; a plan that nothing has captured is `missing`. When no plan exists nothing is `missing` and nothing is
|
|
10721
|
+
`undeclared`: what is expected is then unknown, and the catalog says so (`summary.planned`).
|
|
10722
|
+
|
|
10723
|
+
**Example**
|
|
10724
|
+
|
|
10725
|
+
```ts
|
|
10726
|
+
const catalog = uiCatalog(effective.layers, { version: effective.version, backends: effective.backends });
|
|
10727
|
+
catalog.states.map(state => [state.id, state.cells.map(cell => cell.coverage)]);
|
|
10728
|
+
```
|
|
10729
|
+
|
|
10730
|
+
#### `UiCatalogQuery`
|
|
10731
|
+
|
|
10732
|
+
```ts
|
|
10733
|
+
export type UiCatalogQuery = {
|
|
10734
|
+
/** Words (all of them) found in the id, title, route, fixture, cohort, area, flags, surfaces or producers. */
|
|
10735
|
+
readonly q?: string;
|
|
10736
|
+
readonly coverage?: readonly UiCoverage[];
|
|
10737
|
+
/** Profile keys, as {@link UiProfile.key}. */
|
|
10738
|
+
readonly profile?: readonly string[];
|
|
10739
|
+
readonly area?: readonly string[];
|
|
10740
|
+
readonly route?: readonly string[];
|
|
10741
|
+
readonly cohort?: readonly string[];
|
|
10742
|
+
readonly flag?: readonly string[];
|
|
10743
|
+
/** Surfaces a state covers. */
|
|
10744
|
+
readonly surface?: readonly string[];
|
|
10745
|
+
readonly producer?: readonly string[];
|
|
10746
|
+
};
|
|
10747
|
+
```
|
|
10748
|
+
|
|
10749
|
+
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.
|
|
10750
|
+
|
|
10751
|
+
#### `parseUiCatalogQuery`
|
|
10752
|
+
|
|
10753
|
+
```ts
|
|
10754
|
+
export function parseUiCatalogQuery(params: URLSearchParams): UiCatalogQuery
|
|
10755
|
+
```
|
|
10756
|
+
|
|
10757
|
+
Reads a query from URL search parameters (`q`, and the comma-separated {@link UiCatalogQuery} lists). Unknown coverage values are left out, never an error.
|
|
10758
|
+
|
|
10759
|
+
#### `uiCatalogParams`
|
|
10760
|
+
|
|
10761
|
+
```ts
|
|
10762
|
+
export function uiCatalogParams(query: UiCatalogQuery): URLSearchParams
|
|
10763
|
+
```
|
|
10764
|
+
|
|
10765
|
+
A query as URL search parameters, the form {@link parseUiCatalogQuery} reads back.
|
|
10766
|
+
|
|
10767
|
+
#### `isUiCatalogFiltered`
|
|
10768
|
+
|
|
10769
|
+
```ts
|
|
10770
|
+
export function isUiCatalogFiltered(query: UiCatalogQuery): boolean
|
|
10771
|
+
```
|
|
10772
|
+
|
|
10773
|
+
Whether a query narrows anything.
|
|
10774
|
+
|
|
10775
|
+
#### `filterUiCatalog`
|
|
10776
|
+
|
|
10777
|
+
```ts
|
|
10778
|
+
export function filterUiCatalog(catalog: UiCatalog, query: UiCatalogQuery): UiCatalog
|
|
10779
|
+
```
|
|
10780
|
+
|
|
10781
|
+
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
|
|
10782
|
+
kept when they match the `profile` and `coverage` filters, and a state left with no cell is dropped. The summary is
|
|
10783
|
+
recomputed for what is kept; the profiles, surfaces, facets and sources stay those of the whole catalog, so the
|
|
10784
|
+
controls keep offering every choice.
|
|
10785
|
+
|
|
10786
|
+
#### `pageUiCatalog`
|
|
10787
|
+
|
|
10788
|
+
```ts
|
|
10789
|
+
export function pageUiCatalog(catalog: UiCatalog, page: { readonly limit?: number; readonly offset?: number }): UiCatalog & { readonly total: number; readonly offset: number; readonly limit: number }
|
|
10790
|
+
```
|
|
10791
|
+
|
|
10792
|
+
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.
|
|
10793
|
+
|
|
10794
|
+
#### `uiCatalogMarkdown`
|
|
10795
|
+
|
|
10796
|
+
```ts
|
|
10797
|
+
export function uiCatalogMarkdown(catalog: UiCatalog): string
|
|
10798
|
+
```
|
|
10799
|
+
|
|
10800
|
+
The catalog as Markdown for a person or an agent reading text: the summary in words, the surfaces that are not covered, and
|
|
10801
|
+
one line per state with its coverage in each profile. Only `captured` is called captured.
|
|
10802
|
+
|
|
10803
|
+
### UI state conventions
|
|
10804
|
+
|
|
10805
|
+
Source: `packages/reports/src/ui-state.ts`. Import: `@crvouga/atlas/reports/ui-state`
|
|
10806
|
+
|
|
10807
|
+
How a captured UI state is written in a layer, using nothing but what the layer format already has. A **state** is a
|
|
10808
|
+
check in a part of kind `ui-state`: its `id` names the state, its `status` says how the capture went, a `screenshot`
|
|
10809
|
+
attachment is the picture, and the facts the producer declares about it (route, fixture, cohort, flags, the surfaces it
|
|
10810
|
+
covers) sit under the `atlas.ui-state` extension. A **profile** (viewport, color scheme, device) is the layer's
|
|
10811
|
+
`environment` and `parameters`, so a state captured at another viewport is another result, side by side. What is
|
|
10812
|
+
**expected** is a layer of kind `ui-plan` with the same ids and profiles, every check `not-run`.
|
|
10813
|
+
|
|
10814
|
+
Nothing here decides a result: a status, a slot and a winner are what the report contract says they are. This module
|
|
10815
|
+
only reads the convention into a small optional row field ({@link UiStateFact}) so a viewer needs no second request to
|
|
10816
|
+
list a catalog. A producer that writes something else is still read; what cannot be read is left out, never rejected.
|
|
10817
|
+
|
|
10818
|
+
See docs/ui-catalog.md
|
|
10819
|
+
|
|
10820
|
+
#### `UI_STATE_KIND`
|
|
10821
|
+
|
|
10822
|
+
```ts
|
|
10823
|
+
export const UI_STATE_KIND = …
|
|
10824
|
+
```
|
|
10825
|
+
|
|
10826
|
+
The part kind of captured states.
|
|
10827
|
+
|
|
10828
|
+
#### `UI_PLAN_KIND`
|
|
10829
|
+
|
|
10830
|
+
```ts
|
|
10831
|
+
export const UI_PLAN_KIND = …
|
|
10832
|
+
```
|
|
10833
|
+
|
|
10834
|
+
The part kind of the expected set: states (and surfaces) that should exist, whether or not they were captured.
|
|
10835
|
+
|
|
10836
|
+
#### `UI_STATE_EXTENSION`
|
|
10837
|
+
|
|
10838
|
+
```ts
|
|
10839
|
+
export const UI_STATE_EXTENSION = …
|
|
10840
|
+
```
|
|
10841
|
+
|
|
10842
|
+
The check extension that carries the facts a producer declares about a state.
|
|
10843
|
+
|
|
10844
|
+
#### `isUiKind`
|
|
10845
|
+
|
|
10846
|
+
```ts
|
|
10847
|
+
export function isUiKind(kind: string): boolean
|
|
10848
|
+
```
|
|
10849
|
+
|
|
10850
|
+
Whether a part kind belongs to the UI catalog (`ui-state` or `ui-plan`).
|
|
10851
|
+
|
|
10852
|
+
#### `UiScreenshot`
|
|
10853
|
+
|
|
10854
|
+
```ts
|
|
10855
|
+
export type UiScreenshot = {
|
|
10856
|
+
readonly name: string;
|
|
10857
|
+
readonly bundleKey?: string;
|
|
10858
|
+
readonly uri?: string;
|
|
10859
|
+
readonly contentType?: string;
|
|
10860
|
+
readonly width?: number;
|
|
10861
|
+
readonly height?: number;
|
|
10862
|
+
readonly bytes?: number;
|
|
10863
|
+
readonly sha256?: string;
|
|
10864
|
+
/** Why the producer left the bytes out (`too-large`, `policy`…): a picture that was never taken is told apart from one left out. */
|
|
10865
|
+
readonly omitted?: string;
|
|
10866
|
+
};
|
|
10867
|
+
```
|
|
10868
|
+
|
|
10869
|
+
The picture of a captured state: a stored file (`bundleKey`), or an address (`uri`). Everything but `name` is optional.
|
|
10870
|
+
|
|
10871
|
+
#### `UiStateFacts`
|
|
10872
|
+
|
|
10873
|
+
```ts
|
|
10874
|
+
export type UiStateFacts = {
|
|
10875
|
+
/** The address or screen the state is of (`/checkout/payment`). */
|
|
10876
|
+
readonly route?: string;
|
|
10877
|
+
/** The test data it was captured with. */
|
|
10878
|
+
readonly fixture?: string;
|
|
10879
|
+
/** The audience or account kind it shows (`new-customer`). */
|
|
10880
|
+
readonly cohort?: string;
|
|
10881
|
+
/** Feature flags that were on. */
|
|
10882
|
+
readonly flags?: readonly string[];
|
|
10883
|
+
/** The product surfaces (screens, components) this state covers, for coverage against an inventory. */
|
|
10884
|
+
readonly covers?: readonly string[];
|
|
10885
|
+
/** A grouping a person navigates by (`checkout`). */
|
|
10886
|
+
readonly area?: string;
|
|
10887
|
+
/** On a `ui-plan` check only: this entry is a surface of the product's inventory, not a state. Nothing is captured for it. */
|
|
10888
|
+
readonly inventory?: boolean;
|
|
10889
|
+
};
|
|
10890
|
+
```
|
|
10891
|
+
|
|
10892
|
+
The facts a producer declares about a state, under the `atlas.ui-state` extension. All optional, all strings or lists of strings.
|
|
10893
|
+
|
|
10894
|
+
#### `UiStateFact`
|
|
10895
|
+
|
|
10896
|
+
```ts
|
|
10897
|
+
export type UiStateFact = UiStateFacts & {
|
|
10898
|
+
/** The check's picture: its `screenshot` attachment, else its first image. */
|
|
10899
|
+
readonly screenshot?: UiScreenshot;
|
|
10900
|
+
};
|
|
10901
|
+
```
|
|
10902
|
+
|
|
10903
|
+
What a row of a `ui-state` or `ui-plan` part carries for the catalog. Rides in the report row, only for those kinds.
|
|
10904
|
+
|
|
10905
|
+
#### `uiStateFacts`
|
|
10906
|
+
|
|
10907
|
+
```ts
|
|
10908
|
+
export function uiStateFacts(check: Pick<Check, 'extensions'>): UiStateFacts
|
|
10909
|
+
```
|
|
10910
|
+
|
|
10911
|
+
The declared facts of a check, read from its `atlas.ui-state` extension. Strings are trimmed and bounded, lists are
|
|
10912
|
+
distinct and bounded, and anything of another shape is dropped; a check without the extension has no facts.
|
|
10913
|
+
|
|
10914
|
+
#### `uiStateOf`
|
|
10915
|
+
|
|
10916
|
+
```ts
|
|
10917
|
+
export function uiStateOf(kind: string, check: Pick<Check, 'extensions' | 'attachments'>): UiStateFact | undefined
|
|
10918
|
+
```
|
|
10919
|
+
|
|
10920
|
+
What a check of a part of `kind` carries for the catalog: its declared facts and, in a `ui-state` part, its picture.
|
|
10921
|
+
`undefined` for any other kind, and for a UI check that carries neither, so ordinary rows stay as small as they were.
|
|
10922
|
+
|
|
9985
10923
|
### Report viewer navigation state
|
|
9986
10924
|
|
|
9987
10925
|
Source: `packages/reports/src/use-report-nav.ts`. Import: `@crvouga/atlas/reports/use-report-nav`
|