diff --git a/awx/ui/src/App.test.js b/awx/ui/src/App.test.js index b834bfb00..6676e01c2 100644 --- a/awx/ui/src/App.test.js +++ b/awx/ui/src/App.test.js @@ -1,16 +1,27 @@ import React from 'react'; -import { act } from 'react-dom/test-utils'; +import { render, screen, waitFor } from '@testing-library/react'; import { RootAPI } from 'api'; import * as SessionContext from 'contexts/Session'; -import { shallow } from 'enzyme'; -import { mountWithContexts } from '../testUtils/enzymeHelpers'; import * as navigation from 'util/navigation'; import * as auth from 'util/auth'; +import { renderWithContexts } from '../testUtils/rtlContexts'; import App, { ProtectedRoute } from './App'; jest.mock('./api'); jest.mock('util/webWorker', () => jest.fn()); +// Keep the real `locales` map (App.js validates the active language against it) +// but hold i18n activation pending so App stays on its top-level loading shell. +// This mirrors the original shallow render — it asserts App mounts without +// driving the deep provider tree, whose ConfigProvider/SessionProvider are +// globally mocked in setupTests and warn when mounted without a `value` prop. +jest.mock('./i18nLoader', () => ({ + ...jest.requireActual('./i18nLoader'), + // plain function, not jest.fn — resetMocks would strip a jest.fn's impl and + // make App.js's `dynamicActivate(...).then(...)` throw on undefined. + dynamicActivate: () => new Promise(() => {}), +})); + describe('', () => { beforeEach(() => { RootAPI.readAssetVariables.mockResolvedValue({ @@ -20,6 +31,13 @@ describe('', () => { }); }); + afterEach(() => { + // restoreAllMocks (not clearAllMocks) so jest.spyOn spies are actually + // restored — with resetMocks:true a leftover spy leaks into later tests + // (or partial reruns) as a reset spy that returns undefined. + jest.restoreAllMocks(); + }); + test('renders ok', async () => { const contextValues = { setAuthRedirectTo: jest.fn(), @@ -31,12 +49,12 @@ describe('', () => { .spyOn(SessionContext, 'useSession') .mockImplementation(() => contextValues); - let wrapper; - await act(async () => { - wrapper = shallow(); - }); - expect(wrapper.length).toBe(1); - jest.clearAllMocks(); + // The default export self-mounts HashRouter/CompatRouter, so render it + // directly rather than wrapping it again. dynamicActivate is held pending + // (see mock above) so App stays on its loading shell — asserting the app + // mounted, the RTL counterpart of the original shallow length check. + const { container } = render(); + expect(container).toHaveTextContent('Loading...'); }); test('redirect to login override', async () => { @@ -56,16 +74,13 @@ describe('', () => { .spyOn(SessionContext, 'useSession') .mockImplementation(() => contextValues); - await act(async () => { - mountWithContexts( - -
foo
-
- ); - }); + renderWithContexts( + +
foo
+
+ ); - expect(replaceSpy).toHaveBeenCalled(); - replaceSpy.mockRestore(); + await waitFor(() => expect(replaceSpy).toHaveBeenCalled()); }); test('renders children when authenticated', async () => { @@ -77,15 +92,12 @@ describe('', () => { })); jest.spyOn(auth, 'isAuthenticated').mockReturnValue(true); - let wrapper; - await act(async () => { - wrapper = mountWithContexts( - -
foo
-
- ); - }); - expect(wrapper.find('#protected-child').length).toBeGreaterThan(0); - jest.restoreAllMocks(); + renderWithContexts( + +
foo
+
+ ); + + expect(await screen.findByText('foo')).toBeInTheDocument(); }); }); diff --git a/awx/ui/src/util/omitProps.test.js b/awx/ui/src/util/omitProps.test.js index 03fedcfcb..7f055d037 100644 --- a/awx/ui/src/util/omitProps.test.js +++ b/awx/ui/src/util/omitProps.test.js @@ -1,35 +1,39 @@ import React from 'react'; -import { mount } from 'enzyme'; +import { render } from '@testing-library/react'; import omitProps from './omitProps'; +// omitProps returns a component that forwards its props (minus the omitted +// ones) to the wrapped element. With a plain 'div' the forwarded props land as +// DOM attributes, so the enzyme `.prop('foo')` checks become attribute checks +// on the rendered node (present attribute === forwarded prop; absent === omitted). describe('omitProps', () => { test('should render child component', () => { const Omit = omitProps('div'); - const wrapper = mount(); + const { container } = render(); - const div = wrapper.find('div'); - expect(div).toHaveLength(1); - expect(div.prop('foo')).toEqual('one'); - expect(div.prop('bar')).toEqual('two'); + const div = container.querySelector('div'); + expect(div).not.toBeNull(); + expect(div.getAttribute('foo')).toEqual('one'); + expect(div.getAttribute('bar')).toEqual('two'); }); test('should not pass omitted props to child component', () => { const Omit = omitProps('div', 'foo', 'bar'); - const wrapper = mount(); + const { container } = render(); - const div = wrapper.find('div'); - expect(div).toHaveLength(1); - expect(div.prop('foo')).toEqual(undefined); - expect(div.prop('bar')).toEqual(undefined); + const div = container.querySelector('div'); + expect(div).not.toBeNull(); + expect(div.hasAttribute('foo')).toBe(false); + expect(div.hasAttribute('bar')).toBe(false); }); test('should support mix of omitted and non-omitted props', () => { const Omit = omitProps('div', 'foo'); - const wrapper = mount(); + const { container } = render(); - const div = wrapper.find('div'); - expect(div).toHaveLength(1); - expect(div.prop('foo')).toEqual(undefined); - expect(div.prop('bar')).toEqual('two'); + const div = container.querySelector('div'); + expect(div).not.toBeNull(); + expect(div.hasAttribute('foo')).toBe(false); + expect(div.getAttribute('bar')).toEqual('two'); }); });