@hyperframes/studio 0.8.114 → 0.8.116
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-aXNJKiIg.js → hyperframes-player-mGYvPVpw.js} +1 -1
- package/dist/assets/{index-Bn2RJNTJ.js → index-BWHt0-ir.js} +1 -1
- package/dist/assets/{index-Dz7doz9l.js → index-DYNaXPVf.js} +203 -203
- package/dist/assets/{index-C9N-DGjC.js → index-Dr3hNpvS.js} +1 -1
- package/dist/assets/{index-BYnsJnJV.css → index-t1LH6H5H.css} +1 -1
- package/dist/{chunk-BIUXP3PG.js → chunk-3F47EFCT.js} +142 -277
- package/dist/chunk-3F47EFCT.js.map +1 -0
- package/dist/{chunk-K47E2ZJA.js → chunk-5EKWJZLM.js} +504 -17
- package/dist/chunk-5EKWJZLM.js.map +1 -0
- package/dist/{chunk-VWT7STY3.js → chunk-J6Q572FA.js} +2 -2
- package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-OL7GLSS4.js} +3 -5
- package/dist/index.d.ts +146 -40
- package/dist/index.html +2 -2
- package/dist/index.js +4021 -3591
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +9 -8
- package/src/App.tsx +2 -2
- package/src/captions/hooks/useCaptionSync.ts +10 -6
- package/src/components/LinkedSelectionToggle.tsx +3 -2
- package/src/components/ProjectUnreachableBanner.tsx +3 -3
- package/src/components/dock/Dock.tsx +5 -7
- package/src/components/editor/CanvasContextMenu.tsx +3 -2
- package/src/components/editor/DomEditCropHandles.test.tsx +27 -0
- package/src/components/editor/DomEditCropHandles.tsx +7 -4
- package/src/components/editor/DomEditOverlay.tsx +5 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -10
- package/src/components/editor/MotionPathOverlay.tsx +7 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +12 -2
- package/src/components/editor/SnapToolbar.test.tsx +100 -1
- package/src/components/editor/SnapToolbar.tsx +43 -5
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +48 -1
- package/src/components/editor/audioFxTelemetry.ts +2 -3
- package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
- package/src/components/editor/cropResize.ts +4 -3
- package/src/components/editor/domEditingDom.ts +4 -0
- package/src/components/editor/domEditingLayers.ts +1 -5
- package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
- package/src/components/editor/domGroupMoveBy.ts +95 -0
- package/src/components/editor/dragUndoPaint.test.ts +5 -5
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
- package/src/components/editor/manualEditingAvailability.ts +8 -0
- package/src/components/editor/manualEdits.ts +1 -0
- package/src/components/editor/manualEditsParsing.ts +7 -0
- package/src/components/editor/manualOffsetDrag.ts +5 -6
- package/src/components/editor/marqueeCommit.test.tsx +106 -37
- package/src/components/editor/marqueeCommit.ts +24 -8
- package/src/components/editor/motionPathCommit.test.ts +29 -1
- package/src/components/editor/motionPathCommit.ts +21 -9
- package/src/components/editor/motionPathSelection.test.ts +6 -31
- package/src/components/editor/probeSourceElement.test.ts +45 -0
- package/src/components/editor/probeSourceElement.ts +41 -16
- package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
- package/src/components/editor/propertyPanelFont.tsx +3 -2
- package/src/components/editor/propertyPanelFxAddMenu.tsx +11 -2
- package/src/components/editor/propertyPanelFxCarveModule.tsx +15 -5
- package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +3 -0
- package/src/components/editor/propertyPanelFxNodeParams.tsx +15 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +3 -0
- package/src/components/editor/propertyPanelFxPresetRun.tsx +10 -2
- package/src/components/editor/propertyPanelFxSection.test.tsx +56 -0
- package/src/components/editor/propertyPanelFxSection.tsx +30 -28
- package/src/components/editor/propertyPanelTypes.ts +6 -3
- package/src/components/editor/useColorGradingController.ts +2 -1
- package/src/components/editor/useDomEditNudge.test.tsx +37 -0
- package/src/components/editor/useDomEditNudge.ts +2 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +5 -0
- package/src/components/editor/useDomEditZOrder.ts +12 -2
- package/src/components/editor/useFxCarveNodes.ts +2 -1
- package/src/components/editor/useFxLevelling.ts +2 -1
- package/src/components/feedback/projectProvenance.ts +2 -1
- package/src/components/nle/NLEContext.test.ts +1 -0
- package/src/components/nle/NLEContext.tsx +2 -1
- package/src/components/nle/NLEPreview.tsx +12 -10
- package/src/components/nle/PreviewPoster.tsx +2 -1
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
- package/src/components/renders/useFfmpegStatus.ts +2 -1
- package/src/components/renders/useRenderQueue.ts +5 -4
- package/src/components/sidebar/AssetContextMenu.tsx +4 -4
- package/src/components/sidebar/AssetsTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
- package/src/components/ui/menuStyle.test.ts +61 -0
- package/src/components/ui/menuStyle.ts +10 -0
- package/src/contexts/DomEditContext.tsx +5 -1
- package/src/contexts/TimelineEditContext.tsx +1 -0
- package/src/domEditExports.test.tsx +9 -1
- package/src/hooks/domEditCommitRunner.ts +1 -1
- package/src/hooks/domSelectionTestHarness.ts +23 -0
- package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
- package/src/hooks/domStyleCommit.ts +8 -1
- package/src/hooks/draggedGsapPosition.ts +10 -0
- package/src/hooks/elementOffsetStager.ts +5 -4
- package/src/hooks/gsapDragCommit.test.ts +75 -164
- package/src/hooks/gsapDragCommit.ts +4 -125
- package/src/hooks/gsapDragPositionCommit.ts +36 -384
- package/src/hooks/gsapEditOutcome.ts +22 -2
- package/src/hooks/gsapGestureOutcome.test.ts +161 -0
- package/src/hooks/gsapGestureOutcome.ts +64 -0
- package/src/hooks/gsapGroupPreflight.ts +34 -0
- package/src/hooks/gsapKeyframeCommit.ts +3 -12
- package/src/hooks/gsapMutationClient.ts +4 -3
- package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
- package/src/hooks/gsapParsedTween.test.ts +62 -0
- package/src/hooks/gsapParsedTween.ts +243 -0
- package/src/hooks/gsapPositionDetection.test.ts +78 -0
- package/src/hooks/gsapPositionDetection.ts +52 -33
- package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
- package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
- package/src/hooks/gsapResizeIntercept.test.ts +96 -11
- package/src/hooks/gsapResizeIntercept.ts +79 -168
- package/src/hooks/gsapResizeSweep.test.ts +16 -33
- package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
- package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
- package/src/hooks/gsapRuntimeBridge.ts +105 -131
- package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
- package/src/hooks/gsapScriptCommitTypes.ts +13 -0
- package/src/hooks/gsapShared.ts +7 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +45 -57
- package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
- package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
- package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
- package/src/hooks/gsapValueAtPlayhead.ts +292 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
- package/src/hooks/keyframeCacheAstLoad.ts +8 -6
- package/src/hooks/newTweenTarget.test.ts +41 -76
- package/src/hooks/plainRotation.ts +3 -2
- package/src/hooks/previewPromotionSelection.test.tsx +1 -1
- package/src/hooks/refreshTimelineRowText.test.ts +65 -0
- package/src/hooks/refreshTimelineRowText.ts +29 -0
- package/src/hooks/timelineTimingSync.test.ts +1 -0
- package/src/hooks/timelineTimingSync.ts +2 -1
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
- package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
- package/src/hooks/useAskAgentModal.ts +2 -1
- package/src/hooks/useBlockCatalog.ts +2 -1
- package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
- package/src/hooks/useCaptionDetection.ts +2 -1
- package/src/hooks/useCompositionContentLoader.ts +2 -1
- package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
- package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
- package/src/hooks/useDomEditSession.test.tsx +30 -1
- package/src/hooks/useDomEditSession.ts +10 -0
- package/src/hooks/useDomEditTextCommits.test.tsx +41 -0
- package/src/hooks/useDomEditTextCommits.ts +13 -3
- package/src/hooks/useDomEditWiring.ts +4 -2
- package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
- package/src/hooks/useDomGeometryCommit.ts +8 -4
- package/src/hooks/useDomGeometryCommits.test.tsx +1 -0
- package/src/hooks/useDomGeometryCommits.ts +3 -2
- package/src/hooks/useDomSelection.test.ts +26 -0
- package/src/hooks/useDomSelection.ts +10 -3
- package/src/hooks/useDomSelectionTypes.ts +6 -1
- package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
- package/src/hooks/useEditHistoryActions.test.tsx +15 -0
- package/src/hooks/useEditHistoryActions.ts +16 -5
- package/src/hooks/useElementLifecycleOps.ts +2 -1
- package/src/hooks/useEnableKeyframes.test.ts +34 -3
- package/src/hooks/useEnableKeyframes.ts +7 -4
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +121 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +35 -8
- package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
- package/src/hooks/useFileManager.ts +14 -10
- package/src/hooks/useFileTree.ts +3 -2
- package/src/hooks/useFrameCapture.ts +5 -1
- package/src/hooks/useFreezeFrame.ts +3 -2
- package/src/hooks/useGestureCommit.test.tsx +225 -55
- package/src/hooks/useGestureCommit.ts +86 -55
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +28 -35
- package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
- package/src/hooks/useGsapAwareEditing.ts +101 -195
- package/src/hooks/useGsapAwareGroupMove.ts +179 -0
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
- package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
- package/src/hooks/useGsapKeyframeOps.ts +36 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +60 -1
- package/src/hooks/useGsapScriptCommits.ts +15 -10
- package/src/hooks/useGsapSelectionHandlers.test.tsx +1 -1
- package/src/hooks/useGsapSelectionHandlers.ts +24 -18
- package/src/hooks/useGsapTweenCache.ts +18 -12
- package/src/hooks/useLintModal.ts +2 -1
- package/src/hooks/usePersistentEditHistory.test.ts +277 -1
- package/src/hooks/usePersistentEditHistory.ts +47 -8
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +76 -2
- package/src/hooks/usePreviewInteraction.test.ts +21 -0
- package/src/hooks/usePreviewInteraction.ts +2 -0
- package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
- package/src/hooks/usePreviewPersistence.ts +13 -11
- package/src/hooks/useProjectFileWriter.ts +20 -19
- package/src/hooks/useRemoveBackground.ts +2 -1
- package/src/hooks/useRenderClipContent.test.ts +18 -0
- package/src/hooks/useRenderClipContent.ts +4 -5
- package/src/hooks/useSdkSession.ts +2 -1
- package/src/hooks/useServerConnection.ts +4 -3
- package/src/hooks/useStudioSelectionPublisher.ts +2 -1
- package/src/hooks/useThumbnailStill.test.ts +100 -0
- package/src/hooks/useThumbnailStill.ts +37 -0
- package/src/hooks/useTimelineDeleteOps.test.tsx +30 -22
- package/src/hooks/useTimelineDeleteOps.ts +22 -27
- package/src/hooks/useTimelineEditing.test.tsx +1 -1
- package/src/index.ts +4 -0
- package/src/player/components/AudioWaveform.tsx +3 -2
- package/src/player/components/AutomationSelectionMenu.tsx +3 -2
- package/src/player/components/ClipContextMenu.test.tsx +73 -0
- package/src/player/components/ClipContextMenu.tsx +33 -31
- package/src/player/components/CompositionThumbnail.tsx +2 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +8 -9
- package/src/player/components/OutOfSyncBadge.tsx +5 -5
- package/src/player/components/TextClipContent.test.ts +19 -0
- package/src/player/components/TextClipContent.tsx +27 -0
- package/src/player/components/TrackGapContextMenu.tsx +3 -6
- package/src/player/components/clipMenuNormalize.ts +2 -1
- package/src/player/components/clipMenuToolItems.test.tsx +23 -0
- package/src/player/components/clipMenuToolItems.tsx +13 -9
- package/src/player/components/clipPeakMap.ts +2 -1
- package/src/player/components/timelineCallbacks.ts +1 -0
- package/src/player/components/timelineClipDragGestureLifecycle.ts +1 -1
- package/src/player/components/timelineClipGestureHandlers.test.ts +94 -0
- package/src/player/components/timelineClipGestureHandlers.ts +20 -2
- package/src/player/components/timelineEditing.ts +3 -1
- package/src/player/components/timelineLinkSelection.ts +35 -1
- package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +14 -0
- package/src/player/components/useTimelineClipDrag.ts +6 -7
- package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
- package/src/player/hooks/timelinePlayerSync.ts +6 -1
- package/src/player/hooks/useTimelinePlayer.ts +5 -1
- package/src/player/lib/thumbnailScheduler.test.ts +16 -0
- package/src/player/lib/thumbnailScheduler.ts +10 -2
- package/src/player/lib/timelineElementHelpers.test.ts +87 -1
- package/src/player/lib/timelineElementHelpers.ts +6 -15
- package/src/player/lib/timelineText.ts +40 -0
- package/src/player/sceneSwap.ts +2 -1
- package/src/player/store/timelineElement.ts +12 -0
- package/src/styles/theme.css +3 -0
- package/src/telemetry/client.ts +2 -1
- package/src/utils/blockInstaller.ts +2 -1
- package/src/utils/elementGsap.test.ts +9 -0
- package/src/utils/elementGsap.ts +7 -0
- package/src/utils/gsapSoftReload.ts +2 -3
- package/src/utils/gsapUndoRestore.browser.test.ts +51 -45
- package/src/utils/gsapUndoRestore.ts +3 -6
- package/src/utils/keyframeUsage.ts +79 -0
- package/src/utils/previewFeatureUsage.test.ts +32 -0
- package/src/utils/previewFeatureUsage.ts +46 -0
- package/src/utils/previewPrefetch.ts +2 -1
- package/src/utils/razorSplitTransaction.ts +12 -8
- package/src/utils/softReloadTargets.ts +1 -3
- package/src/utils/studioApiFetch.test.ts +75 -0
- package/src/utils/studioApiFetch.ts +13 -0
- package/src/utils/studioFileHistory.ts +4 -1
- package/src/utils/studioHelpers.ts +7 -1
- package/src/utils/studioPendingEdits.test.ts +23 -6
- package/src/utils/studioPendingEdits.ts +32 -8
- package/src/utils/studioTelemetry.test.ts +107 -0
- package/src/utils/studioTelemetry.ts +51 -5
- package/src/utils/timelineCompositionInsert.ts +3 -2
- package/src/webmcp/StudioAgentTools.tsx +4 -3
- package/dist/chunk-BIUXP3PG.js.map +0 -1
- package/dist/chunk-K47E2ZJA.js.map +0 -1
- /package/dist/{chunk-VWT7STY3.js.map → chunk-J6Q572FA.js.map} +0 -0
- /package/dist/{domEditingLayers-V67MS4WP.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
4
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
5
|
+
import { usePlayerStore } from "../player/store/playerStore";
|
|
6
|
+
import { GSAP_EDIT_BLOCK_COPY } from "./gsapEditOutcome";
|
|
7
|
+
import { liveTween, previewWith, tween } from "./gsapParsedTween.test-helpers";
|
|
8
|
+
import { tryGsapResizeIntercept } from "./gsapResizeIntercept";
|
|
9
|
+
import { tryGsapDragIntercept, tryGsapRotationIntercept } from "./gsapRuntimeBridge";
|
|
10
|
+
import { planValueAtPlayhead, planValueEdit, type PlayheadEdit } from "./gsapValueAtPlayhead";
|
|
11
|
+
|
|
12
|
+
let el: HTMLElement;
|
|
13
|
+
let selection: DomEditSelection;
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
el = document.createElement("div");
|
|
16
|
+
el.id = "box";
|
|
17
|
+
document.body.append(el);
|
|
18
|
+
selection = { id: "box", selector: "#box", element: el } as DomEditSelection;
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
el.remove();
|
|
22
|
+
usePlayerStore.setState({ currentTime: 0, activeKeyframePct: null });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** Drags `#box` by `dx` from a pre-gesture GSAP position of `base`, at `time`. */
|
|
26
|
+
async function drag(
|
|
27
|
+
anims: GsapAnimation[],
|
|
28
|
+
live: unknown[],
|
|
29
|
+
{
|
|
30
|
+
time,
|
|
31
|
+
base,
|
|
32
|
+
dx,
|
|
33
|
+
values = {},
|
|
34
|
+
}: { time: number; base: [number, number]; dx: number; values?: Record<string, number> },
|
|
35
|
+
) {
|
|
36
|
+
usePlayerStore.setState({ currentTime: time });
|
|
37
|
+
el.setAttribute("data-hf-drag-gsap-base-x", String(base[0]));
|
|
38
|
+
el.setAttribute("data-hf-drag-gsap-base-y", String(base[1]));
|
|
39
|
+
const commitMutation = vi.fn();
|
|
40
|
+
const outcome = await tryGsapDragIntercept(
|
|
41
|
+
selection,
|
|
42
|
+
{ x: dx, y: 0 },
|
|
43
|
+
anims,
|
|
44
|
+
previewWith(el, live, values),
|
|
45
|
+
commitMutation,
|
|
46
|
+
async () => anims,
|
|
47
|
+
);
|
|
48
|
+
return { outcome, mutations: commitMutation.mock.calls.map((call) => call[1]) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const fromX = tween({
|
|
52
|
+
id: "#box-from-0-position",
|
|
53
|
+
method: "from",
|
|
54
|
+
properties: { x: -60 },
|
|
55
|
+
resolvedStart: 0,
|
|
56
|
+
duration: 2,
|
|
57
|
+
ease: "none",
|
|
58
|
+
});
|
|
59
|
+
const fromXLive = (end: number) =>
|
|
60
|
+
liveTween(
|
|
61
|
+
el,
|
|
62
|
+
{ start: 0, duration: 2, vars: { x: -60, ease: "none" }, ends: { x: [-60, end] } },
|
|
63
|
+
{ from: true },
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
/** Two linear `to` tweens of `group` on `#box`, 0-2s then 2-3s, their live copies and a commit recorder. */
|
|
67
|
+
function backToBack(
|
|
68
|
+
group: "size" | "scale" | "rotation",
|
|
69
|
+
firstEnd: Record<string, number>,
|
|
70
|
+
secondEnd: Record<string, number>,
|
|
71
|
+
) {
|
|
72
|
+
const flat = (start: number, duration: number, properties: Record<string, number>) =>
|
|
73
|
+
tween({
|
|
74
|
+
id: `#box-to-${start * 1000}-${group}`,
|
|
75
|
+
propertyGroup: group,
|
|
76
|
+
method: "to",
|
|
77
|
+
properties,
|
|
78
|
+
resolvedStart: start,
|
|
79
|
+
duration,
|
|
80
|
+
ease: "none",
|
|
81
|
+
});
|
|
82
|
+
const tweens = [flat(0, 2, firstEnd), flat(2, 1, secondEnd)];
|
|
83
|
+
const live = tweens.map((a) =>
|
|
84
|
+
liveTween(el, { start: a.resolvedStart!, duration: a.duration!, vars: a.properties }),
|
|
85
|
+
);
|
|
86
|
+
const commitMutation = vi.fn();
|
|
87
|
+
const written = () => commitMutation.mock.calls.map((call) => call[1]);
|
|
88
|
+
return { tweens, live, commitMutation, written };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe("a move on a GSAP-animated layer, at the playhead", () => {
|
|
92
|
+
it("adds a keyframe inside a from() tween and keeps its end where GSAP parsed it", async () => {
|
|
93
|
+
const { outcome, mutations } = await drag([fromX], [fromXLive(40)], {
|
|
94
|
+
time: 1,
|
|
95
|
+
base: [-10, 0],
|
|
96
|
+
dx: 30,
|
|
97
|
+
});
|
|
98
|
+
expect(outcome).toEqual({ status: "persisted" });
|
|
99
|
+
expect(mutations).toEqual([
|
|
100
|
+
expect.objectContaining({
|
|
101
|
+
type: "replace-with-keyframes",
|
|
102
|
+
animationId: "#box-from-0-position",
|
|
103
|
+
position: 0,
|
|
104
|
+
duration: 2,
|
|
105
|
+
easeEach: "none",
|
|
106
|
+
keyframes: [
|
|
107
|
+
{ percentage: 0, properties: { x: -60, y: 0 } },
|
|
108
|
+
{ percentage: 50, properties: { x: 20, y: 0 } },
|
|
109
|
+
{ percentage: 100, properties: { x: 40, y: 0 } },
|
|
110
|
+
],
|
|
111
|
+
}),
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("writes the end GSAP parsed from the file, not the dragged element's live value", async () => {
|
|
116
|
+
const { mutations } = await drag([fromX], [fromXLive(40)], {
|
|
117
|
+
time: 1,
|
|
118
|
+
base: [-10, 0],
|
|
119
|
+
dx: 30,
|
|
120
|
+
values: { x: 999 },
|
|
121
|
+
});
|
|
122
|
+
expect(mutations[0].keyframes.at(-1)).toEqual({ percentage: 100, properties: { x: 40, y: 0 } });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("extends a to() tween back to the playhead, holding the start GSAP folded from CSS translate", async () => {
|
|
126
|
+
const toX = tween({
|
|
127
|
+
id: "#box-to-2000-position",
|
|
128
|
+
method: "to",
|
|
129
|
+
properties: { x: 60 },
|
|
130
|
+
resolvedStart: 2,
|
|
131
|
+
duration: 1,
|
|
132
|
+
ease: "none",
|
|
133
|
+
});
|
|
134
|
+
// `translate: 40px 30px` on the element: GSAP parses x from 40, not 0.
|
|
135
|
+
const live = liveTween(el, { start: 2, duration: 1, vars: { x: 60 }, ends: { x: [40, 60] } });
|
|
136
|
+
const { mutations } = await drag([toX], [live], { time: 1, base: [40, 0], dx: -20 });
|
|
137
|
+
expect(mutations).toEqual([
|
|
138
|
+
expect.objectContaining({
|
|
139
|
+
animationId: "#box-to-2000-position",
|
|
140
|
+
position: 1,
|
|
141
|
+
duration: 2,
|
|
142
|
+
easeEach: "none",
|
|
143
|
+
keyframes: [
|
|
144
|
+
{ percentage: 0, properties: { x: 20, y: 0 } },
|
|
145
|
+
{ percentage: 50, properties: { x: 40, y: 0 } },
|
|
146
|
+
{ percentage: 100, properties: { x: 60, y: 0 } },
|
|
147
|
+
],
|
|
148
|
+
}),
|
|
149
|
+
]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("refuses, with the Code tab message, when GSAP has not parsed the tween's end yet", async () => {
|
|
153
|
+
const unparsed = liveTween(el, { start: 0, duration: 2, vars: { x: -60 } }, { from: true });
|
|
154
|
+
const { outcome, mutations } = await drag([fromX], [unparsed], {
|
|
155
|
+
time: 1,
|
|
156
|
+
base: [-10, 0],
|
|
157
|
+
dx: 30,
|
|
158
|
+
});
|
|
159
|
+
expect(outcome).toEqual({
|
|
160
|
+
status: "blocked",
|
|
161
|
+
reason: "keyframes-uneditable",
|
|
162
|
+
detail: "implicit-end-unknown",
|
|
163
|
+
});
|
|
164
|
+
expect(mutations).toEqual([]);
|
|
165
|
+
expect(GSAP_EDIT_BLOCK_COPY["keyframes-uneditable"]).toContain("Code tab");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("adds a keyframe between two steps of a keyframes array", async () => {
|
|
169
|
+
const keys = tween({
|
|
170
|
+
id: "#box-to-0-position",
|
|
171
|
+
method: "to",
|
|
172
|
+
properties: {},
|
|
173
|
+
resolvedStart: 0,
|
|
174
|
+
duration: 3,
|
|
175
|
+
keyframes: {
|
|
176
|
+
format: "object-array",
|
|
177
|
+
keyframes: [
|
|
178
|
+
// The parse rounds the step to 66.7%; GSAP times it at 2 s of 3.
|
|
179
|
+
{ percentage: 66.7, properties: { x: 60 } },
|
|
180
|
+
{ percentage: 100, properties: { x: 120 } },
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
const steps = [
|
|
185
|
+
{ startTime: () => 0, duration: () => 2 },
|
|
186
|
+
{ startTime: () => 2, duration: () => 1 },
|
|
187
|
+
];
|
|
188
|
+
const live = liveTween(
|
|
189
|
+
el,
|
|
190
|
+
{ start: 0, duration: 3, vars: { keyframes: [] } },
|
|
191
|
+
{ parts: steps },
|
|
192
|
+
);
|
|
193
|
+
const { mutations } = await drag([keys], [live], { time: 1, base: [30, 0], dx: 10 });
|
|
194
|
+
expect(mutations.map((m) => m.type)).toEqual(["replace-with-keyframes"]);
|
|
195
|
+
// An array step eases linearly unless it says otherwise; percentage keyframes would not.
|
|
196
|
+
expect(mutations[0].keyframes).toEqual([
|
|
197
|
+
{ percentage: 33.333, properties: { x: 40, y: 0 }, ease: "none" },
|
|
198
|
+
{ percentage: 66.667, properties: { x: 60, y: 0 }, ease: "none" },
|
|
199
|
+
{ percentage: 100, properties: { x: 120, y: 0 }, ease: "none" },
|
|
200
|
+
]);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("never writes x and y into a tween that only animates size", async () => {
|
|
204
|
+
const size = tween({
|
|
205
|
+
id: "#box-to-0-size",
|
|
206
|
+
propertyGroup: "size",
|
|
207
|
+
method: "to",
|
|
208
|
+
properties: {},
|
|
209
|
+
resolvedStart: 0,
|
|
210
|
+
duration: 2,
|
|
211
|
+
keyframes: {
|
|
212
|
+
format: "percentage",
|
|
213
|
+
keyframes: [{ percentage: 100, properties: { width: 300 } }],
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
const live = liveTween(el, {
|
|
217
|
+
start: 0,
|
|
218
|
+
duration: 2,
|
|
219
|
+
vars: { keyframes: { "100%": { width: 300 } } },
|
|
220
|
+
});
|
|
221
|
+
const { mutations } = await drag([size], [live], { time: 1, base: [0, 0], dx: 10 });
|
|
222
|
+
expect(mutations.some((m) => m.animationId === "#box-to-0-size")).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("where two tweens meet at the playhead", () => {
|
|
227
|
+
it("edits the one whose keyframe the file states there, not the one listed first", async () => {
|
|
228
|
+
const fromTo = tween({
|
|
229
|
+
id: "#box-fromTo-2000-position",
|
|
230
|
+
method: "fromTo",
|
|
231
|
+
fromProperties: { x: 0 },
|
|
232
|
+
properties: { x: 60 },
|
|
233
|
+
resolvedStart: 2,
|
|
234
|
+
duration: 1,
|
|
235
|
+
ease: "none",
|
|
236
|
+
});
|
|
237
|
+
const live = liveTween(el, { start: 2, duration: 1, vars: { x: 60 }, ends: { x: [0, 60] } });
|
|
238
|
+
const { mutations } = await drag([fromX, fromTo], [fromXLive(40), live], {
|
|
239
|
+
time: 2,
|
|
240
|
+
base: [0, 0],
|
|
241
|
+
dx: 25,
|
|
242
|
+
});
|
|
243
|
+
expect(mutations).toEqual([
|
|
244
|
+
expect.objectContaining({
|
|
245
|
+
animationId: "#box-fromTo-2000-position",
|
|
246
|
+
easeEach: "none",
|
|
247
|
+
keyframes: [
|
|
248
|
+
{ percentage: 0, properties: { x: 25, y: 0 } },
|
|
249
|
+
{ percentage: 100, properties: { x: 60, y: 0 } },
|
|
250
|
+
],
|
|
251
|
+
}),
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("a resize on a layer whose size GSAP animates, at the playhead", () => {
|
|
257
|
+
it("adds a size keyframe inside the tween at the playhead", async () => {
|
|
258
|
+
const { tweens, live, commitMutation, written } = backToBack(
|
|
259
|
+
"size",
|
|
260
|
+
{ width: 300, height: 200 },
|
|
261
|
+
{ width: 360, height: 240 },
|
|
262
|
+
);
|
|
263
|
+
usePlayerStore.setState({ currentTime: 1 });
|
|
264
|
+
await tryGsapResizeIntercept(
|
|
265
|
+
selection,
|
|
266
|
+
{ width: 330, height: 210 },
|
|
267
|
+
tweens,
|
|
268
|
+
previewWith(el, live),
|
|
269
|
+
commitMutation,
|
|
270
|
+
);
|
|
271
|
+
expect(written()).toEqual([
|
|
272
|
+
expect.objectContaining({
|
|
273
|
+
type: "replace-with-keyframes",
|
|
274
|
+
animationId: "#box-to-0-size",
|
|
275
|
+
easeEach: "none",
|
|
276
|
+
keyframes: [
|
|
277
|
+
{ percentage: 50, properties: { width: 330, height: 210 } },
|
|
278
|
+
{ percentage: 100, properties: { width: 300, height: 200 } },
|
|
279
|
+
],
|
|
280
|
+
}),
|
|
281
|
+
]);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("a resize on a layer whose scale GSAP animates, at the playhead", () => {
|
|
286
|
+
it("adds a scale keyframe inside the tween at the playhead and leaves the next tween alone", async () => {
|
|
287
|
+
const { tweens, live, commitMutation, written } = backToBack(
|
|
288
|
+
"scale",
|
|
289
|
+
{ scale: 1.25 },
|
|
290
|
+
{ scale: 1.5 },
|
|
291
|
+
);
|
|
292
|
+
el.setAttribute("data-hf-studio-original-box-width", "240");
|
|
293
|
+
el.setAttribute("data-hf-studio-original-box-height", "160");
|
|
294
|
+
usePlayerStore.setState({ currentTime: 1 });
|
|
295
|
+
// Dropped at 1.5x the box with a live scale of 1.125, the playhead's value of the first tween.
|
|
296
|
+
await tryGsapResizeIntercept(
|
|
297
|
+
selection,
|
|
298
|
+
{ width: 320, height: 213.333 },
|
|
299
|
+
tweens,
|
|
300
|
+
previewWith(el, live, { scaleX: 1.125, scaleY: 1.125 }),
|
|
301
|
+
commitMutation,
|
|
302
|
+
);
|
|
303
|
+
const writes = written();
|
|
304
|
+
expect(writes[0]).toMatchObject({
|
|
305
|
+
type: "replace-with-keyframes",
|
|
306
|
+
animationId: "#box-to-0-scale",
|
|
307
|
+
easeEach: "none",
|
|
308
|
+
keyframes: [
|
|
309
|
+
{ percentage: 50, properties: { scale: 1.5 } },
|
|
310
|
+
{ percentage: 100, properties: { scale: 1.25 } },
|
|
311
|
+
],
|
|
312
|
+
});
|
|
313
|
+
expect(writes.some((m) => m.animationId === "#box-to-2000-scale")).toBe(false);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("a resize inside one gesture, on a tween that mixes size and position", () => {
|
|
318
|
+
it("writes the size and the anchor move once, into the tween the file has", async () => {
|
|
319
|
+
const keys = tween({
|
|
320
|
+
id: "#box-to-0",
|
|
321
|
+
propertyGroup: undefined,
|
|
322
|
+
method: "to",
|
|
323
|
+
properties: {},
|
|
324
|
+
resolvedStart: 0,
|
|
325
|
+
duration: 3,
|
|
326
|
+
keyframes: {
|
|
327
|
+
format: "object-array",
|
|
328
|
+
keyframes: [
|
|
329
|
+
{ percentage: 66.667, properties: { x: 60, width: 280 }, ease: "none" },
|
|
330
|
+
{ percentage: 100, properties: { x: 120, width: 320 }, ease: "none" },
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
el.setAttribute("data-hf-studio-original-box-width", "260");
|
|
335
|
+
el.setAttribute("data-hf-studio-original-box-height", "160");
|
|
336
|
+
el.setAttribute("data-hf-drag-gsap-base-x", "50");
|
|
337
|
+
el.setAttribute("data-hf-drag-gsap-base-y", "30");
|
|
338
|
+
const live = liveTween(el, { start: 0, duration: 3, vars: { keyframes: [] } });
|
|
339
|
+
usePlayerStore.setState({ currentTime: 1 });
|
|
340
|
+
// A gesture buffers its writes: the file the intercept reads back stays as it was.
|
|
341
|
+
const commitMutation = vi.fn();
|
|
342
|
+
const outcome = await tryGsapResizeIntercept(
|
|
343
|
+
selection,
|
|
344
|
+
{ width: 300, height: 200 },
|
|
345
|
+
[keys],
|
|
346
|
+
previewWith(el, [live]),
|
|
347
|
+
commitMutation,
|
|
348
|
+
async () => [keys],
|
|
349
|
+
{ x: -40, y: -40 },
|
|
350
|
+
);
|
|
351
|
+
expect(outcome).toMatchObject({ status: "persisted", ownsDragOffset: true });
|
|
352
|
+
expect(commitMutation.mock.calls.map((call) => call[1])).toEqual([
|
|
353
|
+
expect.objectContaining({
|
|
354
|
+
type: "replace-with-keyframes",
|
|
355
|
+
animationId: "#box-to-0",
|
|
356
|
+
keyframes: [
|
|
357
|
+
{
|
|
358
|
+
percentage: 33.333,
|
|
359
|
+
properties: { width: 300, height: 200, x: 10, y: -10 },
|
|
360
|
+
ease: "none",
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
percentage: 66.667,
|
|
364
|
+
properties: { x: 60, width: 280, height: 160, y: 30 },
|
|
365
|
+
ease: "none",
|
|
366
|
+
},
|
|
367
|
+
{ percentage: 100, properties: { x: 120, width: 320, height: 160, y: 30 }, ease: "none" },
|
|
368
|
+
],
|
|
369
|
+
}),
|
|
370
|
+
]);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("a rotate on a GSAP-animated layer, at the playhead", () => {
|
|
375
|
+
it("changes the keyframe two flat tweens meet at, on the tween that states it", async () => {
|
|
376
|
+
const { tweens, live, commitMutation, written } = backToBack(
|
|
377
|
+
"rotation",
|
|
378
|
+
{ rotation: 20 },
|
|
379
|
+
{ rotation: 40 },
|
|
380
|
+
);
|
|
381
|
+
usePlayerStore.setState({ currentTime: 2 });
|
|
382
|
+
await tryGsapRotationIntercept(selection, 25, tweens, previewWith(el, live), commitMutation);
|
|
383
|
+
expect(written()).toEqual([
|
|
384
|
+
expect.objectContaining({
|
|
385
|
+
type: "replace-with-keyframes",
|
|
386
|
+
animationId: "#box-to-0-rotation",
|
|
387
|
+
position: 0,
|
|
388
|
+
duration: 2,
|
|
389
|
+
easeEach: "none",
|
|
390
|
+
keyframes: [{ percentage: 100, properties: { rotation: 25 } }],
|
|
391
|
+
}),
|
|
392
|
+
]);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe("a rotate on a layer whose tween turns it on another axis", () => {
|
|
397
|
+
it("holds the new rotation across the whole tween, so the layer does not turn before the playhead", async () => {
|
|
398
|
+
const flip = tween({
|
|
399
|
+
id: "#box-to-0-rotation",
|
|
400
|
+
propertyGroup: "rotation",
|
|
401
|
+
method: "to",
|
|
402
|
+
properties: { rotationY: 180 },
|
|
403
|
+
resolvedStart: 0,
|
|
404
|
+
duration: 2,
|
|
405
|
+
ease: "none",
|
|
406
|
+
});
|
|
407
|
+
usePlayerStore.setState({ currentTime: 1 });
|
|
408
|
+
const commitMutation = vi.fn();
|
|
409
|
+
const live = [liveTween(el, { start: 0, duration: 2, vars: flip.properties })];
|
|
410
|
+
await tryGsapRotationIntercept(selection, 45, [flip], previewWith(el, live), commitMutation);
|
|
411
|
+
expect(commitMutation.mock.calls.map((call) => call[1].keyframes)).toEqual([
|
|
412
|
+
[
|
|
413
|
+
{ percentage: 0, properties: { rotation: 45 } },
|
|
414
|
+
{ percentage: 50, properties: { rotation: 45 } },
|
|
415
|
+
{ percentage: 100, properties: { rotationY: 180, rotation: 45 } },
|
|
416
|
+
],
|
|
417
|
+
]);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe("planValueAtPlayhead", () => {
|
|
422
|
+
const plan = (edit: Partial<PlayheadEdit>) =>
|
|
423
|
+
planValueAtPlayhead({
|
|
424
|
+
anim: tween({ method: "to", properties: {}, resolvedStart: 0, duration: 4 }),
|
|
425
|
+
at: { time: 2 },
|
|
426
|
+
values: {},
|
|
427
|
+
implicitEndValue: () => null,
|
|
428
|
+
...edit,
|
|
429
|
+
});
|
|
430
|
+
const kf = (
|
|
431
|
+
keyframes: Array<{ percentage: number; properties: Record<string, number> }>,
|
|
432
|
+
easeEach?: string,
|
|
433
|
+
) =>
|
|
434
|
+
tween({
|
|
435
|
+
method: "to",
|
|
436
|
+
properties: {},
|
|
437
|
+
resolvedStart: 0,
|
|
438
|
+
duration: 4,
|
|
439
|
+
keyframes: { format: "percentage", keyframes, ...(easeEach && { easeEach }) },
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("changes the keyframe under the playhead and no other", () => {
|
|
443
|
+
const result = plan({
|
|
444
|
+
anim: kf([
|
|
445
|
+
{ percentage: 0, properties: { x: 0 } },
|
|
446
|
+
{ percentage: 50, properties: { x: 10 } },
|
|
447
|
+
{ percentage: 100, properties: { x: 20 } },
|
|
448
|
+
]),
|
|
449
|
+
values: { x: 15 },
|
|
450
|
+
});
|
|
451
|
+
expect(result.ok && result.mutation.keyframes.map((k) => k.properties.x)).toEqual([0, 15, 20]);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("reports an add only when it writes a keyframe the tween did not have", () => {
|
|
455
|
+
const changed = plan({
|
|
456
|
+
anim: kf([
|
|
457
|
+
{ percentage: 0, properties: { x: 0 } },
|
|
458
|
+
{ percentage: 100, properties: { x: 20 } },
|
|
459
|
+
]),
|
|
460
|
+
at: { time: 4 },
|
|
461
|
+
values: { x: 15 },
|
|
462
|
+
});
|
|
463
|
+
const held = plan({
|
|
464
|
+
anim: kf([{ percentage: 100, properties: { x: 20 } }]),
|
|
465
|
+
at: { time: 4 },
|
|
466
|
+
values: { y: 5 },
|
|
467
|
+
backfill: { y: 0 },
|
|
468
|
+
holdFromStart: true,
|
|
469
|
+
});
|
|
470
|
+
expect(changed.ok && changed.added).toBe(false);
|
|
471
|
+
expect(held.ok && held.mutation.keyframes.length).toBe(2);
|
|
472
|
+
expect(held.ok && held.added).toBe(true);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("eases the new segment like the one it splits", () => {
|
|
476
|
+
const result = plan({
|
|
477
|
+
anim: kf([{ percentage: 100, properties: { x: 20 } }], "power2.in"),
|
|
478
|
+
at: { time: 1 },
|
|
479
|
+
values: { x: 5 },
|
|
480
|
+
});
|
|
481
|
+
expect(result.ok && result.mutation).toMatchObject({
|
|
482
|
+
easeEach: "power2.in",
|
|
483
|
+
keyframes: [
|
|
484
|
+
{ percentage: 25, properties: { x: 5 } },
|
|
485
|
+
{ percentage: 100, properties: { x: 20 } },
|
|
486
|
+
],
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("extends past the end and keeps the authored end where it was", () => {
|
|
491
|
+
const result = plan({
|
|
492
|
+
anim: tween({
|
|
493
|
+
method: "to",
|
|
494
|
+
properties: { width: 300 },
|
|
495
|
+
resolvedStart: 0,
|
|
496
|
+
duration: 2,
|
|
497
|
+
ease: "none",
|
|
498
|
+
}),
|
|
499
|
+
at: { time: 3 },
|
|
500
|
+
values: { width: 400 },
|
|
501
|
+
});
|
|
502
|
+
expect(result.ok && result.mutation).toMatchObject({
|
|
503
|
+
position: 0,
|
|
504
|
+
duration: 3,
|
|
505
|
+
easeEach: "none",
|
|
506
|
+
keyframes: [
|
|
507
|
+
{ percentage: 66.667, properties: { width: 300 } },
|
|
508
|
+
{ percentage: 100, properties: { width: 400 } },
|
|
509
|
+
],
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("splits a uniform scale into longhands for a per-axis edit", () => {
|
|
514
|
+
const result = plan({
|
|
515
|
+
anim: tween({
|
|
516
|
+
method: "to",
|
|
517
|
+
properties: { scale: 1.25 },
|
|
518
|
+
resolvedStart: 0,
|
|
519
|
+
duration: 4,
|
|
520
|
+
ease: "none",
|
|
521
|
+
}),
|
|
522
|
+
values: { scaleX: 1.5, scaleY: 1.1 },
|
|
523
|
+
});
|
|
524
|
+
expect(result.ok && result.mutation.keyframes).toEqual([
|
|
525
|
+
{ percentage: 50, properties: { scaleX: 1.5, scaleY: 1.1 } },
|
|
526
|
+
{ percentage: 100, properties: { scaleX: 1.25, scaleY: 1.25 } },
|
|
527
|
+
]);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("refuses keyframes eased as a whole, whose segment curves it cannot keep", () => {
|
|
531
|
+
const anim = { ...kf([{ percentage: 100, properties: { x: 20 } }]), ease: "power2.out" };
|
|
532
|
+
expect(plan({ anim, values: { x: 5 } })).toEqual({ ok: false, reason: "eased-keyframes" });
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("refuses a tween whose selector also animates a sibling, before it plans anything", () => {
|
|
537
|
+
el.className = "card";
|
|
538
|
+
const sibling = document.body.appendChild(document.createElement("div"));
|
|
539
|
+
sibling.className = "card";
|
|
540
|
+
const shared = tween({
|
|
541
|
+
targetSelector: ".card",
|
|
542
|
+
properties: { x: 100 },
|
|
543
|
+
resolvedStart: 0,
|
|
544
|
+
duration: 1,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
expect(planValueEdit(selection, shared, { x: 40 }, null)).toEqual({
|
|
548
|
+
ok: false,
|
|
549
|
+
reason: "shared-tween",
|
|
550
|
+
});
|
|
551
|
+
sibling.remove();
|
|
552
|
+
});
|