Skip to content

Commit 1d35337

Browse files
tyler-daneclaude
andcommitted
test(web): use renderHook for useGridLayout tests
Use the canonical test pattern with renderHook and mock elements rather than rendering full components with prototype-level getBoundingClientRect overrides. This approach is consistent with useCalendarGridLayout tests and avoids CI-specific prototype override issues. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 287dc95 commit 1d35337

1 file changed

Lines changed: 54 additions & 141 deletions

File tree

Lines changed: 54 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,32 @@
11
import { 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";
45
import { Provider } from "react-redux";
56
import { createInitialState } from "@web/__tests__/utils/state/store.test.util";
67
import { reducers } from "@web/store/reducers";
78
import { setWeekInteractionMotionActive } from "@web/views/Week/interaction/state/weekInteractionMotionState";
89
import { useGridLayout } from "./useGridLayout";
910
import { 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

10032
const 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

12249
beforeEach(() => {
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

14453
afterEach(() => {
14554
setWeekInteractionMotionActive(false);
14655
});
14756

14857
describe("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

Comments
 (0)