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');
});
});