mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +00:00
fix(studio): scope timeline ease focus lifecycle (#2710)
This commit is contained in:
@@ -5,11 +5,15 @@ import { createRoot } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
||||
import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext";
|
||||
import { usePlayerStore } from "../../player";
|
||||
import { usePlayerStore } from "../../player/store/playerStore";
|
||||
import { GsapAnimationSection } from "./GsapAnimationSection";
|
||||
|
||||
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const animationCardMock = vi.hoisted(() => ({
|
||||
consumers: [] as Array<{ tweenPercentage: number | null; consume: () => void }>,
|
||||
}));
|
||||
|
||||
vi.mock("./AnimationCard", () => ({
|
||||
AnimationCard: ({
|
||||
animation,
|
||||
@@ -19,22 +23,27 @@ vi.mock("./AnimationCard", () => ({
|
||||
animation: GsapAnimation;
|
||||
focusedSegment: { tweenPercentage: number } | null;
|
||||
onFocusSegmentConsumed: () => void;
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`animation-${animation.id}`}
|
||||
data-focused={focusedSegment ? String(focusedSegment.tweenPercentage) : ""}
|
||||
// Mirrors the real card: the consume callback only fires from the effect
|
||||
// that runs when this card actually received a focusedSegment.
|
||||
onClick={() => focusedSegment && onFocusSegmentConsumed()}
|
||||
/>
|
||||
),
|
||||
}) => {
|
||||
animationCardMock.consumers.push({
|
||||
tweenPercentage: focusedSegment?.tweenPercentage ?? null,
|
||||
consume: onFocusSegmentConsumed,
|
||||
});
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`animation-${animation.id}`}
|
||||
data-focused={focusedSegment ? String(focusedSegment.tweenPercentage) : ""}
|
||||
onClick={onFocusSegmentConsumed}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./GsapAddAnimationControl", () => ({ GsapAddAnimationControl: () => null }));
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
animationCardMock.consumers = [];
|
||||
usePlayerStore.getState().reset();
|
||||
});
|
||||
|
||||
@@ -76,8 +85,22 @@ function renderSection(elementId: string) {
|
||||
return { host, root, render };
|
||||
}
|
||||
|
||||
function focusSharedAnimation(elementId: string, tweenPercentage: number) {
|
||||
const store = usePlayerStore.getState();
|
||||
store.beginTimelineSession("project-a");
|
||||
store.setSelectedElementId(elementId);
|
||||
store.setFocusedEaseSegment({
|
||||
elementId,
|
||||
animationId: sharedAnimation.id,
|
||||
tweenPercentage,
|
||||
});
|
||||
return store;
|
||||
}
|
||||
|
||||
describe("GsapAnimationSection", () => {
|
||||
it("consumes a shared animation id only for the focused element", () => {
|
||||
usePlayerStore.getState().beginTimelineSession("project-a");
|
||||
usePlayerStore.getState().setSelectedElementId("index.html#second");
|
||||
usePlayerStore.getState().setFocusedEaseSegment({
|
||||
elementId: "index.html#second",
|
||||
animationId: sharedAnimation.id,
|
||||
@@ -99,4 +122,70 @@ describe("GsapAnimationSection", () => {
|
||||
expect(usePlayerStore.getState().focusedEaseSegment).toBeNull();
|
||||
act(() => view.root.unmount());
|
||||
});
|
||||
|
||||
it("does not let an older consumer clear a newer request", () => {
|
||||
const store = focusSharedAnimation("index.html#first", 25);
|
||||
const view = renderSection("index.html#first");
|
||||
const staleConsumer = animationCardMock.consumers.at(-1)?.consume;
|
||||
if (!staleConsumer) throw new Error("expected first focus consumer");
|
||||
|
||||
act(() => {
|
||||
store.setFocusedEaseSegment({
|
||||
elementId: "index.html#first",
|
||||
animationId: sharedAnimation.id,
|
||||
tweenPercentage: 75,
|
||||
});
|
||||
});
|
||||
const current = usePlayerStore.getState().focusedEaseSegment;
|
||||
if (!current) throw new Error("expected replacement request");
|
||||
|
||||
act(() => staleConsumer());
|
||||
expect(usePlayerStore.getState().focusedEaseSegment).toBe(current);
|
||||
|
||||
const currentConsumer = animationCardMock.consumers.at(-1)?.consume;
|
||||
if (!currentConsumer) throw new Error("expected replacement focus consumer");
|
||||
act(() => currentConsumer());
|
||||
expect(usePlayerStore.getState().focusedEaseSegment).toBeNull();
|
||||
act(() => view.root.unmount());
|
||||
});
|
||||
|
||||
it("keeps the focus consumer stable across unrelated parent renders", () => {
|
||||
focusSharedAnimation("index.html#first", 25);
|
||||
const view = renderSection("index.html#first");
|
||||
const firstConsumer = animationCardMock.consumers.at(-1)?.consume;
|
||||
if (!firstConsumer) throw new Error("expected focus consumer");
|
||||
|
||||
view.render("index.html#first");
|
||||
|
||||
expect(animationCardMock.consumers.at(-1)?.consume).toBe(firstConsumer);
|
||||
act(() => view.root.unmount());
|
||||
});
|
||||
|
||||
it("rejects a request from an earlier project session", () => {
|
||||
const store = usePlayerStore.getState();
|
||||
store.beginTimelineSession("project-a");
|
||||
store.setSelectedElementId("index.html#first");
|
||||
store.setFocusedEaseSegment({
|
||||
elementId: "index.html#first",
|
||||
animationId: sharedAnimation.id,
|
||||
tweenPercentage: 50,
|
||||
});
|
||||
const staleRequest = usePlayerStore.getState().focusedEaseSegment;
|
||||
if (!staleRequest) throw new Error("expected request");
|
||||
|
||||
const view = renderSection("index.html#first");
|
||||
|
||||
act(() => {
|
||||
store.beginTimelineSession("project-b");
|
||||
store.setSelectedElementId("index.html#first");
|
||||
usePlayerStore.setState({ focusedEaseSegment: staleRequest });
|
||||
});
|
||||
|
||||
const card = view.host.querySelector<HTMLButtonElement>(
|
||||
"[data-testid='animation-shared-animation']",
|
||||
);
|
||||
expect(card?.dataset.focused).toBe("");
|
||||
expect(usePlayerStore.getState().focusedEaseSegment).toBe(staleRequest);
|
||||
act(() => view.root.unmount());
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user