From 4eb92280e7bb1f31b7df498a5bae3705ad55d1c0 Mon Sep 17 00:00:00 2001 From: blaipr Date: Tue, 16 Jun 2026 21:13:15 +0200 Subject: [PATCH 1/2] enzyme -> RTL: convert the hooks test suites Migrate src/hooks tests (useSelected, useExpanded, useModal, useToast, useRequest, useWsTemplates, useDebounce) off enzyme onto React Testing Library. Each hook is exercised via a small harness that exposes its result through a const ref/DOM node (RTL 12 has no renderHook); mutators run inside act(). Behaviour and assertions are preserved. --- awx/ui/src/hooks/useDebounce.test.js | 14 +-- awx/ui/src/hooks/useExpanded.test.js | 98 +++++++++------------ awx/ui/src/hooks/useModal.test.js | 62 ++++++------- awx/ui/src/hooks/useRequest.test.js | 110 +++++++++++------------- awx/ui/src/hooks/useSelected.test.js | 108 +++++++++-------------- awx/ui/src/hooks/useToast.test.js | 72 +++++++--------- awx/ui/src/hooks/useWsTemplates.test.js | 72 ++++++++-------- 7 files changed, 232 insertions(+), 304 deletions(-) diff --git a/awx/ui/src/hooks/useDebounce.test.js b/awx/ui/src/hooks/useDebounce.test.js index ea7ec119b..0090fe3f4 100644 --- a/awx/ui/src/hooks/useDebounce.test.js +++ b/awx/ui/src/hooks/useDebounce.test.js @@ -1,22 +1,22 @@ import React from 'react'; -import { mount } from 'enzyme'; +import { render, act } from '@testing-library/react'; import useDebounce from './useDebounce'; -function TestInner() { - return
; -} function Test({ fn, delay = 500, data }) { const debounce = useDebounce(fn, delay); debounce(data); - return ; + return
; } test('useDebounce', () => { jest.useFakeTimers(); const fn = jest.fn(); - mount(); + render(); expect(fn).toHaveBeenCalledTimes(0); - jest.advanceTimersByTime(510); + act(() => { + jest.advanceTimersByTime(510); + }); expect(fn).toHaveBeenCalledTimes(1); expect(fn).toHaveBeenCalledWith({ data: 123 }); + jest.useRealTimers(); }); diff --git a/awx/ui/src/hooks/useExpanded.test.js b/awx/ui/src/hooks/useExpanded.test.js index 597680de2..e49bb71cc 100644 --- a/awx/ui/src/hooks/useExpanded.test.js +++ b/awx/ui/src/hooks/useExpanded.test.js @@ -1,96 +1,82 @@ import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { mount } from 'enzyme'; +import { render, act } from '@testing-library/react'; import useExpanded from './useExpanded'; const array = [{ id: '1' }, { id: '2' }, { id: '3' }]; -const TestHook = ({ callback }) => { - callback(); +const result = { current: null }; +const latest = () => result.current; + +const TestHook = ({ list }) => { + result.current = useExpanded(list); return null; }; -const testHook = (callback) => { - mount(); +const testHook = (list) => { + render(); }; -describe('useSelected hook', () => { - let expanded; - let isAllExpanded; - let handleExpand; - let setExpanded; - let expandAll; - +describe('useExpanded hook', () => { test('should return expected initial values', () => { - testHook(() => { - ({ expanded, isAllExpanded, handleExpand, setExpanded, expandAll } = - useExpanded()); - }); - expect(expanded).toEqual([]); - expect(isAllExpanded).toEqual(false); - expect(handleExpand).toBeInstanceOf(Function); - expect(setExpanded).toBeInstanceOf(Function); + testHook(); + expect(latest().expanded).toEqual([]); + expect(latest().isAllExpanded).toEqual(false); + expect(latest().handleExpand).toBeInstanceOf(Function); + expect(latest().setExpanded).toBeInstanceOf(Function); }); - test('handleSelect should update and filter selected items', () => { - testHook(() => { - ({ expanded, isAllExpanded, handleExpand, setExpanded, expandAll } = - useExpanded()); - }); + test('handleExpand should update and filter expanded items', () => { + testHook(); act(() => { - handleExpand(array[0]); + latest().handleExpand(array[0]); }); - expect(expanded).toEqual([array[0]]); + expect(latest().expanded).toEqual([array[0]]); act(() => { - handleExpand(array[0]); + latest().handleExpand(array[0]); }); - expect(expanded).toEqual([]); + expect(latest().expanded).toEqual([]); }); - test('should return expected isAllSelected value', () => { - testHook(() => { - ({ expanded, isAllExpanded, handleExpand, setExpanded, expandAll } = - useExpanded(array)); - }); + test('should return expected isAllExpanded value', () => { + testHook(array); act(() => { - handleExpand(array[0]); + latest().handleExpand(array[0]); }); - expect(expanded).toEqual([array[0]]); - expect(isAllExpanded).toEqual(false); + expect(latest().expanded).toEqual([array[0]]); + expect(latest().isAllExpanded).toEqual(false); act(() => { - handleExpand(array[1]); - handleExpand(array[2]); + latest().handleExpand(array[1]); + }); + act(() => { + latest().handleExpand(array[2]); }); - expect(expanded).toEqual(array); - expect(isAllExpanded).toEqual(true); + expect(latest().expanded).toEqual(array); + expect(latest().isAllExpanded).toEqual(true); act(() => { - setExpanded([]); + latest().setExpanded([]); }); - expect(expanded).toEqual([]); - expect(isAllExpanded).toEqual(false); + expect(latest().expanded).toEqual([]); + expect(latest().isAllExpanded).toEqual(false); }); - test('should return selectAll', () => { - testHook(() => { - ({ expanded, isAllExpanded, handleExpand, setExpanded, expandAll } = - useExpanded(array)); - }); + test('should return expandAll', () => { + testHook(array); act(() => { - expandAll(true); + latest().expandAll(true); }); - expect(isAllExpanded).toEqual(true); - expect(expanded).toEqual(array); + expect(latest().isAllExpanded).toEqual(true); + expect(latest().expanded).toEqual(array); act(() => { - expandAll(false); + latest().expandAll(false); }); - expect(isAllExpanded).toEqual(false); - expect(expanded).toEqual([]); + expect(latest().isAllExpanded).toEqual(false); + expect(latest().expanded).toEqual([]); }); }); diff --git a/awx/ui/src/hooks/useModal.test.js b/awx/ui/src/hooks/useModal.test.js index bbae1c343..65078a8cb 100644 --- a/awx/ui/src/hooks/useModal.test.js +++ b/awx/ui/src/hooks/useModal.test.js @@ -1,63 +1,53 @@ import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { mount } from 'enzyme'; +import { render, act } from '@testing-library/react'; import useModal from './useModal'; -const TestHook = ({ callback }) => { - callback(); +const result = { current: null }; +const latest = () => result.current; + +const TestHook = ({ initialValue }) => { + result.current = useModal(initialValue); return null; }; -const testHook = (callback) => { - mount(); +const testHook = (initialValue) => { + render(); }; describe('useModal hook', () => { - let closeModal; - let isModalOpen; - let toggleModal; - test('isModalOpen should return expected default value', () => { - testHook(() => { - ({ isModalOpen, toggleModal, closeModal } = useModal()); - }); - expect(isModalOpen).toEqual(false); - expect(toggleModal).toBeInstanceOf(Function); - expect(closeModal).toBeInstanceOf(Function); + testHook(); + expect(latest().isModalOpen).toEqual(false); + expect(latest().toggleModal).toBeInstanceOf(Function); + expect(latest().closeModal).toBeInstanceOf(Function); }); test('isModalOpen should return expected initialized value', () => { - testHook(() => { - ({ isModalOpen, toggleModal, closeModal } = useModal(true)); - }); - expect(isModalOpen).toEqual(true); - expect(toggleModal).toBeInstanceOf(Function); - expect(closeModal).toBeInstanceOf(Function); + testHook(true); + expect(latest().isModalOpen).toEqual(true); + expect(latest().toggleModal).toBeInstanceOf(Function); + expect(latest().closeModal).toBeInstanceOf(Function); }); test('should return expected isModalOpen value after modal toggle', () => { - testHook(() => { - ({ isModalOpen, toggleModal, closeModal } = useModal()); - }); - expect(isModalOpen).toEqual(false); + testHook(); + expect(latest().isModalOpen).toEqual(false); act(() => { - toggleModal(); + latest().toggleModal(); }); - expect(isModalOpen).toEqual(true); + expect(latest().isModalOpen).toEqual(true); }); test('isModalOpen should be false after closeModal is called', () => { - testHook(() => { - ({ isModalOpen, toggleModal, closeModal } = useModal()); - }); - expect(isModalOpen).toEqual(false); + testHook(); + expect(latest().isModalOpen).toEqual(false); act(() => { - toggleModal(); + latest().toggleModal(); }); - expect(isModalOpen).toEqual(true); + expect(latest().isModalOpen).toEqual(true); act(() => { - closeModal(); + latest().closeModal(); }); - expect(isModalOpen).toEqual(false); + expect(latest().isModalOpen).toEqual(false); }); }); diff --git a/awx/ui/src/hooks/useRequest.test.js b/awx/ui/src/hooks/useRequest.test.js index 5e506faf1..bbe3838f7 100644 --- a/awx/ui/src/hooks/useRequest.test.js +++ b/awx/ui/src/hooks/useRequest.test.js @@ -1,27 +1,26 @@ import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { mount } from 'enzyme'; -import { mountWithContexts } from '../../testUtils/enzymeHelpers'; +import { render, act, waitFor } from '@testing-library/react'; +import { renderWithContexts } from '../../testUtils/rtlContexts'; import useRequest, { useDeleteItems } from './useRequest'; -function TestInner() { - return
; -} +const result = { current: null }; +const latest = () => result.current; + function Test({ makeRequest, initialValue = {} }) { - const request = useRequest(makeRequest, initialValue); - return ; + result.current = useRequest(makeRequest, initialValue); + return null; } function DeleteTest({ makeRequest, args = {} }) { - const request = useDeleteItems(makeRequest, args); - return ; + result.current = useDeleteItems(makeRequest, args); + return null; } describe('useRequest hooks', () => { describe('useRequest', () => { - test('should return initial value as result', async () => { + test('should return initial value as result', () => { const makeRequest = jest.fn(); makeRequest.mockResolvedValue({ data: 'foo' }); - const wrapper = mount( + render( { /> ); - expect(wrapper.find('TestInner').prop('result')).toEqual({ + expect(latest().result).toEqual({ initial: true, }); }); @@ -38,47 +37,43 @@ describe('useRequest hooks', () => { test('should return result', async () => { const makeRequest = jest.fn(); makeRequest.mockResolvedValue({ data: 'foo' }); - const wrapper = mount(); + render(); await act(async () => { - wrapper.find('TestInner').invoke('request')(); + latest().request(); }); - wrapper.update(); - expect(wrapper.find('TestInner').prop('result')).toEqual({ data: 'foo' }); + expect(latest().result).toEqual({ data: 'foo' }); }); - test('should is isLoading flag', async () => { + test('should set isLoading flag', async () => { const makeRequest = jest.fn(); let resolve; const promise = new Promise((r) => { resolve = r; }); makeRequest.mockReturnValue(promise); - const wrapper = mount(); + render(); await act(async () => { - wrapper.find('TestInner').invoke('request')(); + latest().request(); }); - wrapper.update(); - expect(wrapper.find('TestInner').prop('isLoading')).toEqual(true); + expect(latest().isLoading).toEqual(true); await act(async () => { resolve({ data: 'foo' }); }); - wrapper.update(); - expect(wrapper.find('TestInner').prop('isLoading')).toEqual(false); - expect(wrapper.find('TestInner').prop('result')).toEqual({ data: 'foo' }); + expect(latest().isLoading).toEqual(false); + expect(latest().result).toEqual({ data: 'foo' }); }); test('should invoke request function', async () => { const makeRequest = jest.fn(); makeRequest.mockResolvedValue({ data: 'foo' }); - const wrapper = mount(); + render(); expect(makeRequest).not.toHaveBeenCalled(); await act(async () => { - wrapper.find('TestInner').invoke('request')(); + latest().request(); }); - wrapper.update(); expect(makeRequest).toHaveBeenCalledTimes(1); }); @@ -87,13 +82,12 @@ describe('useRequest hooks', () => { const makeRequest = () => { throw error; }; - const wrapper = mount(); + render(); await act(async () => { - wrapper.find('TestInner').invoke('request')(); + latest().request(); }); - wrapper.update(); - expect(wrapper.find('TestInner').prop('error')).toEqual(error); + expect(latest().error).toEqual(error); }); test('should reset error/result on each request', async () => { @@ -105,26 +99,23 @@ describe('useRequest hooks', () => { return { data: 'foo' }; }; - const wrapper = mount(); + render(); await act(async () => { - wrapper.find('TestInner').invoke('request')(true); + latest().request(true); }); - wrapper.update(); - expect(wrapper.find('TestInner').prop('result')).toEqual({}); - expect(wrapper.find('TestInner').prop('error')).toEqual(error); + expect(latest().result).toEqual({}); + expect(latest().error).toEqual(error); await act(async () => { - wrapper.find('TestInner').invoke('request')(); + latest().request(); }); - wrapper.update(); - expect(wrapper.find('TestInner').prop('result')).toEqual({ data: 'foo' }); - expect(wrapper.find('TestInner').prop('error')).toEqual(null); + expect(latest().result).toEqual({ data: 'foo' }); + expect(latest().error).toEqual(null); await act(async () => { - wrapper.find('TestInner').invoke('request')(true); + latest().request(true); }); - wrapper.update(); - expect(wrapper.find('TestInner').prop('result')).toEqual({}); - expect(wrapper.find('TestInner').prop('error')).toEqual(error); + expect(latest().result).toEqual({}); + expect(latest().error).toEqual(error); }); test('should not update state after unmount', async () => { @@ -134,13 +125,13 @@ describe('useRequest hooks', () => { resolve = r; }); makeRequest.mockReturnValue(promise); - const wrapper = mount(); + const { unmount } = render(); expect(makeRequest).not.toHaveBeenCalled(); await act(async () => { - wrapper.find('TestInner').invoke('request')(); + latest().request(); }); - wrapper.unmount(); + unmount(); await act(async () => { resolve({ data: 'foo' }); }); @@ -151,7 +142,7 @@ describe('useRequest hooks', () => { test('should invoke delete function', async () => { const makeRequest = jest.fn(); makeRequest.mockResolvedValue({ data: 'foo' }); - const wrapper = mountWithContexts( + renderWithContexts( { expect(makeRequest).not.toHaveBeenCalled(); await act(async () => { - wrapper.find('TestInner').invoke('deleteItems')(); + await latest().deleteItems(); }); - wrapper.update(); expect(makeRequest).toHaveBeenCalledTimes(1); }); @@ -174,7 +164,7 @@ describe('useRequest hooks', () => { const makeRequest = () => { throw error; }; - const wrapper = mountWithContexts( + renderWithContexts( { ); await act(async () => { - wrapper.find('TestInner').invoke('deleteItems')(); + await latest().deleteItems(); }); - wrapper.update(); - expect(wrapper.find('TestInner').prop('deletionError')).toEqual(error); + await waitFor(() => expect(latest().deletionError).toEqual(error)); }); test('should dismiss error', async () => { @@ -196,7 +185,7 @@ describe('useRequest hooks', () => { const makeRequest = () => { throw error; }; - const wrapper = mountWithContexts( + renderWithContexts( { ); await act(async () => { - wrapper.find('TestInner').invoke('deleteItems')(); + await latest().deleteItems(); }); - wrapper.update(); + await waitFor(() => expect(latest().deletionError).toEqual(error)); await act(async () => { - wrapper.find('TestInner').invoke('clearDeletionError')(); + latest().clearDeletionError(); }); - wrapper.update(); - expect(wrapper.find('TestInner').prop('deletionError')).toEqual(null); + expect(latest().deletionError).toEqual(null); }); }); }); diff --git a/awx/ui/src/hooks/useSelected.test.js b/awx/ui/src/hooks/useSelected.test.js index 2462a44aa..c1cd2b368 100644 --- a/awx/ui/src/hooks/useSelected.test.js +++ b/awx/ui/src/hooks/useSelected.test.js @@ -1,118 +1,96 @@ import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { mount } from 'enzyme'; +import { render, act } from '@testing-library/react'; import useSelected from './useSelected'; const array = [{ id: '1' }, { id: '2' }, { id: '3' }]; -const TestHook = ({ callback }) => { - callback(); +const result = { current: null }; +const latest = () => result.current; + +const TestHook = ({ list }) => { + result.current = useSelected(list); return null; }; -const testHook = (callback) => { - mount(); +const testHook = (list) => { + render(); }; describe('useSelected hook', () => { - let selected; - let isAllSelected; - let handleSelect; - let setSelected; - let selectAll; - let clearSelected; - test('should return expected initial values', () => { - testHook(() => { - ({ selected, isAllSelected, handleSelect, setSelected } = useSelected()); - }); - expect(selected).toEqual([]); - expect(isAllSelected).toEqual(false); - expect(handleSelect).toBeInstanceOf(Function); - expect(setSelected).toBeInstanceOf(Function); + testHook(); + expect(latest().selected).toEqual([]); + expect(latest().isAllSelected).toEqual(false); + expect(latest().handleSelect).toBeInstanceOf(Function); + expect(latest().setSelected).toBeInstanceOf(Function); }); test('handleSelect should update and filter selected items', () => { - testHook(() => { - ({ selected, isAllSelected, handleSelect, setSelected } = useSelected()); - }); + testHook(); act(() => { - handleSelect(array[0]); + latest().handleSelect(array[0]); }); - expect(selected).toEqual([array[0]]); + expect(latest().selected).toEqual([array[0]]); act(() => { - handleSelect(array[0]); + latest().handleSelect(array[0]); }); - expect(selected).toEqual([]); + expect(latest().selected).toEqual([]); }); test('should return expected isAllSelected value', () => { - testHook(() => { - ({ selected, isAllSelected, handleSelect, setSelected } = - useSelected(array)); - }); + testHook(array); act(() => { - handleSelect(array[0]); + latest().handleSelect(array[0]); }); - expect(selected).toEqual([array[0]]); - expect(isAllSelected).toEqual(false); + expect(latest().selected).toEqual([array[0]]); + expect(latest().isAllSelected).toEqual(false); act(() => { - handleSelect(array[1]); - handleSelect(array[2]); + latest().handleSelect(array[1]); }); - expect(selected).toEqual(array); - expect(isAllSelected).toEqual(true); + act(() => { + latest().handleSelect(array[2]); + }); + expect(latest().selected).toEqual(array); + expect(latest().isAllSelected).toEqual(true); act(() => { - setSelected([]); + latest().setSelected([]); }); - expect(selected).toEqual([]); - expect(isAllSelected).toEqual(false); + expect(latest().selected).toEqual([]); + expect(latest().isAllSelected).toEqual(false); }); test('should return selectAll', () => { - testHook(() => { - ({ selected, isAllSelected, handleSelect, setSelected, selectAll } = - useSelected(array)); - }); + testHook(array); act(() => { - selectAll(true); + latest().selectAll(true); }); - expect(isAllSelected).toEqual(true); - expect(selected).toEqual(array); + expect(latest().isAllSelected).toEqual(true); + expect(latest().selected).toEqual(array); act(() => { - selectAll(false); + latest().selectAll(false); }); - expect(isAllSelected).toEqual(false); - expect(selected).toEqual([]); + expect(latest().isAllSelected).toEqual(false); + expect(latest().selected).toEqual([]); }); test('should return clearSelected', () => { - testHook(() => { - ({ - selected, - isAllSelected, - handleSelect, - setSelected, - selectAll, - clearSelected, - } = useSelected(array)); - }); + testHook(array); act(() => { - selectAll(true); + latest().selectAll(true); }); act(() => { - clearSelected(); + latest().clearSelected(); }); - expect(isAllSelected).toEqual(false); - expect(selected).toEqual([]); + expect(latest().isAllSelected).toEqual(false); + expect(latest().selected).toEqual([]); }); }); diff --git a/awx/ui/src/hooks/useToast.test.js b/awx/ui/src/hooks/useToast.test.js index 23b6ca845..3db04a844 100644 --- a/awx/ui/src/hooks/useToast.test.js +++ b/awx/ui/src/hooks/useToast.test.js @@ -1,35 +1,33 @@ import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { shallow, mount } from 'enzyme'; +import { render, screen, act } from '@testing-library/react'; import useToast, { Toast, AlertVariant } from './useToast'; describe('useToast', () => { - const Child = () =>
; + const result = { current: null }; + const latest = () => result.current; const Test = () => { - const toastVals = useToast(); - return ; + result.current = useToast(); + return null; }; test('should provide Toast component', () => { - const wrapper = mount(); - - expect(wrapper.find('Child').prop('Toast')).toEqual(Toast); + render(); + expect(latest().Toast).toEqual(Toast); }); test('should add toast', () => { - const wrapper = mount(); + render(); - expect(wrapper.find('Child').prop('toastProps').toasts).toEqual([]); + expect(latest().toastProps.toasts).toEqual([]); act(() => { - wrapper.find('Child').prop('addToast')({ + latest().addToast({ message: 'one', id: 1, variant: 'success', }); }); - wrapper.update(); - expect(wrapper.find('Child').prop('toastProps').toasts).toEqual([ + expect(latest().toastProps.toasts).toEqual([ { message: 'one', id: 1, @@ -39,30 +37,28 @@ describe('useToast', () => { }); test('should remove toast', () => { - const wrapper = mount(); + render(); act(() => { - wrapper.find('Child').prop('addToast')({ + latest().addToast({ message: 'one', id: 1, variant: 'success', }); }); - wrapper.update(); - expect(wrapper.find('Child').prop('toastProps').toasts).toHaveLength(1); + expect(latest().toastProps.toasts).toHaveLength(1); act(() => { - wrapper.find('Child').prop('removeToast')(1); + latest().removeToast(1); }); - wrapper.update(); - expect(wrapper.find('Child').prop('toastProps').toasts).toHaveLength(0); + expect(latest().toastProps.toasts).toHaveLength(0); }); }); describe('Toast', () => { test('should render nothing with no toasts', () => { - const wrapper = shallow( {}} />); - expect(wrapper).toEqual({}); + const { container } = render( {}} />); + expect(container).toBeEmptyDOMElement(); }); test('should render toast alert', () => { @@ -72,13 +68,12 @@ describe('Toast', () => { id: 1, message: 'the message', }; - const wrapper = shallow( {}} />); + render( {}} />); - const alert = wrapper.find('Alert'); - expect(alert.prop('title')).toEqual('Inventory saved'); - expect(alert.prop('variant')).toEqual('success'); - expect(alert.prop('ouiaId')).toEqual('toast-message-1'); - expect(alert.prop('children')).toEqual('the message'); + const alert = screen.getByText('Inventory saved').closest('.pf-c-alert'); + expect(alert).toHaveClass('pf-m-success'); + expect(alert).toHaveAttribute('data-ouia-component-id', 'toast-message-1'); + expect(alert).toHaveTextContent('the message'); }); test('should call removeToast', () => { @@ -88,12 +83,12 @@ describe('Toast', () => { variant: AlertVariant.success, id: 1, }; - const wrapper = shallow( - - ); + render(); - const alert = wrapper.find('Alert'); - alert.prop('actionClose').props.onClose(1); + const closeButton = screen.getByRole('button', { name: /close/i }); + act(() => { + closeButton.click(); + }); expect(removeToast).toHaveBeenCalledTimes(1); }); @@ -111,14 +106,9 @@ describe('Toast', () => { id: 2, }, ]; - const wrapper = shallow( {}} />); - - const alert = wrapper.find('Alert'); - expect(alert).toHaveLength(2); + render( {}} />); - expect(alert.at(0).prop('title')).toEqual('Inventory saved'); - expect(alert.at(0).prop('variant')).toEqual('success'); - expect(alert.at(1).prop('title')).toEqual('error saving'); - expect(alert.at(1).prop('variant')).toEqual('danger'); + expect(screen.getByText('Inventory saved')).toBeInTheDocument(); + expect(screen.getByText('error saving')).toBeInTheDocument(); }); }); diff --git a/awx/ui/src/hooks/useWsTemplates.test.js b/awx/ui/src/hooks/useWsTemplates.test.js index 774e7e959..d6be3c6f8 100644 --- a/awx/ui/src/hooks/useWsTemplates.test.js +++ b/awx/ui/src/hooks/useWsTemplates.test.js @@ -1,7 +1,7 @@ import React from 'react'; -import { act } from 'react-dom/test-utils'; +import { act, screen, waitFor } from '@testing-library/react'; import WS from 'jest-websocket-mock'; -import { mountWithContexts } from '../../testUtils/enzymeHelpers'; +import { renderWithContexts } from '../../testUtils/rtlContexts'; import useWsTemplates from './useWsTemplates'; /* @@ -13,19 +13,21 @@ jest.mock('./useThrottle', () => ({ default: jest.fn((val) => val), })); -function TestInner() { - return
; -} function Test({ templates }) { const syncedTemplates = useWsTemplates(templates); - return ; + return ( +
{JSON.stringify(syncedTemplates)}
+ ); +} + +function getTemplates() { + return JSON.parse(screen.getByTestId('templates').textContent); } describe('useWsTemplates hook', () => { let debug; - let wrapper; beforeEach(() => { - debug = global.console.debug; // eslint-disable-line prefer-destructuring + ({ debug } = global.console); global.console.debug = () => {}; }); @@ -33,14 +35,16 @@ describe('useWsTemplates hook', () => { global.console.debug = debug; WS.clean(); // Add small delay to ensure websocket cleanup completes - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise((resolve) => { + setTimeout(resolve, 50); + }); }); test('should return templates list', () => { const templates = [{ id: 1 }]; - wrapper = mountWithContexts(); + renderWithContexts(); - expect(wrapper.find('TestInner').prop('templates')).toEqual(templates); + expect(getTemplates()).toEqual(templates); WS.clean(); }); @@ -50,7 +54,7 @@ describe('useWsTemplates hook', () => { const templates = [{ id: 1 }]; await act(async () => { - wrapper = await mountWithContexts(); + renderWithContexts(); }); await mockServer.connected; @@ -68,7 +72,6 @@ describe('useWsTemplates hook', () => { test('should update recent job status', async () => { global.document.cookie = 'csrftoken=abc123'; const mockServer = new WS('ws://localhost/websocket/'); - let testWrapper; const templates = [ { @@ -90,7 +93,7 @@ describe('useWsTemplates hook', () => { }, ]; await act(async () => { - testWrapper = await mountWithContexts(); + renderWithContexts(); }); await mockServer.connected; @@ -103,10 +106,9 @@ describe('useWsTemplates hook', () => { }, }) ); - expect( - testWrapper.find('TestInner').prop('templates')[0].summary_fields - .recent_jobs[0].status - ).toEqual('running'); + expect(getTemplates()[0].summary_fields.recent_jobs[0].status).toEqual( + 'running' + ); act(() => { mockServer.send( JSON.stringify({ @@ -117,18 +119,17 @@ describe('useWsTemplates hook', () => { }) ); }); - testWrapper.update(); - expect( - testWrapper.find('TestInner').prop('templates')[0].summary_fields - .recent_jobs[0].status - ).toEqual('successful'); + await waitFor(() => + expect(getTemplates()[0].summary_fields.recent_jobs[0].status).toEqual( + 'successful' + ) + ); }); test('should add new job status', async () => { global.document.cookie = 'csrftoken=abc123'; const mockServer = new WS('ws://localhost/websocket/'); - let testWrapper; const templates = [ { @@ -150,7 +151,7 @@ describe('useWsTemplates hook', () => { }, ]; await act(async () => { - testWrapper = await mountWithContexts(); + renderWithContexts(); }); await mockServer.connected; @@ -163,10 +164,9 @@ describe('useWsTemplates hook', () => { }, }) ); - expect( - testWrapper.find('TestInner').prop('templates')[0].summary_fields - .recent_jobs[0].status - ).toEqual('running'); + expect(getTemplates()[0].summary_fields.recent_jobs[0].status).toEqual( + 'running' + ); act(() => { mockServer.send( JSON.stringify({ @@ -177,15 +177,11 @@ describe('useWsTemplates hook', () => { }) ); }); - testWrapper.update(); - - expect( - testWrapper.find('TestInner').prop('templates')[0].summary_fields.recent_jobs - ).toHaveLength(3); - expect( - testWrapper.find('TestInner').prop('templates')[0].summary_fields - .recent_jobs[0] - ).toEqual({ + + await waitFor(() => + expect(getTemplates()[0].summary_fields.recent_jobs).toHaveLength(3) + ); + expect(getTemplates()[0].summary_fields.recent_jobs[0]).toEqual({ id: 13, status: 'running', finished: null, From cfa1f5bb4a91af03cb18973742d4494ecca34ee5 Mon Sep 17 00:00:00 2001 From: blaipr Date: Wed, 17 Jun 2026 11:21:01 +0200 Subject: [PATCH 2/2] Address Copilot review feedback on PR #475 RTL tests Await the request() promise inside act() so the resulting state updates flush within act (no not-wrapped-in-act warnings); for the isLoading test capture the pending request and await it in the act() that resolves makeRequest. --- awx/ui/src/hooks/useRequest.test.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/awx/ui/src/hooks/useRequest.test.js b/awx/ui/src/hooks/useRequest.test.js index bbe3838f7..02d7360f8 100644 --- a/awx/ui/src/hooks/useRequest.test.js +++ b/awx/ui/src/hooks/useRequest.test.js @@ -40,7 +40,7 @@ describe('useRequest hooks', () => { render(); await act(async () => { - latest().request(); + await latest().request(); }); expect(latest().result).toEqual({ data: 'foo' }); }); @@ -54,12 +54,16 @@ describe('useRequest hooks', () => { makeRequest.mockReturnValue(promise); render(); + let requestPromise; await act(async () => { - latest().request(); + // capture (don't await) the pending request so isLoading stays true + requestPromise = latest().request(); }); expect(latest().isLoading).toEqual(true); await act(async () => { resolve({ data: 'foo' }); + // await the request inside act so its state updates flush within act + await requestPromise; }); expect(latest().isLoading).toEqual(false); expect(latest().result).toEqual({ data: 'foo' }); @@ -72,7 +76,7 @@ describe('useRequest hooks', () => { expect(makeRequest).not.toHaveBeenCalled(); await act(async () => { - latest().request(); + await latest().request(); }); expect(makeRequest).toHaveBeenCalledTimes(1); }); @@ -102,17 +106,17 @@ describe('useRequest hooks', () => { render(); await act(async () => { - latest().request(true); + await latest().request(true); }); expect(latest().result).toEqual({}); expect(latest().error).toEqual(error); await act(async () => { - latest().request(); + await latest().request(); }); expect(latest().result).toEqual({ data: 'foo' }); expect(latest().error).toEqual(null); await act(async () => { - latest().request(true); + await latest().request(true); }); expect(latest().result).toEqual({}); expect(latest().error).toEqual(error);