11import { configureStore } from "@reduxjs/toolkit" ;
2- import { act , render , screen } from "@testing-library/react" ;
3- import { type FC , useRef } from "react" ;
2+ import { act } from "react" ;
3+ import { renderHook } from "@testing-library/react" ;
4+ import { type FC } from "react" ;
45import { Provider } from "react-redux" ;
56import { createInitialState } from "@web/__tests__/utils/state/store.test.util" ;
67import { reducers } from "@web/store/reducers" ;
78import { setWeekInteractionMotionActive } from "@web/views/Week/interaction/state/weekInteractionMotionState" ;
89import { useGridLayout } from "./useGridLayout" ;
910import { afterEach , beforeEach , describe , expect , it } from "bun:test" ;
1011
11- type ObserverRecord = {
12- node : Element ;
13- callback : ResizeObserverCallback ;
14- } ;
15-
16- const observers : ObserverRecord [ ] = [ ] ;
17-
18- class TestResizeObserver implements ResizeObserver {
19- private callback : ResizeObserverCallback ;
20- disconnect = ( ) => { } ;
21- observe = ( node : Element ) => {
22- observers . push ( {
23- node,
24- callback : this . callback ,
25- } ) ;
26- } ;
27- unobserve = ( ) => { } ;
28-
29- constructor ( callback : ResizeObserverCallback ) {
30- this . callback = callback ;
31- }
32- }
33-
34- // Test-specific rect dimensions
35- const TEST_RECTS : Record < string , { width : number ; height : number } > = {
36- "all-day-row" : { width : 700 , height : 48 } ,
37- "all-day-columns" : { width : 700 , height : 48 } ,
38- "main-grid" : { width : 700 , height : 910 } ,
39- } ;
40-
41- // Custom getBoundingClientRect implementation for tests
42- function testGetBoundingClientRect ( this : HTMLElement ) : DOMRect {
43- const testId = this . getAttribute ?.( "data-testid" ) ;
44- if ( testId && TEST_RECTS [ testId ] ) {
45- const { width, height } = TEST_RECTS [ testId ] ;
46- return {
47- x : 0 ,
48- y : 0 ,
49- top : 0 ,
50- left : 0 ,
51- right : width ,
52- bottom : height ,
53- width,
54- height,
55- toJSON : ( ) => ( { } ) ,
56- } as DOMRect ;
57- }
58- // Return zero rect for unknown elements
59- return {
60- x : 0 ,
61- y : 0 ,
62- top : 0 ,
63- left : 0 ,
64- right : 0 ,
65- bottom : 0 ,
66- width : 0 ,
67- height : 0 ,
68- toJSON : ( ) => ( { } ) ,
69- } as DOMRect ;
70- }
71-
72- const triggerResize = ( node : Element ) => {
73- for ( const observer of observers . filter ( ( entry ) => entry . node === node ) ) {
74- observer . callback ( [ ] , observer as unknown as ResizeObserver ) ;
75- }
76- } ;
77-
78- const GridLayoutHarness : FC = ( ) => {
79- const renderCountRef = useRef ( 0 ) ;
80- renderCountRef . current += 1 ;
81-
82- const { gridRefs, measurements } = useGridLayout ( ) ;
83-
84- return (
85- < div >
86- < div data-testid = "all-day-row" ref = { gridRefs . allDayRowRef } />
87- < div data-testid = "all-day-columns" ref = { gridRefs . allDayRef } />
88- < div data-testid = "main-grid" ref = { gridRefs . mainGridElementRef } />
89- < output data-testid = "render-count" > { renderCountRef . current } </ output >
90- < output data-testid = "hour-height" >
91- { measurements . hourHeight . toString ( ) }
92- </ output >
93- < output data-testid = "col-widths" >
94- { measurements . colWidths . join ( "," ) }
95- </ output >
96- </ div >
97- ) ;
12+ const elementWithRect = ( width : number , height = 780 ) => {
13+ const element = document . createElement ( "div" ) ;
14+ Object . defineProperty ( element , "getBoundingClientRect" , {
15+ value : ( ) =>
16+ ( {
17+ bottom : height ,
18+ height,
19+ left : 0 ,
20+ right : width ,
21+ top : 0 ,
22+ width,
23+ x : 0 ,
24+ y : 0 ,
25+ } ) as DOMRect ,
26+ writable : true ,
27+ configurable : true ,
28+ } ) ;
29+ return element ;
9830} ;
9931
10032const createStore = ( ) =>
@@ -109,80 +41,61 @@ const createStore = () =>
10941 } ) ,
11042 } ) ;
11143
112- const renderHarness = ( ) => {
44+ const wrapper : FC < { children : React . ReactNode } > = ( { children } ) => {
11345 const store = createStore ( ) ;
114-
115- return render (
116- < Provider store = { store } >
117- < GridLayoutHarness />
118- </ Provider > ,
119- ) ;
46+ return < Provider store = { store } > { children } </ Provider > ;
12047} ;
12148
12249beforeEach ( ( ) => {
123- observers . length = 0 ;
12450 setWeekInteractionMotionActive ( false ) ;
125-
126- // Override getBoundingClientRect on both prototypes since JSDOM may use
127- // window.HTMLElement internally while tests may reference globalThis.HTMLElement
128- HTMLElement . prototype . getBoundingClientRect = testGetBoundingClientRect ;
129- if (
130- typeof window !== "undefined" &&
131- window . HTMLElement ?. prototype &&
132- window . HTMLElement !== HTMLElement
133- ) {
134- window . HTMLElement . prototype . getBoundingClientRect =
135- testGetBoundingClientRect ;
136- }
137-
138- window . ResizeObserver =
139- TestResizeObserver as unknown as typeof ResizeObserver ;
140- globalThis . ResizeObserver =
141- TestResizeObserver as unknown as typeof ResizeObserver ;
14251} ) ;
14352
14453afterEach ( ( ) => {
14554 setWeekInteractionMotionActive ( false ) ;
14655} ) ;
14756
14857describe ( "useGridLayout" , ( ) => {
149- it ( "measures grid elements from callback refs and derives column widths" , async ( ) => {
150- await act ( async ( ) => {
151- renderHarness ( ) ;
152- } ) ;
58+ it ( "measures grid elements from callback refs and derives column widths" , ( ) => {
59+ const { result } = renderHook ( ( ) => useGridLayout ( ) , { wrapper } ) ;
15360
154- // Wait for React to process state updates
155- await act ( async ( ) => {
156- await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) ) ;
61+ act ( ( ) => {
62+ result . current . gridRefs . allDayRef ( elementWithRect ( 700 ) ) ;
63+ result . current . gridRefs . mainGridElementRef ( elementWithRect ( 700 ) ) ;
15764 } ) ;
15865
159- const hourHeight = screen . getByTestId ( "hour-height" ) . textContent ;
160- const colWidths = screen . getByTestId ( "col-widths" ) . textContent ;
161-
162- // Expected values: hourHeight = 910 / 13 = 70, colWidths = 700 / 7 = 100 each
163- expect ( hourHeight ) . toBe ( "70" ) ;
164- expect ( colWidths ) . toBe ( "100,100,100,100,100,100,100" ) ;
66+ // Expected values: hourHeight = 780 / 13 = 60, colWidths = 700 / 7 = 100 each
67+ expect ( result . current . measurements . hourHeight ) . toBe ( 60 ) ;
68+ expect ( result . current . measurements . colWidths ) . toEqual ( [
69+ 100 , 100 , 100 , 100 , 100 , 100 , 100 ,
70+ ] ) ;
16571 } ) ;
16672
167- it ( "does not re-render when ResizeObserver reports unchanged measurements" , async ( ) => {
168- await act ( async ( ) => {
169- renderHarness ( ) ;
170- } ) ;
73+ it ( "does not update measurements when same dimensions are reported" , ( ) => {
74+ const { result } = renderHook ( ( ) => useGridLayout ( ) , { wrapper } ) ;
17175
172- await act ( async ( ) => {
173- await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) ) ;
76+ act ( ( ) => {
77+ result . current . gridRefs . allDayRef ( elementWithRect ( 700 ) ) ;
78+ result . current . gridRefs . mainGridElementRef ( elementWithRect ( 700 ) ) ;
17479 } ) ;
17580
17681 // Verify initial measurements are set
177- expect ( screen . getByTestId ( "hour-height" ) . textContent ) . toBe ( "70" ) ;
178-
179- const before = Number ( screen . getByTestId ( "render-count" ) . textContent ) ;
180-
181- await act ( async ( ) => {
182- triggerResize ( screen . getByTestId ( "main-grid" ) ) ;
183- triggerResize ( screen . getByTestId ( "all-day-columns" ) ) ;
82+ expect ( result . current . measurements . hourHeight ) . toBe ( 60 ) ;
83+ expect ( result . current . measurements . colWidths ) . toEqual ( [
84+ 100 , 100 , 100 , 100 , 100 , 100 , 100 ,
85+ ] ) ;
86+
87+ // Store references to current measurements
88+ const prevHourHeight = result . current . measurements . hourHeight ;
89+ const prevColWidths = result . current . measurements . colWidths ;
90+
91+ // Call refs again with identical dimensions
92+ act ( ( ) => {
93+ result . current . gridRefs . allDayRef ( elementWithRect ( 700 ) ) ;
94+ result . current . gridRefs . mainGridElementRef ( elementWithRect ( 700 ) ) ;
18495 } ) ;
18596
186- expect ( Number ( screen . getByTestId ( "render-count" ) . textContent ) ) . toBe ( before ) ;
97+ // Measurements should be the same values
98+ expect ( result . current . measurements . hourHeight ) . toBe ( prevHourHeight ) ;
99+ expect ( result . current . measurements . colWidths ) . toEqual ( prevColWidths ) ;
187100 } ) ;
188101} ) ;
0 commit comments