@pygmalionjs/pygmalion 0.8.0 → 0.9.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/binding-source.d.ts +25 -0
- package/dist-lib/{CameraLayer-nkNrazn8.js → CameraLayer-BdOko8XE.js} +17 -17
- package/dist-lib/pygmalion.js +24599 -12746
- package/dist-lib/{runtime-DdYnT6k4.js → runtime-Dxb-IaLN.js} +12427 -11116
- package/dist-lib/style.css +1 -1
- package/dist-lib/testing.js +10 -10
- package/dist-lib/types/binding/catalog.d.ts +10 -0
- package/dist-lib/types/binding/contracts.d.ts +202 -0
- package/dist-lib/types/binding/documentCommands.d.ts +38 -0
- package/dist-lib/types/binding/index.d.ts +6 -0
- package/dist-lib/types/binding/preview.d.ts +37 -0
- package/dist-lib/types/binding/source.d.ts +109 -0
- package/dist-lib/types/binding/useDataProjection.d.ts +9 -0
- package/dist-lib/types/binding/validate.d.ts +8 -0
- package/dist-lib/types/core/runtime.d.ts +6 -0
- package/dist-lib/types/document/appearance.d.ts +112 -0
- package/dist-lib/types/document/assets.d.ts +204 -0
- package/dist-lib/types/document/components.d.ts +169 -0
- package/dist-lib/types/document/contracts.d.ts +171 -0
- package/dist-lib/types/document/engine.d.ts +20 -0
- package/dist-lib/types/document/exportSettings.d.ts +45 -0
- package/dist-lib/types/document/geometry.d.ts +11 -0
- package/dist-lib/types/document/layout.d.ts +53 -0
- package/dist-lib/types/document/prototype.d.ts +424 -0
- package/dist-lib/types/document/prototypeReferences.d.ts +3 -0
- package/dist-lib/types/document/richText.d.ts +185 -0
- package/dist-lib/types/document/styles.d.ts +216 -0
- package/dist-lib/types/document/svgImport.d.ts +17 -0
- package/dist-lib/types/document/vector.d.ts +222 -0
- package/dist-lib/types/editor/designCompiler.d.ts +7 -0
- package/dist-lib/types/editor/designImport.d.ts +7 -0
- package/dist-lib/types/editor/inspect.d.ts +8 -0
- package/dist-lib/types/editor/revisionCatalogInstall.d.ts +1 -1
- package/dist-lib/types/editor/screenInteractions.d.ts +3 -3
- package/dist-lib/types/editor/store.d.ts +1 -1
- package/dist-lib/types/editor/surfaceDemands.d.ts +3 -1
- package/dist-lib/types/host/project.d.ts +44 -0
- package/dist-lib/types/host/registryConnections.d.ts +11 -0
- package/dist-lib/types/host/sourceFilePlan.d.ts +15 -0
- package/dist-lib/types/host/sourceWriteback.d.ts +50 -0
- package/dist-lib/types/lib.d.ts +101 -3
- package/dist-lib/types/token-library/bindings.d.ts +62 -0
- package/dist-lib/types/token-library/contracts.d.ts +117 -0
- package/dist-lib/types/token-library/engine.d.ts +13 -0
- package/dist-lib/types/workspace/WorkspaceShell.d.ts +9 -0
- package/dist-lib/types/workspace/application/ApplicationWorkspace.d.ts +3 -0
- package/dist-lib/types/workspace/configuration.d.ts +39 -0
- package/dist-lib/types/workspace/contracts.d.ts +64 -0
- package/dist-lib/types/workspace/controller.d.ts +35 -0
- package/dist-lib/types/workspace/documentSessions.d.ts +42 -0
- package/dist-lib/types/workspace/edit/AppearancePanel.d.ts +23 -0
- package/dist-lib/types/workspace/edit/ArrangeLayers.d.ts +10 -0
- package/dist-lib/types/workspace/edit/AssetsPanel.d.ts +16 -0
- package/dist-lib/types/workspace/edit/CanvasGuides.d.ts +19 -0
- package/dist-lib/types/workspace/edit/ComponentPanel.d.ts +11 -0
- package/dist-lib/types/workspace/edit/DataPanel.d.ts +16 -0
- package/dist-lib/types/workspace/edit/DesignWorkspace.d.ts +10 -0
- package/dist-lib/types/workspace/edit/LayerTree.d.ts +14 -0
- package/dist-lib/types/workspace/edit/LayoutPanel.d.ts +13 -0
- package/dist-lib/types/workspace/edit/PrototypePanel.d.ts +12 -0
- package/dist-lib/types/workspace/edit/ScopeTransferDialog.d.ts +40 -0
- package/dist-lib/types/workspace/edit/SelectionHandles.d.ts +52 -0
- package/dist-lib/types/workspace/edit/StylesPanel.d.ts +8 -0
- package/dist-lib/types/workspace/edit/TextEditor.d.ts +50 -0
- package/dist-lib/types/workspace/edit/VectorEditor.d.ts +89 -0
- package/dist-lib/types/workspace/edit/VectorToolbar.d.ts +5 -0
- package/dist-lib/types/workspace/edit/canvasGuideModel.d.ts +102 -0
- package/dist-lib/types/workspace/edit/clipboard.d.ts +35 -0
- package/dist-lib/types/workspace/edit/duplicateMetadata.d.ts +2 -0
- package/dist-lib/types/workspace/edit/geometryHandles.d.ts +224 -0
- package/dist-lib/types/workspace/edit/layerArrangement.d.ts +39 -0
- package/dist-lib/types/workspace/edit/layerMoves.d.ts +44 -0
- package/dist-lib/types/workspace/edit/projection.d.ts +12 -0
- package/dist-lib/types/workspace/edit/scopeTransfer.d.ts +96 -0
- package/dist-lib/types/workspace/edit/textStyles.d.ts +75 -0
- package/dist-lib/types/workspace/edit/vectorTools.d.ts +136 -0
- package/dist-lib/types/workspace/legacyModeAdapter.d.ts +13 -0
- package/dist-lib/types/workspace/source/ResponsiveLayoutPanel.d.ts +20 -0
- package/dist-lib/types/workspace/source/SourceReview.d.ts +11 -0
- package/dist-lib/types/workspace/source/appearanceSource.d.ts +19 -0
- package/dist-lib/types/workspace/source/compiler.d.ts +5 -0
- package/dist-lib/types/workspace/source/contracts.d.ts +156 -0
- package/dist-lib/types/workspace/source/controller.d.ts +50 -0
- package/dist-lib/types/workspace/source/documentRevision.d.ts +2 -0
- package/dist-lib/types/workspace/source/draftPreviewClient.d.ts +13 -0
- package/dist-lib/types/workspace/source/draftRecipe.d.ts +30 -0
- package/dist-lib/types/workspace/source/flowContracts.d.ts +72 -0
- package/dist-lib/types/workspace/source/prototypeSource.d.ts +4 -0
- package/dist-lib/types/workspace/source/responsive.d.ts +13 -0
- package/dist-lib/types/workspace/source/responsiveContracts.d.ts +78 -0
- package/dist-lib/types/workspace/source/structure.d.ts +21 -0
- package/dist-lib/types/workspace/source/textSource.d.ts +28 -0
- package/dist-lib/types/workspace/tokens/TokenLibraryContext.d.ts +11 -0
- package/dist-lib/types/workspace/tokens/TokenLibraryWorkspace.d.ts +14 -0
- package/dist-lib/types/workspace/tokens/contracts.d.ts +74 -0
- package/dist-lib/types/workspace/tokens/controller.d.ts +44 -0
- package/dist-lib/types/workspace/view/PrototypePlayer.d.ts +17 -0
- package/dist-lib/types/workspace/view/StoryboardWorkspace.d.ts +3 -0
- package/dist-lib/types/workspace/view/prototypePlayback.d.ts +46 -0
- package/draft-preview.d.ts +54 -0
- package/inspect.d.ts +2 -0
- package/node/design-session.mjs +3 -0
- package/node/inspect-plugin.mjs +3 -1
- package/node/inspect-writeback.mjs +18 -2
- package/node/preview-artifact-store.mjs +10 -7
- package/node/source-archive.mjs +88 -0
- package/node/source-bindings.mjs +65 -0
- package/node/source-file-lock.mjs +15 -0
- package/node/source-file-plan.mjs +162 -0
- package/node/source-file-proofs.mjs +44 -0
- package/node/source-responsive-codec.d.mts +17 -0
- package/node/source-responsive-codec.mjs +133 -0
- package/node/source-responsive.mjs +189 -0
- package/node/source-structure-reconcile.mjs +186 -0
- package/node/source-structure-wrapper-codec.d.mts +20 -0
- package/node/source-structure-wrapper-codec.mjs +205 -0
- package/node/source-structure.mjs +404 -0
- package/node/token-source-references.mjs +136 -0
- package/node/workspace-draft-preview-plugin.mjs +70 -0
- package/node/workspace-draft-preview.mjs +231 -0
- package/node/workspace-source-identity.d.mts +1 -0
- package/node/workspace-source-identity.mjs +21 -0
- package/node/workspace-source-plan.mjs +195 -0
- package/package.json +52 -6
- package/source-files.d.ts +35 -0
- package/source-responsive.d.ts +10 -0
- package/source-structure.d.ts +155 -0
- package/workspace-source-plan.d.ts +44 -0
- package/docs/coverage-contracts.md +0 -431
- package/docs/screen-state-contract.md +0 -252
|
@@ -1,431 +0,0 @@
|
|
|
1
|
-
# Coverage contracts
|
|
2
|
-
|
|
3
|
-
A host that ships a storyboard wants two questions answered before a change
|
|
4
|
-
merges:
|
|
5
|
-
|
|
6
|
-
1. **Representation.** Is every source route the application can render shown
|
|
7
|
-
somewhere the editor can open — a route preview or a registered component
|
|
8
|
-
fixture — or excluded for a stated reason?
|
|
9
|
-
2. **Classification.** Is every user scenario verified exactly once, and by
|
|
10
|
-
which declaration?
|
|
11
|
-
|
|
12
|
-
They are different questions with different answers. A preview URL proves that
|
|
13
|
-
a route renders; it proves nothing about a user scenario or an interaction. A
|
|
14
|
-
scenario claim proves where a scenario is verified; it says nothing about
|
|
15
|
-
routes nobody declared. The package answers each with its own functions, and a
|
|
16
|
-
green answer to one never stands in for the other.
|
|
17
|
-
|
|
18
|
-
Everything here runs in plain Node: no DOM, no Vite server, no editor mount,
|
|
19
|
-
no network. The functions are pure; they take declarations and return
|
|
20
|
-
diagnostics with structured identities. `scripts/smoke-consumer/smoke.ts` is a
|
|
21
|
-
complete consumer that imports them from the packed package and runs the
|
|
22
|
-
positive and negative cases below.
|
|
23
|
-
|
|
24
|
-
## Who owns what
|
|
25
|
-
|
|
26
|
-
| The host owns | The package validates |
|
|
27
|
-
| --- | --- |
|
|
28
|
-
| Source discovery and router-specific normalization: which files are routes, what their canonical pattern is, what their stable id is | That every discovered route is represented, by which screen, and how |
|
|
29
|
-
| Screen cases, fixtures, and the mapping from a fixture to the routes it stands for | That every mapping names a route discovery reports and a component the registry has |
|
|
30
|
-
| Verdicts for routes no screen represents: covered by a named declaration, or excluded for a reason | That every verdict names a surface that exists and a declaration that exists, exactly once |
|
|
31
|
-
| The scenario inventory and the claims that classify it | That every scenario is classified exactly once against declarations that exist |
|
|
32
|
-
| Product scope, migration baselines, and what the CI gate fails on | Counts by outcome, with exclusions and todo items apart from coverage |
|
|
33
|
-
|
|
34
|
-
The package never reads a router, a file system, or a running application in
|
|
35
|
-
this path. When a guarantee needs product knowledge, it belongs in the host;
|
|
36
|
-
the section [What stays in the host](#what-stays-in-the-host) lists those.
|
|
37
|
-
|
|
38
|
-
## Identify a route by its pattern, not by a preview URL
|
|
39
|
-
|
|
40
|
-
Discovery hands the audit `DiscoveredRoute` values:
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
interface DiscoveredRoute {
|
|
44
|
-
/** The canonical pattern, in whatever syntax the host normalized to. */
|
|
45
|
-
path?: string;
|
|
46
|
-
/** The host's stable identity for the route — usually its source path. */
|
|
47
|
-
id?: string;
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
`path` is the route as the router declares it, not an address someone visited:
|
|
52
|
-
`/items/:id`, or `/items/[id]` if the host keeps the framework's syntax. `id`
|
|
53
|
-
is the join key back to the source. A candidate for an unrepresented route is
|
|
54
|
-
identified as `route:<normalized path>` and carries the `id` as `sourceId`, so
|
|
55
|
-
a diagnostic can be matched to a route name or a source file without parsing
|
|
56
|
-
its message.
|
|
57
|
-
|
|
58
|
-
The package matches a discovered pattern against a screen's concrete `route`
|
|
59
|
-
in one way: segment for segment, where a `:param` or `*` segment accepts any
|
|
60
|
-
nonempty concrete segment. There is no prefix matching and no other parameter
|
|
61
|
-
syntax. A host whose router writes `[id]` either normalizes to `:id` before
|
|
62
|
-
calling the audit, or maps the route explicitly as the next section shows.
|
|
63
|
-
Router-specific discovery and normalization stay in the host on purpose; the
|
|
64
|
-
package does not carry a parser for any framework.
|
|
65
|
-
|
|
66
|
-
## Represent a route: preview, mapping, or fixture
|
|
67
|
-
|
|
68
|
-
A declared screen says what it represents through three fields, all of which a
|
|
69
|
-
`DesignScreenCase` already has:
|
|
70
|
-
|
|
71
|
-
```ts
|
|
72
|
-
interface DeclaredScreenRoute {
|
|
73
|
-
id: string;
|
|
74
|
-
/** The concrete canonical address the screen boots. */
|
|
75
|
-
route?: string;
|
|
76
|
-
/** The registered component the screen renders instead of navigating. */
|
|
77
|
-
componentName?: string;
|
|
78
|
-
/** Source routes the screen explicitly represents, by discovered id or path. */
|
|
79
|
-
coverageRoutes?: readonly string[];
|
|
80
|
-
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
`auditRouteCoverage` compares the two lists:
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
import { auditRouteCoverage } from '@pygmalionjs/pygmalion';
|
|
87
|
-
|
|
88
|
-
const audit = auditRouteCoverage({
|
|
89
|
-
routes, // DiscoveredRoute[] from the host's discovery
|
|
90
|
-
declaredScreens, // DesignScreenCase[] or result.routeMappings
|
|
91
|
-
registeredComponents, // the component registry, or its names
|
|
92
|
-
});
|
|
93
|
-
audit.candidates; // SurfaceCandidate[] — routes nothing represents
|
|
94
|
-
audit.represented; // RouteRepresentation[] — every (route, screen) pair that holds
|
|
95
|
-
audit.diagnostics; // RouteCoverageDiagnostic[] — mappings that name something missing
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
A screen represents a route in one of three ways, recorded as `via` on each
|
|
99
|
-
`RouteRepresentation`:
|
|
100
|
-
|
|
101
|
-
| `via` | How | Example |
|
|
102
|
-
| --- | --- | --- |
|
|
103
|
-
| `preview` | The screen's concrete `route` satisfies the discovered pattern | `route: '/records/main'` represents `/records/:section` |
|
|
104
|
-
| `mapping` | The screen lists the route in `coverageRoutes` and boots a route of its own | `route: '/items/fixture-1', coverageRoutes: ['/items/[id]']` |
|
|
105
|
-
| `fixture` | The screen lists the route in `coverageRoutes` and renders a registered component | `componentName: 'GuardedFixture', coverageRoutes: ['/guarded']` |
|
|
106
|
-
|
|
107
|
-
An explicit entry matches exactly: it equals the discovered route's `id`, or
|
|
108
|
-
it normalizes to the discovered route's path. No parameter is substituted and
|
|
109
|
-
no prefix is expanded. That is what keeps a fixture from covering more than the
|
|
110
|
-
host said, and a concrete preview from covering a pattern by resemblance.
|
|
111
|
-
|
|
112
|
-
The two opt-in checks are on by supplying their input. With
|
|
113
|
-
`registeredComponents`, a screen whose `componentName` is not registered
|
|
114
|
-
represents nothing, and the audit reports it. Without it, a fixture mapping is
|
|
115
|
-
taken at its word, which is the 0.7 behaviour plus explicit mappings.
|
|
116
|
-
|
|
117
|
-
### Diagnostics
|
|
118
|
-
|
|
119
|
-
Invalid mappings are reported apart from missing representation. The
|
|
120
|
-
candidates list is "what owes a verdict"; the diagnostics list is "what the
|
|
121
|
-
host wrote that no longer holds". A stale mapping produces both: the
|
|
122
|
-
diagnostic on the screen, and the route back among the candidates, because a
|
|
123
|
-
mapping that names something missing must never keep covering.
|
|
124
|
-
|
|
125
|
-
| `code` | Meaning | Fields |
|
|
126
|
-
| --- | --- | --- |
|
|
127
|
-
| `unknown-route` | A `coverageRoutes` entry matches no discovered route; it was removed or renamed | `screenId`, `route`, `componentName?` |
|
|
128
|
-
| `unregistered-component` | The screen's `componentName` is not among `registeredComponents` | `screenId`, `componentName` |
|
|
129
|
-
| `empty-mapping` | A `coverageRoutes` entry is blank | `screenId`, `route` |
|
|
130
|
-
|
|
131
|
-
`enumerateRouteCandidates(input)` returns `audit.candidates` alone, for a
|
|
132
|
-
caller that only needs the holes.
|
|
133
|
-
|
|
134
|
-
### Examples
|
|
135
|
-
|
|
136
|
-
A static route with a preview:
|
|
137
|
-
|
|
138
|
-
```ts
|
|
139
|
-
auditRouteCoverage({
|
|
140
|
-
routes: [{ id: 'app/settings/page', path: '/settings' }],
|
|
141
|
-
declaredScreens: [{ id: 'screen:settings', route: '/settings' }],
|
|
142
|
-
});
|
|
143
|
-
// candidates: [] represented: [{ screenId: 'screen:settings', via: 'preview', … }]
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
A dynamic route, in the framework's own syntax, represented by the concrete
|
|
147
|
-
preview that instantiates it:
|
|
148
|
-
|
|
149
|
-
```ts
|
|
150
|
-
auditRouteCoverage({
|
|
151
|
-
routes: [{ id: 'app/items/[id]/page', path: '/items/[id]' }],
|
|
152
|
-
declaredScreens: [
|
|
153
|
-
{
|
|
154
|
-
id: 'screen:item-detail',
|
|
155
|
-
route: '/items/fixture-1',
|
|
156
|
-
coverageRoutes: ['app/items/[id]/page'],
|
|
157
|
-
},
|
|
158
|
-
],
|
|
159
|
-
});
|
|
160
|
-
// candidates: [] represented: [{ via: 'mapping', sourceId: 'app/items/[id]/page', … }]
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Without the `coverageRoutes` entry the same input leaves `route:/items/[id]`
|
|
164
|
-
among the candidates: a concrete address does not satisfy a bracket pattern,
|
|
165
|
-
because the package does not parse that syntax. The route stays missing until
|
|
166
|
-
the host supplies the mapping, a registered fixture, or a normalized pattern
|
|
167
|
-
(`path: '/items/:id'`, which the concrete preview then satisfies).
|
|
168
|
-
|
|
169
|
-
A guarded route, represented by a registered component fixture and no
|
|
170
|
-
navigation URL:
|
|
171
|
-
|
|
172
|
-
```ts
|
|
173
|
-
auditRouteCoverage({
|
|
174
|
-
routes: [{ id: 'app/guarded/page', path: '/guarded' }],
|
|
175
|
-
declaredScreens: [
|
|
176
|
-
{ id: 'screen:guarded', componentName: 'GuardedFixture', coverageRoutes: ['/guarded'] },
|
|
177
|
-
],
|
|
178
|
-
registeredComponents: registry,
|
|
179
|
-
});
|
|
180
|
-
// candidates: [] represented: [{ via: 'fixture', componentName: 'GuardedFixture', … }]
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
Remove `GuardedFixture` from the registry and the same call reports
|
|
184
|
-
`unregistered-component` on `screen:guarded` and `route:/guarded` among the
|
|
185
|
-
candidates.
|
|
186
|
-
|
|
187
|
-
A multi-screen journey — every step is its own source route and its own
|
|
188
|
-
screen; the journey itself is declared to the storyboard as a `screenFlows`
|
|
189
|
-
path, which is a separate, host-owned declaration:
|
|
190
|
-
|
|
191
|
-
```ts
|
|
192
|
-
auditRouteCoverage({
|
|
193
|
-
routes: [
|
|
194
|
-
{ id: 'app/checkout/page', path: '/checkout' },
|
|
195
|
-
{ id: 'app/checkout/review/page', path: '/checkout/review' },
|
|
196
|
-
{ id: 'app/checkout/done/page', path: '/checkout/done' },
|
|
197
|
-
],
|
|
198
|
-
declaredScreens: [
|
|
199
|
-
{ id: 'screen:checkout', route: '/checkout' },
|
|
200
|
-
{ id: 'screen:checkout-review', route: '/checkout/review' },
|
|
201
|
-
{ id: 'screen:checkout-done', componentName: 'CheckoutDoneFixture', coverageRoutes: ['/checkout/done'] },
|
|
202
|
-
],
|
|
203
|
-
registeredComponents: registry,
|
|
204
|
-
});
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
A step reached from another step without a navigation (a confirmation dialog
|
|
208
|
-
on the review screen) is a screen state or an interactive-state axis of the
|
|
209
|
-
review screen, not a route; the [screen state contract](./screen-state-contract.md)
|
|
210
|
-
decides which.
|
|
211
|
-
|
|
212
|
-
### Audit before the collection strips the mapping
|
|
213
|
-
|
|
214
|
-
`createDesignScreenCollection` removes `coverageRoutes` from the pages it
|
|
215
|
-
returns, because the editor does not need it. The result keeps the route facts
|
|
216
|
-
beside them as `routeMappings`, one `DeclaredScreenRoute` per case, so the
|
|
217
|
-
audit runs on exactly the declarations the editor will show:
|
|
218
|
-
|
|
219
|
-
```ts
|
|
220
|
-
const result = createDesignScreenCollection({ cases, scenarios, behaviorScenarios });
|
|
221
|
-
const audit = auditRouteCoverage({
|
|
222
|
-
routes,
|
|
223
|
-
declaredScreens: result.routeMappings,
|
|
224
|
-
registeredComponents: registry,
|
|
225
|
-
});
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
The collection throws when scenario classification fails, so a gate that wants
|
|
229
|
-
the route audit even then passes the original `cases` instead; they satisfy
|
|
230
|
-
`DeclaredScreenRoute` as written.
|
|
231
|
-
|
|
232
|
-
## Answer the remaining routes: verdicts
|
|
233
|
-
|
|
234
|
-
A route nothing represents still exists; the host says what happens to it. A
|
|
235
|
-
`SurfaceClassification` names the candidate and gives one of two verdicts:
|
|
236
|
-
|
|
237
|
-
```ts
|
|
238
|
-
const classifications: SurfaceClassification[] = [
|
|
239
|
-
{ id: 'route:/labs', verdict: 'covered', by: 'screen:labs' },
|
|
240
|
-
{ id: 'route:/legacy', verdict: 'excluded', reason: 'redirects to /settings on every entry' },
|
|
241
|
-
];
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
`validateSurfaceDemands(candidates, classifications, options?)` checks them.
|
|
245
|
-
Every failure is an error; a warning would let a surface go unreviewed while
|
|
246
|
-
the build stayed green.
|
|
247
|
-
|
|
248
|
-
| `code` | Meaning |
|
|
249
|
-
| --- | --- |
|
|
250
|
-
| `unclassified` | A candidate has no verdict |
|
|
251
|
-
| `stale-verdict` | A verdict names a surface no enumerator reports |
|
|
252
|
-
| `duplicate-verdict` | Two verdicts answer one surface |
|
|
253
|
-
| `duplicate-candidate` | An enumerator reported one surface twice |
|
|
254
|
-
| `exclusion-reason` | An exclusion's reason is shorter than `MIN_SURFACE_REASON_LENGTH` |
|
|
255
|
-
| `empty-target` | A covered verdict names nothing |
|
|
256
|
-
| `unknown-target` | A covered verdict names a declaration that is not among `knownTargets` |
|
|
257
|
-
|
|
258
|
-
`unknown-target` is the check a deleted screen used to slip past: a verdict
|
|
259
|
-
saying `by: 'screen:settings'` kept covering `/settings` after the screen was
|
|
260
|
-
gone, because the target only had to be nonempty. Pass the declarations and it
|
|
261
|
-
becomes an error:
|
|
262
|
-
|
|
263
|
-
```ts
|
|
264
|
-
import { declaredCoverageTargets, validateSurfaceDemands } from '@pygmalionjs/pygmalion';
|
|
265
|
-
|
|
266
|
-
const knownTargets = declaredCoverageTargets({
|
|
267
|
-
screens: cases, // named by their own id, e.g. 'screen:settings'
|
|
268
|
-
components: registry, // named 'component:<name>'
|
|
269
|
-
targets: axes.map((axis) => `axis:${axis.id}`), // anything else, verbatim
|
|
270
|
-
});
|
|
271
|
-
const diagnostics = validateSurfaceDemands(audit.candidates, classifications, { knownTargets });
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
A screen is named by its own id — the same string a scenario claim's
|
|
275
|
-
`screenId` uses. A host with a different vocabulary for targets builds the set
|
|
276
|
-
itself and hands it to `knownTargets` directly.
|
|
277
|
-
|
|
278
|
-
## Classify scenarios exactly once
|
|
279
|
-
|
|
280
|
-
`createDesignScreenCollection` takes the scenario inventory and the cases that
|
|
281
|
-
demonstrate it and throws before the editor opens when any scenario is
|
|
282
|
-
classified zero times or twice, or when a case names a scenario the inventory
|
|
283
|
-
lacks. Its `coverage` counts screens and behaviors apart and lists what is
|
|
284
|
-
missing. The invariant is exact-once classification of the scenarios the host
|
|
285
|
-
supplied; the function does not discover scenarios, and it does not learn
|
|
286
|
-
about routes.
|
|
287
|
-
|
|
288
|
-
`validateScenarioCoverage(claims, context)` is the same invariant for the
|
|
289
|
-
claims ledger, where a scenario may be verified by a frame, a screen-state
|
|
290
|
-
sibling, a component prop, a viewport preset, a catalog sheet, an interactive
|
|
291
|
-
axis, a card slot, or a list length, or admitted as `behavior` or `todo` with
|
|
292
|
-
a reason. Every diagnostic carries a `code`, the `scenarioId`, the claim's
|
|
293
|
-
`kind`, its `screenId` when it named one, and the `target` the context lacks:
|
|
294
|
-
|
|
295
|
-
| `code` | Meaning |
|
|
296
|
-
| --- | --- |
|
|
297
|
-
| `unknown-scenario` | The claim names a scenario the inventory lacks |
|
|
298
|
-
| `unclaimed` | A scenario has no claim |
|
|
299
|
-
| `duplicate-claim` | A scenario has more than one claim |
|
|
300
|
-
| `insufficient-reason` | A `behavior` or `todo` reason is below the floor |
|
|
301
|
-
| `unknown-screen` | The screen or catalog sheet is not declared |
|
|
302
|
-
| `not-a-state-group` | A `screen-state` claim names a screen with no sibling states |
|
|
303
|
-
| `unknown-component`, `unknown-prop`, `unknown-value` | The registry lacks the component, the prop, or the value |
|
|
304
|
-
| `unknown-preset` | The viewport preset is not declared |
|
|
305
|
-
| `unknown-axis`, `unknown-option` | The interactive axis or option, or the card option, is not declared |
|
|
306
|
-
| `unknown-card`, `unknown-slot` | The card or slot is not declared |
|
|
307
|
-
| `unknown-list`, `count-out-of-range` | The list is not declared, or the count is outside its range |
|
|
308
|
-
| `base-option` | Warning: the claimed option is the axis base, which reproduces the capture |
|
|
309
|
-
|
|
310
|
-
### Case and step identity
|
|
311
|
-
|
|
312
|
-
Exact-once classification depends on the granularity of scenario ids. The
|
|
313
|
-
rule: one scenario id per state a reviewer should be able to select again,
|
|
314
|
-
classified by exactly one declaration.
|
|
315
|
-
|
|
316
|
-
- A journey of three screens is three scenario ids (or one id per step), each
|
|
317
|
-
classified by its own case. One scenario id spanning three screens cannot be
|
|
318
|
-
classified exactly once.
|
|
319
|
-
- A screen-state variant is a case of its own in the same `scenario` group,
|
|
320
|
-
distinguished by `state`. It carries its own scenario id, or the group's
|
|
321
|
-
canonical case carries the set in `scenarioIds`.
|
|
322
|
-
- A case demonstrates every id in its `scenarioIds`; the canonical
|
|
323
|
-
`scenarioId` is only where a reviewer is sent first.
|
|
324
|
-
- A viewport variant of a case is not a new scenario. Expand with
|
|
325
|
-
`expandDesignScreenCasesViewports`, which derives variant ids from the case
|
|
326
|
-
id and keeps the scenario ids with the authored case.
|
|
327
|
-
- An interaction step inside a recipe is how a case is reached, not a
|
|
328
|
-
scenario. A step a reviewer must be able to select on its own is a
|
|
329
|
-
screen-state axis option or a case.
|
|
330
|
-
- A route candidate id (`route:/checkout`) and a scenario id (`US-checkout`)
|
|
331
|
-
are different namespaces. A covered verdict names the screen that
|
|
332
|
-
represents the route; a claim names the screen that verifies the scenario.
|
|
333
|
-
The same screen may do both.
|
|
334
|
-
|
|
335
|
-
## A preview is not a scenario
|
|
336
|
-
|
|
337
|
-
A route the audit reports as represented has a screen that renders it. That
|
|
338
|
-
is all it says. Whether the screen demonstrates a user scenario is the claims
|
|
339
|
-
ledger's question, answered by a claim naming that screen; whether an
|
|
340
|
-
interaction is covered is a screen-state axis or an interactive-state claim,
|
|
341
|
-
never inferred from a URL. A gate that reports "all routes represented" has
|
|
342
|
-
not reported scenario coverage.
|
|
343
|
-
|
|
344
|
-
## Counts a report shows
|
|
345
|
-
|
|
346
|
-
Two summaries turn diagnostics into numbers without re-deriving them:
|
|
347
|
-
|
|
348
|
-
```ts
|
|
349
|
-
summarizeSurfaceDemands(candidates, classifications, { knownTargets });
|
|
350
|
-
// { total, covered, excluded, invalid, unclassified, excludedIds, invalidIds, unclassifiedIds }
|
|
351
|
-
|
|
352
|
-
summarizeScenarioCoverage(claims, context);
|
|
353
|
-
// { total, covered, behavior, todo, invalid, unclaimed, behaviorIds, todoIds, invalidIds, unclaimedIds, unknownIds }
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
`covered` counts only what validated clean. An exclusion is in `excluded`,
|
|
357
|
-
never in `covered`. A `todo` is in `todo`. A verdict or claim with an error is
|
|
358
|
-
in `invalid`. A surface or scenario nobody answered is in `unclassified` or
|
|
359
|
-
`unclaimed`. Nothing moves between buckets, so a report that shows all of
|
|
360
|
-
them shows the whole picture, and a report that shows only `covered` is
|
|
361
|
-
visibly incomplete.
|
|
362
|
-
|
|
363
|
-
## Join diagnostics to source
|
|
364
|
-
|
|
365
|
-
| Diagnostic | Identity fields |
|
|
366
|
-
| --- | --- |
|
|
367
|
-
| `SurfaceDemandDiagnostic` | `code`, `kind` (`route`, `control`, `request`), `candidateId`, `sourceId?`, `frameId?`, `target?` |
|
|
368
|
-
| `RouteCoverageDiagnostic` | `code`, `screenId`, `route?`, `componentName?` |
|
|
369
|
-
| `ScenarioCoverageDiagnostic` | `code`, `scenarioId`, `kind?`, `screenId?`, `target?` |
|
|
370
|
-
| `RouteRepresentation` | `candidateId`, `path`, `sourceId?`, `screenId`, `via`, `componentName?` |
|
|
371
|
-
|
|
372
|
-
`message` is for people. A gate joins on the fields: `sourceId` back to the
|
|
373
|
-
route's source path, `screenId` to the case and its `sourcePath`,
|
|
374
|
-
`scenarioId` to the scenario's `sourcePath`, `target` to the declaration that
|
|
375
|
-
is missing.
|
|
376
|
-
|
|
377
|
-
## What stays in the host
|
|
378
|
-
|
|
379
|
-
These guarantees need product knowledge, so the package does not provide
|
|
380
|
-
them; a host adopting the gate implements them itself:
|
|
381
|
-
|
|
382
|
-
- **Discovery and normalization.** Which files are routes, what their canonical
|
|
383
|
-
pattern is, and a stable `id` for each. The package matches patterns in one
|
|
384
|
-
syntax and parses no framework's.
|
|
385
|
-
- **Scope.** Which discovered routes are in the product at all. A route that is
|
|
386
|
-
out of scope gets an `excluded` verdict with a reason; the package does not
|
|
387
|
-
decide eligibility.
|
|
388
|
-
- **Fixture fidelity.** That a registered component fixture renders what the
|
|
389
|
-
route renders. The package checks that the component is registered, not
|
|
390
|
-
what it draws.
|
|
391
|
-
- **The inventory.** Scenario ids and their granularity, per the rules above.
|
|
392
|
-
- **Baselines.** A migration window is a host-held set of candidate ids that
|
|
393
|
-
are allowed to stay unclassified for now, subtracted from the diagnostics
|
|
394
|
-
before failing the build. The package does not downgrade severities.
|
|
395
|
-
- **CI policy.** Which diagnostics fail the build, on which Node version. The
|
|
396
|
-
package declares `engines.node`; the gate should run on the host's
|
|
397
|
-
supported version and on the declared minimum.
|
|
398
|
-
|
|
399
|
-
## A headless gate, end to end
|
|
400
|
-
|
|
401
|
-
```ts
|
|
402
|
-
import {
|
|
403
|
-
auditRouteCoverage,
|
|
404
|
-
createDesignScreenCollection,
|
|
405
|
-
declaredCoverageTargets,
|
|
406
|
-
summarizeScenarioCoverage,
|
|
407
|
-
summarizeSurfaceDemands,
|
|
408
|
-
validateScenarioCoverage,
|
|
409
|
-
validateSurfaceDemands,
|
|
410
|
-
} from '@pygmalionjs/pygmalion';
|
|
411
|
-
|
|
412
|
-
const routes = discoverRoutes(); // host: DiscoveredRoute[]
|
|
413
|
-
const result = createDesignScreenCollection({ cases, scenarios, behaviorScenarios });
|
|
414
|
-
|
|
415
|
-
const audit = auditRouteCoverage({
|
|
416
|
-
routes,
|
|
417
|
-
declaredScreens: result.routeMappings,
|
|
418
|
-
registeredComponents: registry,
|
|
419
|
-
});
|
|
420
|
-
const surfaces = validateSurfaceDemands(audit.candidates, classifications, {
|
|
421
|
-
knownTargets: declaredCoverageTargets({ screens: cases, components: registry }),
|
|
422
|
-
});
|
|
423
|
-
const ledger = validateScenarioCoverage(claims, context);
|
|
424
|
-
|
|
425
|
-
const failures = [...audit.diagnostics, ...surfaces, ...ledger].filter(
|
|
426
|
-
(diagnostic) => diagnostic.severity === 'error' && !baseline.has(keyOf(diagnostic)),
|
|
427
|
-
);
|
|
428
|
-
console.table(summarizeSurfaceDemands(audit.candidates, classifications));
|
|
429
|
-
console.table(summarizeScenarioCoverage(claims, context));
|
|
430
|
-
process.exit(failures.length === 0 ? 0 : 1);
|
|
431
|
-
```
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
# Screen state contract
|
|
2
|
-
|
|
3
|
-
Pygmalion uses six review placements. A host must choose the placement from
|
|
4
|
-
how the variation is reproduced, not from how different its pixels look.
|
|
5
|
-
|
|
6
|
-
| Placement | Use when | Pygmalion declaration |
|
|
7
|
-
| --- | --- | --- |
|
|
8
|
-
| Frame | The variation is an independent task, journey checkpoint, structural composition, overlay context, or supported viewport that reviewers must navigate to directly. | `DesignScreenCase` |
|
|
9
|
-
| Interaction state | The same booted screen has a deterministic, reversible visual endpoint, reached either directly or through a local gesture. | `ScreenStateAxisDef` with `kind: 'interaction'` and `desiredState` or `steps` |
|
|
10
|
-
| Condition state | The same screen needs external data, network, storage, permission, media, or timing conditions before it boots. | `ScreenStateAxisDef` with `kind: 'condition'` |
|
|
11
|
-
| Editable parameter | The variation only samples text length, row count, content, or another freely editable value and has no distinct product endpoint. | Edit mode, component props, list controls, viewport controls, or QA data |
|
|
12
|
-
| Motion preview | The same screen continuously changes through CSS or Web Animations without becoming a new product state. | Nothing to declare: a live frame animates on its own; a capture holds the settled baseline |
|
|
13
|
-
| Behavior | The action has no stable visual endpoint to preserve. | `DesignBehaviorScenario` or an automated interaction test |
|
|
14
|
-
|
|
15
|
-
The practical test is: after reproducing the variation, is there a stable state
|
|
16
|
-
that another reviewer should be able to select again? If yes, use a frame or a
|
|
17
|
-
screen-state axis. If the answer is only “this value is longer” or “the request
|
|
18
|
-
was sent,” keep it in editing or QA instead.
|
|
19
|
-
|
|
20
|
-
## Frame versus same-frame state
|
|
21
|
-
|
|
22
|
-
Preserve a separate frame when at least one of these is true:
|
|
23
|
-
|
|
24
|
-
- it is a named step in a user journey;
|
|
25
|
-
- it changes the screen's primary task or structural composition;
|
|
26
|
-
- it provides an independent review anchor that must remain visible beside
|
|
27
|
-
another screen;
|
|
28
|
-
- it is a supported viewport whose responsive composition must be compared.
|
|
29
|
-
|
|
30
|
-
Use a same-frame axis when all of these are true:
|
|
31
|
-
|
|
32
|
-
- the route, task, and main structure stay the same;
|
|
33
|
-
- the variation is deterministic and can be recreated from the captured base;
|
|
34
|
-
- choosing it does not depend on an undocumented sequence;
|
|
35
|
-
- the final visual state remains visible long enough to capture.
|
|
36
|
-
|
|
37
|
-
Pseudo states such as hover, keyboard focus, and pointer active are always
|
|
38
|
-
interaction states. They must not become duplicate frames.
|
|
39
|
-
|
|
40
|
-
## Frame size
|
|
41
|
-
|
|
42
|
-
The W and H controls in the frame inspector change the frame viewport, not the
|
|
43
|
-
canvas camera. A committed size is part of the frame fingerprint and its exact
|
|
44
|
-
capture recipe. The request carries the route, width, height, conditions,
|
|
45
|
-
desired state, and interactions that produced that fingerprint. A returned
|
|
46
|
-
artifact is seeded under that requested recipe instead of the authored catalog
|
|
47
|
-
key.
|
|
48
|
-
|
|
49
|
-
This distinction is observable: resizing a frame may change responsive layout,
|
|
50
|
-
while changing canvas zoom only changes how large the same frame appears in the
|
|
51
|
-
editor. Hosts that implement an on-demand capture adapter must apply the
|
|
52
|
-
requested `route`, `width`, and `height` overrides before launching the capture.
|
|
53
|
-
|
|
54
|
-
## Automatic pseudo-state coverage
|
|
55
|
-
|
|
56
|
-
Pygmalion discovers pseudo-state surfaces while it serializes a screen. Every
|
|
57
|
-
rendered target matched by `:hover`, `:focus`, `:focus-visible`,
|
|
58
|
-
`:focus-within`, or `:active` is stamped into the inert DOM preview. React
|
|
59
|
-
hover and focus handlers are discovered from the rendered element props; the
|
|
60
|
-
inspect transform also marks native JSX event surfaces. The right panel's
|
|
61
|
-
variant matrix shows the chosen target as an **Elements** row with Default,
|
|
62
|
-
Hover, Focus, and Pressed options as applicable; a filterable picker chooses
|
|
63
|
-
the target, and picking an element with Inspect on chooses it from the canvas.
|
|
64
|
-
|
|
65
|
-
Discovery does not apply a semantic or size threshold. A plain button whose
|
|
66
|
-
only hover effect is a color, border, shadow, or opacity change belongs in the
|
|
67
|
-
panel just as much as a title that reveals an edit affordance. Transparent hit
|
|
68
|
-
areas are included when they occupy layout space. This makes pseudo-state
|
|
69
|
-
coverage a property of the captured UI instead of a list each host must keep
|
|
70
|
-
in sync by hand.
|
|
71
|
-
|
|
72
|
-
Hosts still declare an interaction axis when the endpoint needs several steps,
|
|
73
|
-
a product-specific label, or behavior beyond a pseudo gesture. If a declared
|
|
74
|
-
axis targets the same element and pseudo action, Pygmalion suppresses that
|
|
75
|
-
automatic duplicate. Click-driven toggles and other application states remain
|
|
76
|
-
declared axes unless they already have their own review frame.
|
|
77
|
-
|
|
78
|
-
In view-only mode, `requires.testId` is matched against the frozen preview's
|
|
79
|
-
lightweight **reachable** test-id index as well as an imported layer tree. A
|
|
80
|
-
test id under `display: none`, hidden visibility, zero opacity, or an inert
|
|
81
|
-
ancestor does not qualify merely because React kept that branch mounted. This
|
|
82
|
-
keeps right-panel state controls available without turning selection into a
|
|
83
|
-
layer import or live boot, while hiding axes that the captured frame cannot
|
|
84
|
-
actually reach. Legacy snapshots without a capture-time index are measured
|
|
85
|
-
after their inert DOM mounts.
|
|
86
|
-
|
|
87
|
-
## Automatic motion coverage
|
|
88
|
-
|
|
89
|
-
Pygmalion discovers visible CSS animation owners, animated pseudo elements,
|
|
90
|
-
matching animation rules, and Web Animations API targets while it serializes a
|
|
91
|
-
screen, and stamps every target into the inert preview. The stamps are what
|
|
92
|
-
the capture runtime settles: finite animations at their terminal state,
|
|
93
|
-
repeating animations at the start of their cycle. This keeps entrance motion
|
|
94
|
-
from hiding the base UI while screenshots and visual comparisons remain
|
|
95
|
-
repeatable, and a mounted frozen preview holds the same baseline.
|
|
96
|
-
|
|
97
|
-
Motion discovery has no semantic or size threshold. Small progress dots,
|
|
98
|
-
ordinary status indicators, shimmer bars, and full-surface animation are
|
|
99
|
-
settled the same way. There are no playback controls in the editor: a live
|
|
100
|
-
frame runs the real application, so its motion plays as it does in the
|
|
101
|
-
product, and a capture is by definition still.
|
|
102
|
-
|
|
103
|
-
An animation does not need duplicate frames merely to show several points in
|
|
104
|
-
its cycle. Preserve a separate frame only when the motion ends in an independent
|
|
105
|
-
task, journey checkpoint, structural composition, or other state that already
|
|
106
|
-
meets the frame criteria above.
|
|
107
|
-
|
|
108
|
-
## Required axis classification
|
|
109
|
-
|
|
110
|
-
`kind` is mandatory and is enforced when the editor installs the axes.
|
|
111
|
-
`validateScreenStateAxes` is also exported for host validation gates.
|
|
112
|
-
|
|
113
|
-
```ts
|
|
114
|
-
import type { ScreenStateAxisDef } from '@pygmalionjs/pygmalion';
|
|
115
|
-
|
|
116
|
-
export const screenStateAxes: readonly ScreenStateAxisDef[] = [
|
|
117
|
-
{
|
|
118
|
-
id: 'editable-heading',
|
|
119
|
-
label: 'Heading',
|
|
120
|
-
kind: 'interaction',
|
|
121
|
-
requires: { testId: 'editable-heading' },
|
|
122
|
-
options: [
|
|
123
|
-
{ id: 'rest', label: 'Rest' },
|
|
124
|
-
{
|
|
125
|
-
id: 'hover',
|
|
126
|
-
label: 'Hover',
|
|
127
|
-
steps: [
|
|
128
|
-
{
|
|
129
|
-
action: 'hover',
|
|
130
|
-
label: 'Hover heading',
|
|
131
|
-
selector: '[data-testid="editable-heading"]',
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
id: 'editing',
|
|
137
|
-
label: 'Editing',
|
|
138
|
-
steps: [
|
|
139
|
-
{
|
|
140
|
-
action: 'click',
|
|
141
|
-
label: 'Open heading editor',
|
|
142
|
-
selector: '[data-testid="editable-heading"]',
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
action: 'wait',
|
|
146
|
-
label: 'Heading input is visible',
|
|
147
|
-
selector: '[data-testid="heading-input"]',
|
|
148
|
-
},
|
|
149
|
-
],
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
id: 'collapsed',
|
|
153
|
-
label: 'Collapsed',
|
|
154
|
-
desiredState: 'collapsed',
|
|
155
|
-
},
|
|
156
|
-
],
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
id: 'results',
|
|
160
|
-
label: 'Results',
|
|
161
|
-
kind: 'condition',
|
|
162
|
-
options: [
|
|
163
|
-
{ id: 'loaded', label: 'Loaded' },
|
|
164
|
-
{
|
|
165
|
-
id: 'failed',
|
|
166
|
-
label: 'Failed',
|
|
167
|
-
environment: {
|
|
168
|
-
network: {
|
|
169
|
-
conditions: [
|
|
170
|
-
{ match: '/api/results', outcome: { kind: 'fail', status: 500 } },
|
|
171
|
-
],
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
],
|
|
176
|
-
},
|
|
177
|
-
];
|
|
178
|
-
|
|
179
|
-
<PygmalionEditor screenStateAxes={screenStateAxes} />;
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
An interaction option cannot declare `environment`. Every non-base condition
|
|
183
|
-
option must declare one. A direct `desiredState` value cannot be combined with
|
|
184
|
-
steps or an environment on the same option. An interaction option ending in
|
|
185
|
-
`fill` is rejected unless a later targeted `wait` proves that the input reached
|
|
186
|
-
a distinct visual result. This keeps empty strings and maximum-length samples
|
|
187
|
-
out of the state panel while still allowing a search input to reproduce a real
|
|
188
|
-
empty-result state.
|
|
189
|
-
|
|
190
|
-
## Direct desired state
|
|
191
|
-
|
|
192
|
-
Use direct desired state for a locally controlled endpoint whose final visual
|
|
193
|
-
state matters more than the gestures used to reach it. Pygmalion builds a
|
|
194
|
-
complete map from selected axes, such as `{ sidebar: 'collapsed' }`, and calls
|
|
195
|
-
the host's `executeDesiredState` adapter after mount and before any remaining
|
|
196
|
-
interaction steps. The map participates in capture and preview identity.
|
|
197
|
-
|
|
198
|
-
The adapter must replace its whole preview state, not merge it. Pygmalion calls
|
|
199
|
-
it with `{}` as well, so a warm iframe reused for another frame cannot retain a
|
|
200
|
-
previous override. A typical React host exposes the map through an external
|
|
201
|
-
store and lets each component read only its own axis. The adapter is preview
|
|
202
|
-
infrastructure: it must not dispatch the application's production store or
|
|
203
|
-
write persistent product state. When a reviewer uses the real control, the
|
|
204
|
-
component should release its preview override and continue from the currently
|
|
205
|
-
displayed value.
|
|
206
|
-
|
|
207
|
-
The right-panel selection starts delivery in the same input task. Once one
|
|
208
|
-
option has a live document, sibling direct-state options share that document:
|
|
209
|
-
Pygmalion skips boot, route entry, gesture replay, assertions, and geometry
|
|
210
|
-
settling, then sends the replacement map to the host adapter. The React commit
|
|
211
|
-
is the presentation boundary; snapshot serialization may continue afterward
|
|
212
|
-
without blocking the visible state. A live direct-state document is pinned
|
|
213
|
-
against normal warm-pool eviction and cannot be claimed through gesture-prefix
|
|
214
|
-
matching, because those signatures do not describe host-owned state.
|
|
215
|
-
Sibling delivery calls the live adapter directly rather than joining the replay
|
|
216
|
-
runner's FIFO. Frames that expose a direct-state axis also reserve their
|
|
217
|
-
activation-warmed document from unrelated gesture prefetch and align it
|
|
218
|
-
invisibly behind the frozen frame. The first selection can therefore reveal the
|
|
219
|
-
already-positioned React document after its adapter commit; speculative work
|
|
220
|
-
cannot reintroduce a queue between the panel click and that commit.
|
|
221
|
-
|
|
222
|
-
Keep gesture steps when the transition itself is under review, when effects
|
|
223
|
-
outside local render state establish the endpoint, or when the host has no safe
|
|
224
|
-
preview-only adapter for that state.
|
|
225
|
-
|
|
226
|
-
## Scroll positions
|
|
227
|
-
|
|
228
|
-
A stable scroll endpoint is an interaction state. Use a `scroll` recipe step
|
|
229
|
-
with absolute `scrollX` and/or `scrollY` coordinates. With no selector the step
|
|
230
|
-
restores the document viewport; a selector targets a specific overflow
|
|
231
|
-
container. An omitted axis keeps its current position.
|
|
232
|
-
|
|
233
|
-
Absolute coordinates make the endpoint repeatable in both the in-editor replay
|
|
234
|
-
and the capture worker. A free wheel gesture remains transient until the host
|
|
235
|
-
preserves its endpoint as a recipe. The frame inspector enumerates the document
|
|
236
|
-
viewport and independently scrollable nested surfaces. Preserving one records
|
|
237
|
-
both its stable selector and coordinates; the target picker keeps nested list
|
|
238
|
-
or panel scroll distinct from the document viewport.
|
|
239
|
-
|
|
240
|
-
## Held pseudo states
|
|
241
|
-
|
|
242
|
-
Recipes support `hover`, `focus-visible`, and `active` in addition to `focus`.
|
|
243
|
-
These actions remain held through capture. The browser runner dispatches the
|
|
244
|
-
corresponding events, the Playwright worker performs the real pointer or focus
|
|
245
|
-
gesture, and frozen DOM previews materialize `:hover`, `:focus`,
|
|
246
|
-
`:focus-visible`, `:focus-within`, and `:active` selectors as scoped data
|
|
247
|
-
attributes. A DOM preview therefore preserves the same pseudo-state styling as
|
|
248
|
-
the captured application rather than relying on the editor's current pointer.
|
|
249
|
-
|
|
250
|
-
A held pseudo action must be terminal; only `wait` steps may follow it. This
|
|
251
|
-
prevents a declaration from claiming a transient hover that a later click has
|
|
252
|
-
already replaced.
|