11import React from 'react' ;
22import { render , screen , fireEvent } from '@testing-library/react' ;
3- import { describe , it , expect } from 'vitest' ;
3+ import { describe , it , expect , beforeEach , vi } from 'vitest' ;
44import InteractiveViewer from './InteractiveViewer' ;
55
6+ // getBoundingClientRect is not implemented in jsdom — mock it so mouse-position
7+ // tests can assert normalized values without relying on a real layout engine.
8+ const mockContainerRect : DOMRect = {
9+ left : 0 ,
10+ top : 0 ,
11+ right : 600 ,
12+ bottom : 400 ,
13+ width : 600 ,
14+ height : 400 ,
15+ x : 0 ,
16+ y : 0 ,
17+ toJSON : ( ) => ( { } ) ,
18+ } ;
19+
20+ beforeEach ( ( ) => {
21+ vi . spyOn ( Element . prototype , 'getBoundingClientRect' ) . mockReturnValue ( mockContainerRect ) ;
22+ } ) ;
23+
624describe ( 'InteractiveViewer' , ( ) => {
25+ // ── Existing behaviour ────────────────────────────────────────────────────
26+
727 it ( 'renders children correctly' , ( ) => {
828 render (
929 < InteractiveViewer >
@@ -24,9 +44,9 @@ describe('InteractiveViewer', () => {
2444 // Focus and press 'w' to pan up
2545 fireEvent . keyDown ( viewerContainer , { key : 'w' } ) ;
2646
27- // Check if the transform style was updated on the inner div
28- const innerDiv = viewerContainer . firstChild as HTMLElement ;
29- expect ( innerDiv . style . transform ) . toContain ( 'translate(0px, 30px) scale(1)' ) ;
47+ // The content div is the second child (after the parallax layer)
48+ const contentDiv = viewerContainer . children [ 1 ] as HTMLElement ;
49+ expect ( contentDiv . style . transform ) . toContain ( 'translate(0px, 30px) scale(1)' ) ;
3050 } ) ;
3151
3252 it ( 'handles keyboard navigation for zooming' , ( ) => {
@@ -40,9 +60,8 @@ describe('InteractiveViewer', () => {
4060 // Focus and press '+' to zoom in
4161 fireEvent . keyDown ( viewerContainer , { key : '+' } ) ;
4262
43- // Check if the transform style was updated on the inner div
44- const innerDiv = viewerContainer . firstChild as HTMLElement ;
45- expect ( innerDiv . style . transform ) . toContain ( 'scale(1.1)' ) ;
63+ const contentDiv = viewerContainer . children [ 1 ] as HTMLElement ;
64+ expect ( contentDiv . style . transform ) . toContain ( 'scale(1.1)' ) ;
4665 } ) ;
4766
4867 it ( 'ignores key presses if an input element is focused' , ( ) => {
@@ -55,11 +74,107 @@ describe('InteractiveViewer', () => {
5574 const input = screen . getByTestId ( 'input' ) ;
5675 input . focus ( ) ;
5776
77+ // The viewer container is grandparent: input → content div → viewer
5878 const viewerContainer = input . parentElement ?. parentElement as HTMLElement ;
5979 fireEvent . keyDown ( viewerContainer , { key : 'w' } ) ;
6080
61- const innerDiv = viewerContainer . firstChild as HTMLElement ;
62- // Should not have panned
63- expect ( innerDiv . style . transform ) . toContain ( 'translate(0px, 0px) scale(1)' ) ;
81+ const contentDiv = viewerContainer . children [ 1 ] as HTMLElement ;
82+ // Should not have panned since an input had focus
83+ expect ( contentDiv . style . transform ) . toContain ( 'translate(0px, 0px) scale(1)' ) ;
84+ } ) ;
85+
86+ // ── Parallax background layer ─────────────────────────────────────────────
87+
88+ it ( 'renders the parallax background layer behind the card content' , ( ) => {
89+ render (
90+ < InteractiveViewer >
91+ < div > Content</ div >
92+ </ InteractiveViewer >
93+ ) ;
94+ // The parallax layer is always present in the DOM (opacity transitions handle visibility)
95+ expect ( screen . getByTestId ( 'parallax-bg-layer' ) ) . toBeDefined ( ) ;
96+ } ) ;
97+
98+ it ( 'renders the cursor glow element inside the parallax layer' , ( ) => {
99+ render (
100+ < InteractiveViewer >
101+ < div > Content</ div >
102+ </ InteractiveViewer >
103+ ) ;
104+ const glow = screen . getByTestId ( 'parallax-cursor-glow' ) ;
105+ expect ( glow ) . toBeDefined ( ) ;
106+ } ) ;
107+
108+ it ( 'shows the cursor glow at full opacity when the pointer enters the container' , ( ) => {
109+ const { container } = render (
110+ < InteractiveViewer >
111+ < div > Content</ div >
112+ </ InteractiveViewer >
113+ ) ;
114+ const viewerContainer = container . firstChild as HTMLElement ;
115+ const glow = screen . getByTestId ( 'parallax-cursor-glow' ) ;
116+
117+ // Before hover: glow opacity should be 0 (faded out)
118+ expect ( glow . style . opacity ) . toBe ( '0' ) ;
119+
120+ // Simulate pointer entering the container
121+ fireEvent . pointerEnter ( viewerContainer ) ;
122+
123+ // After hover: glow should become visible (opacity 1)
124+ expect ( glow . style . opacity ) . toBe ( '1' ) ;
125+ } ) ;
126+
127+ it ( 'hides the cursor glow when the pointer leaves the container' , ( ) => {
128+ const { container } = render (
129+ < InteractiveViewer >
130+ < div > Content</ div >
131+ </ InteractiveViewer >
132+ ) ;
133+ const viewerContainer = container . firstChild as HTMLElement ;
134+ const glow = screen . getByTestId ( 'parallax-cursor-glow' ) ;
135+
136+ fireEvent . pointerEnter ( viewerContainer ) ;
137+ expect ( glow . style . opacity ) . toBe ( '1' ) ;
138+
139+ fireEvent . pointerLeave ( viewerContainer ) ;
140+ // Glow should fade back out on leave
141+ expect ( glow . style . opacity ) . toBe ( '0' ) ;
142+ } ) ;
143+
144+ it ( 'updates the cursor glow position on pointer move' , ( ) => {
145+ const { container } = render (
146+ < InteractiveViewer >
147+ < div > Content</ div >
148+ </ InteractiveViewer >
149+ ) ;
150+ const viewerContainer = container . firstChild as HTMLElement ;
151+ const glow = screen . getByTestId ( 'parallax-cursor-glow' ) ;
152+
153+ // Move pointer to top-left quadrant of the mocked 600×400 container
154+ fireEvent . pointerMove ( viewerContainer , { clientX : 150 , clientY : 100 } ) ;
155+
156+ // Normalized: x = 150/600 = 0.25, y = 100/400 = 0.25
157+ // Glow `left` should be "25%" and `top` should be "25%"
158+ expect ( glow . style . left ) . toBe ( '25%' ) ;
159+ expect ( glow . style . top ) . toBe ( '25%' ) ;
160+ } ) ;
161+
162+ it ( 'resets glow position to center (50%) when the pointer leaves' , ( ) => {
163+ const { container } = render (
164+ < InteractiveViewer >
165+ < div > Content</ div >
166+ </ InteractiveViewer >
167+ ) ;
168+ const viewerContainer = container . firstChild as HTMLElement ;
169+ const glow = screen . getByTestId ( 'parallax-cursor-glow' ) ;
170+
171+ // Move pointer to an off-center position
172+ fireEvent . pointerMove ( viewerContainer , { clientX : 60 , clientY : 80 } ) ;
173+ expect ( glow . style . left ) . not . toBe ( '50%' ) ;
174+
175+ // Leave the container — position resets to center so it fades from center
176+ fireEvent . pointerLeave ( viewerContainer ) ;
177+ expect ( glow . style . left ) . toBe ( '50%' ) ;
178+ expect ( glow . style . top ) . toBe ( '50%' ) ;
64179 } ) ;
65180} ) ;
0 commit comments