@@ -5,11 +5,15 @@ import { createRoot } from "react-dom/client";
55import { afterEach , describe , expect , it , vi } from "vitest" ;
66import type { GsapAnimation } from "@hyperframes/core/gsap-parser" ;
77import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext" ;
8- import { usePlayerStore } from "../../player" ;
8+ import { usePlayerStore } from "../../player/store/playerStore " ;
99import { GsapAnimationSection } from "./GsapAnimationSection" ;
1010
1111( globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT : boolean } ) . IS_REACT_ACT_ENVIRONMENT = true ;
1212
13+ const animationCardMock = vi . hoisted ( ( ) => ( {
14+ consumers : [ ] as Array < { tweenPercentage : number | null ; consume : ( ) => void } > ,
15+ } ) ) ;
16+
1317vi . mock ( "./AnimationCard" , ( ) => ( {
1418 AnimationCard : ( {
1519 animation,
@@ -19,22 +23,27 @@ vi.mock("./AnimationCard", () => ({
1923 animation : GsapAnimation ;
2024 focusedSegment : { tweenPercentage : number } | null ;
2125 onFocusSegmentConsumed : ( ) => void ;
22- } ) => (
23- < button
24- type = "button"
25- data-testid = { `animation-${ animation . id } ` }
26- data-focused = { focusedSegment ? String ( focusedSegment . tweenPercentage ) : "" }
27- // Mirrors the real card: the consume callback only fires from the effect
28- // that runs when this card actually received a focusedSegment.
29- onClick = { ( ) => focusedSegment && onFocusSegmentConsumed ( ) }
30- />
31- ) ,
26+ } ) => {
27+ animationCardMock . consumers . push ( {
28+ tweenPercentage : focusedSegment ?. tweenPercentage ?? null ,
29+ consume : onFocusSegmentConsumed ,
30+ } ) ;
31+ return (
32+ < button
33+ type = "button"
34+ data-testid = { `animation-${ animation . id } ` }
35+ data-focused = { focusedSegment ? String ( focusedSegment . tweenPercentage ) : "" }
36+ onClick = { onFocusSegmentConsumed }
37+ />
38+ ) ;
39+ } ,
3240} ) ) ;
3341
3442vi . mock ( "./GsapAddAnimationControl" , ( ) => ( { GsapAddAnimationControl : ( ) => null } ) ) ;
3543
3644afterEach ( ( ) => {
3745 document . body . innerHTML = "" ;
46+ animationCardMock . consumers = [ ] ;
3847 usePlayerStore . getState ( ) . reset ( ) ;
3948} ) ;
4049
@@ -78,6 +87,8 @@ function renderSection(elementId: string) {
7887
7988describe ( "GsapAnimationSection" , ( ) => {
8089 it ( "consumes a shared animation id only for the focused element" , ( ) => {
90+ usePlayerStore . getState ( ) . beginTimelineSession ( "project-a" ) ;
91+ usePlayerStore . getState ( ) . setSelectedElementId ( "index.html#second" ) ;
8192 usePlayerStore . getState ( ) . setFocusedEaseSegment ( {
8293 elementId : "index.html#second" ,
8394 animationId : sharedAnimation . id ,
@@ -99,4 +110,65 @@ describe("GsapAnimationSection", () => {
99110 expect ( usePlayerStore . getState ( ) . focusedEaseSegment ) . toBeNull ( ) ;
100111 act ( ( ) => view . root . unmount ( ) ) ;
101112 } ) ;
113+
114+ it ( "does not let an older consumer clear a newer request" , ( ) => {
115+ const store = usePlayerStore . getState ( ) ;
116+ store . beginTimelineSession ( "project-a" ) ;
117+ store . setSelectedElementId ( "index.html#first" ) ;
118+ store . setFocusedEaseSegment ( {
119+ elementId : "index.html#first" ,
120+ animationId : sharedAnimation . id ,
121+ tweenPercentage : 25 ,
122+ } ) ;
123+ const view = renderSection ( "index.html#first" ) ;
124+ const staleConsumer = animationCardMock . consumers . at ( - 1 ) ?. consume ;
125+ if ( ! staleConsumer ) throw new Error ( "expected first focus consumer" ) ;
126+
127+ act ( ( ) => {
128+ store . setFocusedEaseSegment ( {
129+ elementId : "index.html#first" ,
130+ animationId : sharedAnimation . id ,
131+ tweenPercentage : 75 ,
132+ } ) ;
133+ } ) ;
134+ const current = usePlayerStore . getState ( ) . focusedEaseSegment ;
135+ if ( ! current ) throw new Error ( "expected replacement request" ) ;
136+
137+ act ( ( ) => staleConsumer ( ) ) ;
138+ expect ( usePlayerStore . getState ( ) . focusedEaseSegment ) . toBe ( current ) ;
139+
140+ const currentConsumer = animationCardMock . consumers . at ( - 1 ) ?. consume ;
141+ if ( ! currentConsumer ) throw new Error ( "expected replacement focus consumer" ) ;
142+ act ( ( ) => currentConsumer ( ) ) ;
143+ expect ( usePlayerStore . getState ( ) . focusedEaseSegment ) . toBeNull ( ) ;
144+ act ( ( ) => view . root . unmount ( ) ) ;
145+ } ) ;
146+
147+ it ( "rejects a request from an earlier project session" , ( ) => {
148+ const store = usePlayerStore . getState ( ) ;
149+ store . beginTimelineSession ( "project-a" ) ;
150+ store . setSelectedElementId ( "index.html#first" ) ;
151+ store . setFocusedEaseSegment ( {
152+ elementId : "index.html#first" ,
153+ animationId : sharedAnimation . id ,
154+ tweenPercentage : 50 ,
155+ } ) ;
156+ const staleRequest = usePlayerStore . getState ( ) . focusedEaseSegment ;
157+ if ( ! staleRequest ) throw new Error ( "expected request" ) ;
158+
159+ const view = renderSection ( "index.html#first" ) ;
160+
161+ act ( ( ) => {
162+ store . beginTimelineSession ( "project-b" ) ;
163+ store . setSelectedElementId ( "index.html#first" ) ;
164+ usePlayerStore . setState ( { focusedEaseSegment : staleRequest } ) ;
165+ } ) ;
166+
167+ const card = view . host . querySelector < HTMLButtonElement > (
168+ "[data-testid='animation-shared-animation']" ,
169+ ) ;
170+ expect ( card ?. dataset . focused ) . toBe ( "" ) ;
171+ expect ( usePlayerStore . getState ( ) . focusedEaseSegment ) . toBe ( staleRequest ) ;
172+ act ( ( ) => view . root . unmount ( ) ) ;
173+ } ) ;
102174} ) ;
0 commit comments