@pygmalionjs/pygmalion 0.6.28 → 0.6.29
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/dist-lib/{FrozenRoutePreview-Xm7o3urC.js → FrozenRoutePreview-D9J35QsM.js} +2842 -2687
- package/dist-lib/pygmalion.js +7002 -6734
- package/dist-lib/testing.js +1 -1
- package/dist-lib/types/canvas/ShadowRoutePreview.d.ts +3 -0
- package/dist-lib/types/editor/flowSessions.d.ts +3 -1
- package/dist-lib/types/editor/frameInteraction.d.ts +7 -1
- package/dist-lib/types/editor/interactiveSessionSurface.d.ts +19 -0
- package/dist-lib/types/editor/interactiveStates.d.ts +3 -2
- package/dist-lib/types/editor/previewReachability.d.ts +7 -0
- package/dist-lib/types/editor/store.d.ts +14 -2
- package/docs/screen-state-contract.md +7 -4
- package/node/storyboard-capture-runtime.mjs +50 -0
- package/package.json +1 -1
package/dist-lib/testing.js
CHANGED
|
@@ -17,4 +17,7 @@ export declare const ShadowRoutePreview: import("react").FunctionComponent<{
|
|
|
17
17
|
mountPriority?: number;
|
|
18
18
|
onDocumentHeightChange?: (height: number) => void;
|
|
19
19
|
onReplayIssuesChange?: (issues: string[]) => void;
|
|
20
|
+
onReachableTestIdsChange?: (testIds: readonly string[]) => void;
|
|
21
|
+
/** Enables immediate scrolling while the live React document warms. */
|
|
22
|
+
interactionEnabled?: boolean;
|
|
20
23
|
}>;
|
|
@@ -144,7 +144,9 @@ export declare const __flowSessionWarmDebug: {
|
|
|
144
144
|
bootsByRunner: Record<string, number>;
|
|
145
145
|
};
|
|
146
146
|
/** Drops parked instances globally, or only those owned by one runner. */
|
|
147
|
-
export declare function evictFlowSessionWarmInstances(ownerLabel?: string
|
|
147
|
+
export declare function evictFlowSessionWarmInstances(ownerLabel?: string, options?: {
|
|
148
|
+
preservePinned?: boolean;
|
|
149
|
+
}): void;
|
|
148
150
|
/**
|
|
149
151
|
* True when a walk of this path would resume on a parked instance. Activation
|
|
150
152
|
* warming reads it to decide whether the frame already has a resumable
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type FrameInteractionPhase = 'idle' | 'starting' | 'interacting';
|
|
1
|
+
export type FrameInteractionPhase = 'idle' | 'starting' | 'preview' | 'interacting';
|
|
2
2
|
export interface FrameInteractionScrollState {
|
|
3
3
|
x: number;
|
|
4
4
|
y: number;
|
|
@@ -51,6 +51,12 @@ export declare function subscribeFrameInteraction(listener: () => void): () => v
|
|
|
51
51
|
* without learning whether the running surface is an iframe or another host.
|
|
52
52
|
*/
|
|
53
53
|
export declare function connectFrameInteractionViewport(frameId: string, adapter: FrameInteractionViewportAdapter): () => void;
|
|
54
|
+
/**
|
|
55
|
+
* Makes the frozen DOM useful immediately while its live React document warms.
|
|
56
|
+
* The preview is a scroll-only bridge: navigation and form submission stay
|
|
57
|
+
* contained, and the live iframe later replaces it without ending the session.
|
|
58
|
+
*/
|
|
59
|
+
export declare function connectFrameInteractionPreview(frameId: string, root: ShadowRoot, contentElement: HTMLElement): () => void;
|
|
54
60
|
/** Scrolls the running frame viewport and immediately republishes its reading. */
|
|
55
61
|
export declare function setFrameInteractionScroll(position: {
|
|
56
62
|
x?: number;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* published, while the active frame may mirror the running document
|
|
7
7
|
* immediately so capture stability never becomes interaction latency.
|
|
8
8
|
*/
|
|
9
|
+
import type { DesignScreenDesiredState } from './designImport';
|
|
9
10
|
export interface InteractiveSessionSurface {
|
|
10
11
|
iframe: HTMLIFrameElement;
|
|
11
12
|
screenId: string;
|
|
@@ -19,8 +20,26 @@ export declare function directInteractiveSessionSurfaceId(pageId: string, stateI
|
|
|
19
20
|
export declare function setDirectInteractiveStateRequestHandler(handler: ((pageId: string) => void) | null): void;
|
|
20
21
|
/** Starts direct-state delivery in the same task as the right-panel click. */
|
|
21
22
|
export declare function requestDirectInteractiveState(pageId: string): boolean;
|
|
23
|
+
/** Installs the paint-first path used before the observable page recipe changes. */
|
|
24
|
+
export declare function setDirectInteractiveStatePreviewHandler(handler: ((request: {
|
|
25
|
+
pageId: string;
|
|
26
|
+
stateId: string;
|
|
27
|
+
desiredState: DesignScreenDesiredState;
|
|
28
|
+
}) => boolean) | null): void;
|
|
29
|
+
/** Applies a direct host state without invalidating the editor model yet. */
|
|
30
|
+
export declare function requestDirectInteractiveStatePreview(request: {
|
|
31
|
+
pageId: string;
|
|
32
|
+
stateId: string;
|
|
33
|
+
desiredState: DesignScreenDesiredState;
|
|
34
|
+
}): boolean;
|
|
22
35
|
/** Exposes a runner-owned iframe without transferring its ownership. */
|
|
23
36
|
export declare function exposeInteractiveSessionSurface(screenId: string, iframe: HTMLIFrameElement): void;
|
|
37
|
+
/**
|
|
38
|
+
* Hands an in-flight prewarm slot to the frame that has just exposed its axis.
|
|
39
|
+
* If preparation has not presented the iframe yet, its first exposure follows
|
|
40
|
+
* the redirect instead of landing in the now-private staging slot.
|
|
41
|
+
*/
|
|
42
|
+
export declare function redirectInteractiveSessionSurface(fromScreenId: string, toScreenId: string): boolean;
|
|
24
43
|
/** Releases a surface only when the caller still owns the registered iframe. */
|
|
25
44
|
export declare function releaseInteractiveSessionSurface(screenId: string, iframe: HTMLIFrameElement): void;
|
|
26
45
|
/** Clears a presentation slot when its page returns to an authored state. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DesignScreenInteraction, DesignSerializable, StoryboardEnvironment } from './designImport';
|
|
2
2
|
import type { PageModel } from './store';
|
|
3
|
+
export { reachablePreviewTestIds } from './previewReachability';
|
|
3
4
|
/**
|
|
4
5
|
* Screen-state axes — same-frame variations reproduced by a gesture or a boot
|
|
5
6
|
* condition.
|
|
@@ -136,10 +137,10 @@ export declare function validateInteractiveStates(defs: readonly InteractiveStat
|
|
|
136
137
|
/** Validates both interaction and condition axes. */
|
|
137
138
|
export declare const validateScreenStateAxes: typeof validateInteractiveStates;
|
|
138
139
|
/** Reads only the control-presence evidence needed by the view-only inspector. */
|
|
139
|
-
export declare function previewMarkupTestIds(markup: string | null | undefined): readonly string[];
|
|
140
|
+
export declare function previewMarkupTestIds(markup: string | null | undefined, bodyAttributes?: string | null): readonly string[];
|
|
140
141
|
/**
|
|
141
142
|
* Axes available on one frame. The test-id requirement is checked against
|
|
142
|
-
* either the imported tree or the frozen preview's
|
|
143
|
+
* either the imported tree or the frozen preview's reachable test-id index.
|
|
143
144
|
* View-only can therefore offer a real screen control without importing the
|
|
144
145
|
* whole layer tree, while a frame without that control still hides the axis.
|
|
145
146
|
*/
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const REACHABLE_TEST_IDS_ATTRIBUTE = "data-pygmalion-reachable-testids";
|
|
2
|
+
/** Test ids whose rendered controls can actually be reached from this frame. */
|
|
3
|
+
export declare function reachablePreviewTestIds(root: ParentNode): readonly string[];
|
|
4
|
+
/** Stamps capture-time reachability without mutating the running document. */
|
|
5
|
+
export declare function stampReachablePreviewTestIds(source: Document, clone: HTMLElement): void;
|
|
6
|
+
/** Reads a capture-time reachability index, or null for a legacy snapshot. */
|
|
7
|
+
export declare function indexedReachablePreviewTestIds(markup: string): readonly string[] | null;
|
|
@@ -221,6 +221,12 @@ export interface PageModel {
|
|
|
221
221
|
interactiveStateId?: string;
|
|
222
222
|
/** The active option is rendered through host desired state, without replay. */
|
|
223
223
|
interactiveStateUsesDesiredState?: boolean;
|
|
224
|
+
/**
|
|
225
|
+
* Imported layers still showing behind a direct live surface. The surface
|
|
226
|
+
* paints first; FrameView consumes this key and invalidates the old layers
|
|
227
|
+
* on the following frame so a large tree cannot block the visible switch.
|
|
228
|
+
*/
|
|
229
|
+
interactiveLayerRefreshKey?: string;
|
|
224
230
|
interactiveOptionId?: string;
|
|
225
231
|
/**
|
|
226
232
|
* Options held together on a combining axis (InteractiveStateDef.multiple).
|
|
@@ -260,7 +266,7 @@ export interface PageModel {
|
|
|
260
266
|
assertions?: DesignScreenAssertion[];
|
|
261
267
|
/** Host-declared warm instance group. Set when the screen state is declared, not replayed. */
|
|
262
268
|
session?: string;
|
|
263
|
-
/**
|
|
269
|
+
/** Reachable test ids observed in the frozen preview before editable layers are imported. */
|
|
264
270
|
previewTestIds?: readonly string[];
|
|
265
271
|
layerImportCount?: number;
|
|
266
272
|
layerImportTruncated?: boolean;
|
|
@@ -467,6 +473,8 @@ export declare class EditorStore {
|
|
|
467
473
|
private pageEditRevisions;
|
|
468
474
|
/** pageId → edit revision at the moment a frozen preview was promoted without a boot (D2). */
|
|
469
475
|
private provisionalLayerImports;
|
|
476
|
+
/** Latest paint-first direct-state commit scheduled for each page. */
|
|
477
|
+
private directStateCommitRevisions;
|
|
470
478
|
/** pageId → source geometry awaiting its post-mount fidelity measurement. */
|
|
471
479
|
private pendingLayerImportGeometry;
|
|
472
480
|
/** View-only motion playback never becomes part of a frame recipe or undo history. */
|
|
@@ -737,7 +745,11 @@ export declare class EditorStore {
|
|
|
737
745
|
* Pure-CSS pseudo options short-circuit through the held-pseudo fast path
|
|
738
746
|
* (applyHeldPseudoInteractiveState) and never touch the recipe.
|
|
739
747
|
*/
|
|
740
|
-
applyInteractiveState(pageId: string, def: InteractiveStateDef, optionId: string
|
|
748
|
+
applyInteractiveState(pageId: string, def: InteractiveStateDef, optionId: string, options?: {
|
|
749
|
+
skipPaintFirstPreview?: boolean;
|
|
750
|
+
}): boolean;
|
|
751
|
+
/** Invalidates retained layers only after their direct live replacement painted. */
|
|
752
|
+
commitInteractiveLayerRefresh(pageId: string, refreshKey: string): boolean;
|
|
741
753
|
/** The root viewport endpoint currently preserved in this frame recipe. */
|
|
742
754
|
frameScrollRecipe(pageId: string): Readonly<{
|
|
743
755
|
x: number;
|
|
@@ -75,10 +75,13 @@ automatic duplicate. Click-driven toggles and other application states remain
|
|
|
75
75
|
declared axes unless they already have their own review frame.
|
|
76
76
|
|
|
77
77
|
In view-only mode, `requires.testId` is matched against the frozen preview's
|
|
78
|
-
lightweight test-id index as well as an imported layer tree.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
lightweight **reachable** test-id index as well as an imported layer tree. A
|
|
79
|
+
test id under `display: none`, hidden visibility, zero opacity, or an inert
|
|
80
|
+
ancestor does not qualify merely because React kept that branch mounted. This
|
|
81
|
+
keeps right-panel state controls available without turning selection into a
|
|
82
|
+
layer import or live boot, while hiding axes that the captured frame cannot
|
|
83
|
+
actually reach. Legacy snapshots without a capture-time index are measured
|
|
84
|
+
after their inert DOM mounts.
|
|
82
85
|
|
|
83
86
|
## Automatic motion coverage
|
|
84
87
|
|
|
@@ -1056,6 +1056,56 @@ export function serializeStoryboardPreviewDocument(
|
|
|
1056
1056
|
const clone = document.documentElement.cloneNode(true);
|
|
1057
1057
|
if (!(clone instanceof HTMLElement)) return null;
|
|
1058
1058
|
|
|
1059
|
+
// A frozen snapshot preserves branches that are mounted but visually absent
|
|
1060
|
+
// (for example, two workspace panels cross-faded with opacity). Presence
|
|
1061
|
+
// alone therefore cannot tell the inspector which controls this captured
|
|
1062
|
+
// frame can actually reach. Record a compact, capture-time visibility index
|
|
1063
|
+
// on the body; older snapshots without it are measured again after mount.
|
|
1064
|
+
try {
|
|
1065
|
+
const reachableTestIds = new Set();
|
|
1066
|
+
const hasRenderedBox = (element) => {
|
|
1067
|
+
const rect = element.getBoundingClientRect();
|
|
1068
|
+
if (rect.width > 0 && rect.height > 0) return true;
|
|
1069
|
+
return [...element.querySelectorAll('*')].some((descendant) => {
|
|
1070
|
+
const descendantRect = descendant.getBoundingClientRect();
|
|
1071
|
+
return descendantRect.width > 0 && descendantRect.height > 0;
|
|
1072
|
+
});
|
|
1073
|
+
};
|
|
1074
|
+
const isReachable = (element) => {
|
|
1075
|
+
for (
|
|
1076
|
+
let current = element;
|
|
1077
|
+
current;
|
|
1078
|
+
current = current.parentElement
|
|
1079
|
+
) {
|
|
1080
|
+
const style = window.getComputedStyle(current);
|
|
1081
|
+
const opacity = Number.parseFloat(style.opacity || '1');
|
|
1082
|
+
if (
|
|
1083
|
+
current.hasAttribute('hidden') ||
|
|
1084
|
+
current.hasAttribute('inert') ||
|
|
1085
|
+
style.display === 'none' ||
|
|
1086
|
+
style.visibility === 'hidden' ||
|
|
1087
|
+
style.visibility === 'collapse' ||
|
|
1088
|
+
(Number.isFinite(opacity) && opacity <= 0)
|
|
1089
|
+
) {
|
|
1090
|
+
return false;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return hasRenderedBox(element);
|
|
1094
|
+
};
|
|
1095
|
+
for (const element of document.documentElement.querySelectorAll('*')) {
|
|
1096
|
+
const testId = element.getAttribute('data-testid');
|
|
1097
|
+
if (testId && isReachable(element)) reachableTestIds.add(testId);
|
|
1098
|
+
}
|
|
1099
|
+
const cloneBody = clone.querySelector('body');
|
|
1100
|
+
cloneBody?.setAttribute(
|
|
1101
|
+
'data-pygmalion-reachable-testids',
|
|
1102
|
+
encodeURIComponent(JSON.stringify([...reachableTestIds].sort())),
|
|
1103
|
+
);
|
|
1104
|
+
} catch {
|
|
1105
|
+
// Reachability metadata is additive. A constrained capture realm may not
|
|
1106
|
+
// implement layout; the mounted-preview fallback handles that snapshot.
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1059
1109
|
const sourceInputs = [...document.querySelectorAll('input')];
|
|
1060
1110
|
const cloneInputs = [...clone.querySelectorAll('input')];
|
|
1061
1111
|
sourceInputs.forEach((source, index) => {
|