@hyperframes/studio 0.8.24 → 0.8.26
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/assets/{hyperframes-player-CTQCs_KG.js → hyperframes-player-C-CArn13.js} +1 -1
- package/dist/assets/{index-okdE01ky.js → index-BIXyJgK5.js} +1 -1
- package/dist/assets/{index-Bq3M0sjr.js → index-D5yni1t6.js} +218 -218
- package/dist/assets/index-DnRfAiK2.css +1 -0
- package/dist/assets/{index-9WJFcXTP.js → index-pfeP2Y63.js} +1 -1
- package/dist/{chunk-6BT6DTB4.js → chunk-A7S5SGNA.js} +2 -2
- package/dist/chunk-A7S5SGNA.js.map +1 -0
- package/dist/{domEditingLayers-URA7BLWE.js → domEditingLayers-JMMVOGTO.js} +2 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.html +2 -2
- package/dist/index.js +6230 -5019
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/components/editor/TopologyLens.test.tsx +294 -0
- package/src/components/editor/TopologyLens.tsx +206 -0
- package/src/components/editor/domEditOverlayGeometry.ts +14 -0
- package/src/components/editor/domEditOverlayGeometryBatch.test.ts +18 -0
- package/src/components/editor/domEditingLayers.test.ts +9 -0
- package/src/components/editor/domEditingLayers.ts +8 -8
- package/src/components/editor/topologyLensGeometry.test.ts +44 -0
- package/src/components/editor/topologyLensGeometry.ts +119 -0
- package/src/components/editor/topologyLensState.test.ts +116 -0
- package/src/components/editor/topologyLensState.ts +72 -0
- package/src/components/nle/PreviewOverlays.topologyLens.test.tsx +126 -0
- package/src/components/nle/PreviewOverlays.tsx +24 -18
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +16 -0
- package/src/components/sidebar/CompositionsTab.tsx +10 -3
- package/src/components/ui/HyperframesLoader.tsx +3 -48
- package/src/components/ui/HyperframesMark.tsx +53 -0
- package/src/contexts/DomEditContext.tsx +12 -0
- package/src/hooks/domEditCommitRunner.test.ts +59 -0
- package/src/hooks/domEditCommitRunner.ts +55 -20
- package/src/hooks/domEditCommitTypes.ts +8 -1
- package/src/hooks/domEditTextCommitPlan.ts +47 -0
- package/src/hooks/timelineTrackVisibility.test.ts +7 -1
- package/src/hooks/timelineTrackVisibility.ts +5 -5
- package/src/hooks/useDomEditAttributeCommits.ts +4 -1
- package/src/hooks/useDomEditCommits.test.tsx +243 -8
- package/src/hooks/useDomEditCommits.ts +69 -14
- package/src/hooks/useDomEditPositionPatchCommit.test.tsx +3 -4
- package/src/hooks/useDomEditPositionPatchCommit.ts +27 -29
- package/src/hooks/useDomEditSession.ts +6 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +86 -2
- package/src/hooks/useDomEditTextCommits.ts +64 -86
- package/src/hooks/useDomEditWiring.ts +1 -1
- package/src/hooks/useDomSelection.ts +2 -0
- package/src/hooks/useDomSelectionTypes.ts +2 -0
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +71 -4
- package/src/hooks/useExternalFileChangeCoordinator.ts +18 -9
- package/src/hooks/useGsapAnimationFetchFallback.test.ts +13 -1
- package/src/hooks/useGsapAnimationFetchFallback.ts +7 -3
- package/src/hooks/useGsapAnimationOps.test.tsx +101 -0
- package/src/hooks/useGsapAnimationOps.ts +4 -4
- package/src/hooks/useGsapAwareEditing.test.tsx +30 -2
- package/src/hooks/useGsapAwareEditing.ts +31 -10
- package/src/hooks/useGsapSelectionHandlers.test.tsx +3 -1
- package/src/hooks/useGsapSelectionHandlers.ts +21 -11
- package/src/hooks/useRenderClipContent.test.ts +27 -0
- package/src/hooks/useRenderClipContent.ts +5 -0
- package/src/hooks/useStudioExternalFileChanges.ts +5 -0
- package/src/player/components/CompositionThumbnail.test.ts +66 -1
- package/src/player/components/CompositionThumbnail.tsx +6 -0
- package/src/player/hooks/timelineSyncHydration.ts +10 -2
- package/src/player/store/playerStore.test.ts +24 -0
- package/src/player/store/thumbnailSlice.ts +6 -0
- package/src/styles/studio.css +179 -0
- package/src/utils/domEditSaveQueue.test.ts +5 -3
- package/src/utils/domEditSaveQueue.ts +6 -1
- package/src/webmcp/StudioAgentTools.test.ts +70 -0
- package/src/webmcp/StudioAgentTools.tsx +89 -21
- package/src/webmcp/handles.test.ts +172 -15
- package/src/webmcp/handles.ts +152 -35
- package/src/webmcp/tools/animationTools.test.ts +241 -60
- package/src/webmcp/tools/animationTools.ts +191 -104
- package/src/webmcp/tools/contentTools.test.ts +103 -41
- package/src/webmcp/tools/contentTools.ts +149 -105
- package/src/webmcp/tools/inspectTools.test.ts +47 -5
- package/src/webmcp/tools/inspectTools.ts +59 -19
- package/src/webmcp/tools/lookTools.test.ts +185 -38
- package/src/webmcp/tools/lookTools.ts +134 -21
- package/src/webmcp/tools/selectionTools.test.ts +105 -9
- package/src/webmcp/tools/selectionTools.ts +34 -19
- package/src/webmcp/tools/targetedWriteTools.test.ts +125 -0
- package/src/webmcp/tools/transformTools.test.ts +140 -54
- package/src/webmcp/tools/transformTools.ts +215 -56
- package/src/webmcp/useStudioAgentTools.test.tsx +310 -10
- package/src/webmcp/useStudioAgentTools.ts +27 -15
- package/src/webmcp/webmcpTestUtils.ts +37 -0
- package/src/webmcp/writeCoordinator.test.ts +375 -0
- package/src/webmcp/writeCoordinator.ts +473 -0
- package/dist/assets/index-yGhfxxoL.css +0 -1
- package/dist/chunk-6BT6DTB4.js.map +0 -1
- /package/dist/{domEditingLayers-URA7BLWE.js.map → domEditingLayers-JMMVOGTO.js.map} +0 -0
|
@@ -1,9 +1,49 @@
|
|
|
1
|
-
import { useCallback, useMemo } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo } from "react";
|
|
2
2
|
import { useDomEditActionsContext, useDomEditSelectionContext } from "../contexts/DomEditContext";
|
|
3
3
|
import { useStudioShellContext } from "../contexts/StudioContext";
|
|
4
4
|
import { usePlayerStore } from "../player";
|
|
5
5
|
import { useStudioAgentTools, type StudioAgentToolsDeps } from "./useStudioAgentTools";
|
|
6
|
-
import type
|
|
6
|
+
import { collectStudioLookScene, type StudioLookSnapshot } from "./tools/lookTools";
|
|
7
|
+
import { studioEditLifecycle } from "./writeCoordinator";
|
|
8
|
+
import { findElementForSelection } from "../components/editor/domEditingElement";
|
|
9
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
10
|
+
import {
|
|
11
|
+
applyStudioBoxSizeDraft,
|
|
12
|
+
captureStudioBoxSize,
|
|
13
|
+
restoreStudioBoxSize,
|
|
14
|
+
} from "../components/editor/manualEdits";
|
|
15
|
+
|
|
16
|
+
export function readLiveSelectionBox(
|
|
17
|
+
doc: Document | null | undefined,
|
|
18
|
+
selection: DomEditSelection,
|
|
19
|
+
activeCompositionPath: string | null,
|
|
20
|
+
) {
|
|
21
|
+
const liveElement = doc
|
|
22
|
+
? findElementForSelection(doc, selection, activeCompositionPath)
|
|
23
|
+
: selection.element;
|
|
24
|
+
if (!liveElement) throw new Error("the target is missing from the current Studio preview");
|
|
25
|
+
const rect = liveElement.getBoundingClientRect();
|
|
26
|
+
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ResizeCommit = (
|
|
30
|
+
selection: DomEditSelection,
|
|
31
|
+
next: { width: number; height: number },
|
|
32
|
+
offset?: { x: number; y: number },
|
|
33
|
+
restore?: () => void,
|
|
34
|
+
) => Promise<void>;
|
|
35
|
+
|
|
36
|
+
export function resizeSelectionFromAgent(
|
|
37
|
+
selection: DomEditSelection,
|
|
38
|
+
next: { width: number; height: number },
|
|
39
|
+
commit: ResizeCommit,
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const previous = captureStudioBoxSize(selection.element);
|
|
42
|
+
applyStudioBoxSizeDraft(selection.element, next);
|
|
43
|
+
return commit(selection, next, undefined, () => {
|
|
44
|
+
restoreStudioBoxSize(selection.element, previous);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
7
47
|
|
|
8
48
|
/**
|
|
9
49
|
* Mounts Studio's WebMCP tool surface. Renders nothing.
|
|
@@ -20,6 +60,7 @@ export function StudioAgentTools() {
|
|
|
20
60
|
const { projectId, activeCompPath, editHistory, writeBlockedReason } = useStudioShellContext();
|
|
21
61
|
const {
|
|
22
62
|
domEditSelection,
|
|
63
|
+
activeGroupElement,
|
|
23
64
|
selectedGsapAnimations,
|
|
24
65
|
gsapMultipleTimelines,
|
|
25
66
|
gsapUnsupportedTimelinePattern,
|
|
@@ -28,8 +69,8 @@ export function StudioAgentTools() {
|
|
|
28
69
|
previewIframeRef,
|
|
29
70
|
buildDomSelectionFromTarget,
|
|
30
71
|
applyDomSelection,
|
|
31
|
-
|
|
32
|
-
|
|
72
|
+
handleDomTextCommitForSelection,
|
|
73
|
+
handleDomStyleCommitForSelection,
|
|
33
74
|
handleDomPathOffsetCommit,
|
|
34
75
|
handleDomBoxSizeCommit,
|
|
35
76
|
handleDomRotationCommit,
|
|
@@ -37,8 +78,14 @@ export function StudioAgentTools() {
|
|
|
37
78
|
handleGsapUpdateMeta,
|
|
38
79
|
handleGsapAddKeyframeBatch,
|
|
39
80
|
handleGsapDeleteAnimation,
|
|
81
|
+
getGsapAnimationsForSelection,
|
|
40
82
|
} = useDomEditActionsContext();
|
|
41
83
|
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
studioEditLifecycle.activateProject(projectId);
|
|
86
|
+
return () => studioEditLifecycle.reset();
|
|
87
|
+
}, [projectId]);
|
|
88
|
+
|
|
42
89
|
const getSnapshot = useCallback((): StudioLookSnapshot => {
|
|
43
90
|
const player = usePlayerStore.getState();
|
|
44
91
|
return {
|
|
@@ -48,6 +95,11 @@ export function StudioAgentTools() {
|
|
|
48
95
|
duration: player.duration,
|
|
49
96
|
isPlaying: player.isPlaying,
|
|
50
97
|
elements: player.elements,
|
|
98
|
+
scene: collectStudioLookScene(
|
|
99
|
+
previewIframeRef.current?.contentDocument ?? null,
|
|
100
|
+
activeCompPath,
|
|
101
|
+
activeGroupElement,
|
|
102
|
+
),
|
|
51
103
|
selection: domEditSelection,
|
|
52
104
|
selectionAnimationCount: selectedGsapAnimations.length,
|
|
53
105
|
history: {
|
|
@@ -57,13 +109,21 @@ export function StudioAgentTools() {
|
|
|
57
109
|
redoLabel: editHistory.redoLabel ?? null,
|
|
58
110
|
},
|
|
59
111
|
};
|
|
60
|
-
}, [
|
|
112
|
+
}, [
|
|
113
|
+
projectId,
|
|
114
|
+
activeCompPath,
|
|
115
|
+
domEditSelection,
|
|
116
|
+
activeGroupElement,
|
|
117
|
+
selectedGsapAnimations,
|
|
118
|
+
editHistory,
|
|
119
|
+
previewIframeRef,
|
|
120
|
+
]);
|
|
61
121
|
|
|
62
122
|
const deps = useMemo<StudioAgentToolsDeps>(
|
|
63
123
|
() => ({
|
|
64
124
|
getSnapshot,
|
|
65
125
|
getPreviewDocument: () => previewIframeRef.current?.contentDocument ?? null,
|
|
66
|
-
buildSelection: (element) => buildDomSelectionFromTarget(element),
|
|
126
|
+
buildSelection: (element) => buildDomSelectionFromTarget(element, { exactTarget: true }),
|
|
67
127
|
applySelection: (selection) => applyDomSelection(selection, { revealPanel: true }),
|
|
68
128
|
requestSeek: (time) => usePlayerStore.getState().requestSeek(time),
|
|
69
129
|
readPlayhead: () => {
|
|
@@ -90,22 +150,29 @@ export function StudioAgentTools() {
|
|
|
90
150
|
wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
91
151
|
getCurrentSelection: () => domEditSelection,
|
|
92
152
|
getWriteBlockedReason: () => writeBlockedReason,
|
|
93
|
-
setText: (value, fieldKey) =>
|
|
94
|
-
|
|
153
|
+
setText: (selection, value, fieldKey) =>
|
|
154
|
+
handleDomTextCommitForSelection(selection, value, fieldKey),
|
|
155
|
+
setStyle: (selection, property, value) =>
|
|
156
|
+
handleDomStyleCommitForSelection(selection, property, value),
|
|
95
157
|
// Measured, not authored: the tool compares this before and after to
|
|
96
|
-
// tell a real change from a handler that did nothing and resolved.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
158
|
+
// tell a real change from a handler that did nothing and resolved. A
|
|
159
|
+
// successful commit may replace the preview document, so re-resolve the
|
|
160
|
+
// source-safe selection instead of measuring its detached old node.
|
|
161
|
+
readBox: (selection) =>
|
|
162
|
+
readLiveSelectionBox(previewIframeRef.current?.contentDocument, selection, activeCompPath),
|
|
101
163
|
moveTo: (selection, next) => handleDomPathOffsetCommit(selection, next),
|
|
102
|
-
resizeTo: (selection, next) =>
|
|
164
|
+
resizeTo: (selection, next) =>
|
|
165
|
+
resizeSelectionFromAgent(selection, next, handleDomBoxSizeCommit),
|
|
103
166
|
rotateTo: (selection, next) => handleDomRotationCommit(selection, next),
|
|
104
|
-
addAnimation: (method) => handleGsapAddAnimation(method),
|
|
105
|
-
updateAnimation: (animationId, updates) =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
167
|
+
addAnimation: (selection, method) => handleGsapAddAnimation(method, selection),
|
|
168
|
+
updateAnimation: (selection, animationId, updates) =>
|
|
169
|
+
handleGsapUpdateMeta(animationId, updates, selection),
|
|
170
|
+
addKeyframe: (selection, animationId, percent, properties) =>
|
|
171
|
+
handleGsapAddKeyframeBatch(animationId, percent, properties, undefined, selection),
|
|
172
|
+
deleteAnimation: (selection, animationId) =>
|
|
173
|
+
handleGsapDeleteAnimation(animationId, selection),
|
|
174
|
+
getAnimationsForSelection: async (selection) =>
|
|
175
|
+
await getGsapAnimationsForSelection(selection),
|
|
109
176
|
getGsapDiagnostics: () => ({
|
|
110
177
|
animations: selectedGsapAnimations,
|
|
111
178
|
multipleTimelines: gsapMultipleTimelines,
|
|
@@ -120,8 +187,8 @@ export function StudioAgentTools() {
|
|
|
120
187
|
projectId,
|
|
121
188
|
activeCompPath,
|
|
122
189
|
writeBlockedReason,
|
|
123
|
-
|
|
124
|
-
|
|
190
|
+
handleDomTextCommitForSelection,
|
|
191
|
+
handleDomStyleCommitForSelection,
|
|
125
192
|
handleDomPathOffsetCommit,
|
|
126
193
|
handleDomBoxSizeCommit,
|
|
127
194
|
handleDomRotationCommit,
|
|
@@ -129,6 +196,7 @@ export function StudioAgentTools() {
|
|
|
129
196
|
handleGsapUpdateMeta,
|
|
130
197
|
handleGsapAddKeyframeBatch,
|
|
131
198
|
handleGsapDeleteAnimation,
|
|
199
|
+
getGsapAnimationsForSelection,
|
|
132
200
|
domEditSelection,
|
|
133
201
|
selectedGsapAnimations,
|
|
134
202
|
gsapMultipleTimelines,
|
|
@@ -5,23 +5,15 @@ import {
|
|
|
5
5
|
mintElementHandle,
|
|
6
6
|
parseElementHandle,
|
|
7
7
|
resolveElementHandle,
|
|
8
|
+
resolveLiveHandleSelection,
|
|
8
9
|
timelineElementAddress,
|
|
9
10
|
} from "./handles";
|
|
11
|
+
import { previewDoc } from "./webmcpTestUtils";
|
|
10
12
|
|
|
11
13
|
function timelineElement(overrides: Partial<TimelineElement>): TimelineElement {
|
|
12
14
|
return { id: "synthetic-id", tag: "div", start: 0, duration: 1, track: 0, ...overrides };
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
/** A separate document, standing in for the preview iframe's realm. */
|
|
16
|
-
function previewDoc(html: string): Document {
|
|
17
|
-
const iframe = document.createElement("iframe");
|
|
18
|
-
document.body.append(iframe);
|
|
19
|
-
const doc = iframe.contentDocument;
|
|
20
|
-
if (!doc) throw new Error("expected iframe document");
|
|
21
|
-
doc.body.innerHTML = html;
|
|
22
|
-
return doc;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
17
|
describe("mintElementHandle", () => {
|
|
26
18
|
it("prefers data-hf-id, the stable patch target", () => {
|
|
27
19
|
const handle = mintElementHandle(
|
|
@@ -29,7 +21,7 @@ describe("mintElementHandle", () => {
|
|
|
29
21
|
timelineElement({ hfId: "abc123", domId: "headline", selector: ".title" }),
|
|
30
22
|
),
|
|
31
23
|
);
|
|
32
|
-
expect(handle).toBe("hf:abc123");
|
|
24
|
+
expect(handle).toBe("hf:v1:index.html:index.html:abc123");
|
|
33
25
|
});
|
|
34
26
|
|
|
35
27
|
it("falls back to the DOM id when there is no hf id", () => {
|
|
@@ -37,7 +29,7 @@ describe("mintElementHandle", () => {
|
|
|
37
29
|
mintElementHandle(
|
|
38
30
|
timelineElementAddress(timelineElement({ domId: "headline", selector: ".title" })),
|
|
39
31
|
),
|
|
40
|
-
).toBe("dom:headline");
|
|
32
|
+
).toBe("dom:v1:index.html:index.html:headline");
|
|
41
33
|
});
|
|
42
34
|
|
|
43
35
|
it("falls back to a selector with its occurrence index", () => {
|
|
@@ -45,12 +37,12 @@ describe("mintElementHandle", () => {
|
|
|
45
37
|
mintElementHandle(
|
|
46
38
|
timelineElementAddress(timelineElement({ selector: ".card", selectorIndex: 2 })),
|
|
47
39
|
),
|
|
48
|
-
).toBe("sel:.card#2");
|
|
40
|
+
).toBe("sel:v1:index.html:index.html:.card#2");
|
|
49
41
|
});
|
|
50
42
|
|
|
51
43
|
it("defaults a missing occurrence index to the first match", () => {
|
|
52
44
|
expect(mintElementHandle(timelineElementAddress(timelineElement({ selector: ".card" })))).toBe(
|
|
53
|
-
"sel:.card#0",
|
|
45
|
+
"sel:v1:index.html:index.html:.card#0",
|
|
54
46
|
);
|
|
55
47
|
});
|
|
56
48
|
|
|
@@ -60,17 +52,110 @@ describe("mintElementHandle", () => {
|
|
|
60
52
|
});
|
|
61
53
|
});
|
|
62
54
|
|
|
55
|
+
describe("resolveLiveHandleSelection", () => {
|
|
56
|
+
it("reports a transient change when the replacement preview has not rebuilt the target", async () => {
|
|
57
|
+
const firstDoc = previewDoc('<div id="headline">before</div>');
|
|
58
|
+
const replacementDoc = previewDoc("<main>reloading</main>");
|
|
59
|
+
let currentDoc = firstDoc;
|
|
60
|
+
|
|
61
|
+
const result = await resolveLiveHandleSelection(
|
|
62
|
+
() => currentDoc,
|
|
63
|
+
"dom:headline",
|
|
64
|
+
async (element) => {
|
|
65
|
+
currentDoc = replacementDoc;
|
|
66
|
+
return { element };
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(result).toEqual({ status: "changed" });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("reports a stable missing target as not found", async () => {
|
|
74
|
+
const doc = previewDoc("<main>settled</main>");
|
|
75
|
+
|
|
76
|
+
const result = await resolveLiveHandleSelection(
|
|
77
|
+
() => doc,
|
|
78
|
+
"dom:headline",
|
|
79
|
+
async (element) => ({ element }),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(result).toEqual({ status: "not-found" });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
63
86
|
describe("parseElementHandle", () => {
|
|
87
|
+
it("round-trips source ownership and the active composition", () => {
|
|
88
|
+
expect(
|
|
89
|
+
parseElementHandle(
|
|
90
|
+
mintElementHandle({
|
|
91
|
+
domId: "headline:hero",
|
|
92
|
+
sourceFile: "compositions/hero.html",
|
|
93
|
+
activeCompositionPath: "index.html",
|
|
94
|
+
})!,
|
|
95
|
+
),
|
|
96
|
+
).toEqual({
|
|
97
|
+
scheme: "dom",
|
|
98
|
+
version: 1,
|
|
99
|
+
value: "headline:hero",
|
|
100
|
+
index: 0,
|
|
101
|
+
sourceFile: "compositions/hero.html",
|
|
102
|
+
activeCompositionPath: "index.html",
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("encodes path, selector, colon, and hash delimiters without ambiguity", () => {
|
|
107
|
+
const handle = mintElementHandle({
|
|
108
|
+
selector: '#hero[data-label="a:b"] > .card:nth-child(2)',
|
|
109
|
+
selectorIndex: 3,
|
|
110
|
+
sourceFile: "compositions/a:b#hero.html",
|
|
111
|
+
activeCompositionPath: "scenes/root:wide.html",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(parseElementHandle(handle!)).toEqual({
|
|
115
|
+
scheme: "sel",
|
|
116
|
+
version: 1,
|
|
117
|
+
value: '#hero[data-label="a:b"] > .card:nth-child(2)',
|
|
118
|
+
index: 3,
|
|
119
|
+
sourceFile: "compositions/a:b#hero.html",
|
|
120
|
+
activeCompositionPath: "scenes/root:wide.html",
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
64
124
|
it("splits the index off the LAST hash, so id selectors survive", () => {
|
|
65
125
|
expect(parseElementHandle("sel:#card > .title#3")).toEqual({
|
|
66
126
|
scheme: "sel",
|
|
127
|
+
version: 0,
|
|
67
128
|
value: "#card > .title",
|
|
68
129
|
index: 3,
|
|
69
130
|
});
|
|
70
131
|
});
|
|
71
132
|
|
|
72
133
|
it("treats a selector with no index as the first match", () => {
|
|
73
|
-
expect(parseElementHandle("sel:.card")).toEqual({
|
|
134
|
+
expect(parseElementHandle("sel:.card")).toEqual({
|
|
135
|
+
scheme: "sel",
|
|
136
|
+
version: 0,
|
|
137
|
+
value: ".card",
|
|
138
|
+
index: 0,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("round-trips the project identity in a writable v2 handle", () => {
|
|
143
|
+
const handle = mintElementHandle({
|
|
144
|
+
projectId: "project:demo",
|
|
145
|
+
domId: "headline",
|
|
146
|
+
sourceFile: "index.html",
|
|
147
|
+
activeCompositionPath: "index.html",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(parseElementHandle(handle!)).toEqual({
|
|
151
|
+
scheme: "dom",
|
|
152
|
+
version: 2,
|
|
153
|
+
projectId: "project:demo",
|
|
154
|
+
value: "headline",
|
|
155
|
+
sourceFile: "index.html",
|
|
156
|
+
activeCompositionPath: "index.html",
|
|
157
|
+
index: 0,
|
|
158
|
+
});
|
|
74
159
|
});
|
|
75
160
|
|
|
76
161
|
it("rejects an unknown scheme", () => {
|
|
@@ -106,6 +191,78 @@ describe("resolveElementHandle", () => {
|
|
|
106
191
|
expect(resolved instanceof HTMLElement).toBe(false);
|
|
107
192
|
});
|
|
108
193
|
|
|
194
|
+
it("distinguishes duplicate authored ids by source file", () => {
|
|
195
|
+
const doc = previewDoc(
|
|
196
|
+
`<main data-composition-id="root" data-composition-file="index.html">
|
|
197
|
+
<div id="duplicate">root</div>
|
|
198
|
+
<section data-composition-id="nested" data-composition-file="compositions/nested.html">
|
|
199
|
+
<div id="duplicate">nested</div>
|
|
200
|
+
</section>
|
|
201
|
+
</main>`,
|
|
202
|
+
);
|
|
203
|
+
const rootHandle = mintElementHandle({
|
|
204
|
+
domId: "duplicate",
|
|
205
|
+
sourceFile: "index.html",
|
|
206
|
+
activeCompositionPath: "index.html",
|
|
207
|
+
});
|
|
208
|
+
const nestedHandle = mintElementHandle({
|
|
209
|
+
domId: "duplicate",
|
|
210
|
+
sourceFile: "compositions/nested.html",
|
|
211
|
+
activeCompositionPath: "index.html",
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(rootHandle).not.toBe(nestedHandle);
|
|
215
|
+
expect(resolveElementHandle(doc, rootHandle!)?.textContent).toBe("root");
|
|
216
|
+
expect(resolveElementHandle(doc, nestedHandle!)?.textContent).toBe("nested");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("keeps selector occurrence indexes scoped to their source file", () => {
|
|
220
|
+
const doc = previewDoc(
|
|
221
|
+
`<main data-composition-id="root" data-composition-file="index.html">
|
|
222
|
+
<div class="card">root first</div><div class="card">root second</div>
|
|
223
|
+
<section data-composition-id="nested" data-composition-file="compositions/nested.html">
|
|
224
|
+
<div class="card">nested first</div><div class="card">nested second</div>
|
|
225
|
+
</section>
|
|
226
|
+
</main>`,
|
|
227
|
+
);
|
|
228
|
+
const rootSecond = mintElementHandle({
|
|
229
|
+
selector: ".card",
|
|
230
|
+
selectorIndex: 1,
|
|
231
|
+
sourceFile: "index.html",
|
|
232
|
+
activeCompositionPath: "index.html",
|
|
233
|
+
});
|
|
234
|
+
const nestedSecond = mintElementHandle({
|
|
235
|
+
selector: ".card",
|
|
236
|
+
selectorIndex: 1,
|
|
237
|
+
sourceFile: "compositions/nested.html",
|
|
238
|
+
activeCompositionPath: "index.html",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(resolveElementHandle(doc, rootSecond!)?.textContent).toBe("root second");
|
|
242
|
+
expect(resolveElementHandle(doc, nestedSecond!)?.textContent).toBe("nested second");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("re-resolves a scoped handle against the live document after reload", () => {
|
|
246
|
+
const handle = mintElementHandle({
|
|
247
|
+
domId: "headline",
|
|
248
|
+
sourceFile: "index.html",
|
|
249
|
+
activeCompositionPath: "index.html",
|
|
250
|
+
});
|
|
251
|
+
const firstDoc = previewDoc(
|
|
252
|
+
'<main data-composition-id="root" data-composition-file="index.html"><div id="headline">before</div></main>',
|
|
253
|
+
);
|
|
254
|
+
const first = resolveElementHandle(firstDoc, handle!);
|
|
255
|
+
const reloadedDoc = previewDoc(
|
|
256
|
+
'<main data-composition-id="root" data-composition-file="index.html"><div id="headline">after</div></main>',
|
|
257
|
+
);
|
|
258
|
+
const reloaded = resolveElementHandle(reloadedDoc, handle!);
|
|
259
|
+
|
|
260
|
+
expect(first?.textContent).toBe("before");
|
|
261
|
+
expect(reloaded?.textContent).toBe("after");
|
|
262
|
+
expect(reloaded).not.toBe(first);
|
|
263
|
+
expect(reloaded?.ownerDocument).toBe(reloadedDoc);
|
|
264
|
+
});
|
|
265
|
+
|
|
109
266
|
it("returns null for a handle that no longer matches", () => {
|
|
110
267
|
const doc = previewDoc('<div id="headline">A</div>');
|
|
111
268
|
expect(resolveElementHandle(doc, "dom:deleted")).toBeNull();
|