@samitouri / QOS-React-1 / commits / d4e78c42a9

Add ref callback test for cleanup fn vs null call (#28895)

Used this test scenario to clarify how callback refs work when detached based on the availability of a cleanup function to update documentation in https://github.com/reactjs/react.dev/pull/6770 Checking it in for additional test coverage and test-based documentation

Jack Pope committed Apr 23, 2024 at 12:13 UTC d4e78c42a94be027b4dc7ed2659a5fddfbf9bd4e
1 file changed +97
packages/react-dom/src/__tests__/refs-test.js
+97
@@ -706,4 +706,101 @@ describe('refs return clean up function', () => {
706 expect(setup).toHaveBeenCalledTimes(1);
707 expect(cleanUp).toHaveBeenCalledTimes(1);
708 });
709 +
710 + it('handles detaching refs with either cleanup function or null argument', async () => {
711 + const container = document.createElement('div');
712 + const cleanUp = jest.fn();
713 + const setup = jest.fn();
714 + const setup2 = jest.fn();
715 + const nullHandler = jest.fn();
716 +
717 + function _onRefChangeWithCleanup(_ref) {
718 + if (_ref) {
719 + setup(_ref.id);
720 + } else {
721 + nullHandler();
722 + }
723 + return cleanUp;
724 + }
725 +
726 + function _onRefChangeWithoutCleanup(_ref) {
727 + if (_ref) {
728 + setup2(_ref.id);
729 + } else {
730 + nullHandler();
731 + }
732 + }
733 +
734 + const root = ReactDOMClient.createRoot(container);
735 + await act(() => {
736 + root.render(<div id="test-div" ref={_onRefChangeWithCleanup} />);
737 + });
738 +
739 + expect(setup).toBeCalledWith('test-div');
740 + expect(setup).toHaveBeenCalledTimes(1);
741 + expect(cleanUp).toHaveBeenCalledTimes(0);
742 +
743 + await act(() => {
744 + root.render(<div id="test-div2" ref={_onRefChangeWithoutCleanup} />);
745 + });
746 +
747 + // Existing setup call was not called again
748 + expect(setup).toHaveBeenCalledTimes(1);
749 + // No null call because cleanup is returned
750 + expect(nullHandler).toHaveBeenCalledTimes(0);
751 + // Now we have a cleanup
752 + expect(cleanUp).toHaveBeenCalledTimes(1);
753 +
754 + // New ref is setup
755 + expect(setup2).toBeCalledWith('test-div2');
756 + expect(setup2).toHaveBeenCalledTimes(1);
757 +
758 + // Now, render with the original ref again
759 + await act(() => {
760 + root.render(<div id="test-div3" ref={_onRefChangeWithCleanup} />);
761 + });
762 +
763 + // Setup was not called again
764 + expect(setup2).toBeCalledWith('test-div2');
765 + expect(setup2).toHaveBeenCalledTimes(1);
766 +
767 + // Null handler hit because no cleanup is returned
768 + expect(nullHandler).toHaveBeenCalledTimes(1);
769 +
770 + // Original setup hit one more time
771 + expect(setup).toHaveBeenCalledTimes(2);
772 + });
773 +
774 + it('calls cleanup function on unmount', async () => {
775 + const container = document.createElement('div');
776 + const cleanUp = jest.fn();
777 + const setup = jest.fn();
778 + const nullHandler = jest.fn();
779 +
780 + function _onRefChangeWithCleanup(_ref) {
781 + if (_ref) {
782 + setup(_ref.id);
783 + } else {
784 + nullHandler();
785 + }
786 + return cleanUp;
787 + }
788 +
789 + const root = ReactDOMClient.createRoot(container);
790 + await act(() => {
791 + root.render(<div id="test-div" ref={_onRefChangeWithCleanup} />);
792 + });
793 +
794 + expect(setup).toHaveBeenCalledTimes(1);
795 + expect(cleanUp).toHaveBeenCalledTimes(0);
796 + expect(nullHandler).toHaveBeenCalledTimes(0);
797 +
798 + root.unmount();
799 +
800 + expect(setup).toHaveBeenCalledTimes(1);
801 + // Now cleanup has been called
802 + expect(cleanUp).toHaveBeenCalledTimes(1);
803 + // Ref callback never called with null when cleanup is returned
804 + expect(nullHandler).toHaveBeenCalledTimes(0);
805 + });
806 });