diff --git a/CLAUDE.md b/CLAUDE.md index cad5725ce..a2b4f2521 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -155,6 +155,18 @@ The repo uses Husky with lint-staged. On commit: - **Formatting**: Prettier 3.4.2 - **CSS**: Sass 1.93.2 +## Bug Fix Workflow (TDD) + +When fixing bugs, always follow Test-Driven Development: + +1. **Write the test first** - Create a failing test that reproduces the bug +2. **Confirm it fails** - Run the test to verify it captures the broken behavior +3. **Implement the fix** - Make the minimal code change to fix the issue +4. **Verify the test passes** - Run the test again to confirm the fix works +5. **Run full test suite** - Ensure no regressions with `yarn test:ci` + +This ensures every bug fix has regression coverage and documents the expected behavior. + ## Code Conventions - **Prettier handles all code formatting** - don't worry about tabs vs spaces diff --git a/src/test/timepicker_test.test.tsx b/src/test/timepicker_test.test.tsx index 2d677dd22..ab9fd96aa 100644 --- a/src/test/timepicker_test.test.tsx +++ b/src/test/timepicker_test.test.tsx @@ -80,6 +80,44 @@ describe("TimePicker", () => { expect(resizeObserverCallback).toBe(null); }); }); + + it("should not cause infinite loop when resize callback is called multiple times", async () => { + const { container } = render( + , + ); + + await waitFor(() => { + expect(mockObserve).toHaveBeenCalledTimes(1); + }); + + const resizeObserverCallback = getResizeObserverCallback(); + const mockObserveElement = mockObserve.mock.calls[0][0]; + expect(typeof resizeObserverCallback).toBe("function"); + + const timeList = container.querySelector( + ".react-datepicker__time-list", + ) as HTMLElement; + expect(timeList).not.toBeNull(); + + // Get initial height + const initialHeight = timeList.style.height; + + // Call resize callback multiple times (simulating what would happen in an infinite loop) + if (resizeObserverCallback) { + resizeObserverCallback([], mockObserveElement); + resizeObserverCallback([], mockObserveElement); + resizeObserverCallback([], mockObserveElement); + } + + // Height should remain stable (not grow infinitely) + // The fix ensures setState is only called when height actually changes + expect(timeList.style.height).toBe(initialHeight); + }); }); it("should update on input time change", () => { diff --git a/src/time.tsx b/src/time.tsx index ed0c50f76..aed28c994 100644 --- a/src/time.tsx +++ b/src/time.tsx @@ -97,9 +97,14 @@ export default class Time extends Component { private updateContainerHeight(): void { if (this.props.monthRef && this.header) { - this.setState({ - height: this.props.monthRef.clientHeight - this.header.clientHeight, - }); + const newHeight = + this.props.monthRef.clientHeight - this.header.clientHeight; + // Only update state if height actually changed to prevent infinite resize loops + if (this.state.height !== newHeight) { + this.setState({ + height: newHeight, + }); + } } }