Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 40 additions & 28 deletions awx/ui/src/App.test.js
Original file line number Diff line number Diff line change
@@ -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('<App />', () => {
beforeEach(() => {
RootAPI.readAssetVariables.mockResolvedValue({
Expand All @@ -20,6 +31,13 @@ describe('<App />', () => {
});
});

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(),
Expand All @@ -31,12 +49,12 @@ describe('<App />', () => {
.spyOn(SessionContext, 'useSession')
.mockImplementation(() => contextValues);

let wrapper;
await act(async () => {
wrapper = shallow(<App />);
});
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(<App />);
expect(container).toHaveTextContent('Loading...');
});

test('redirect to login override', async () => {
Expand All @@ -56,16 +74,13 @@ describe('<App />', () => {
.spyOn(SessionContext, 'useSession')
.mockImplementation(() => contextValues);

await act(async () => {
mountWithContexts(
<ProtectedRoute>
<div>foo</div>
</ProtectedRoute>
);
});
renderWithContexts(
<ProtectedRoute>
<div>foo</div>
</ProtectedRoute>
);

expect(replaceSpy).toHaveBeenCalled();
replaceSpy.mockRestore();
await waitFor(() => expect(replaceSpy).toHaveBeenCalled());
});

test('renders children when authenticated', async () => {
Expand All @@ -77,15 +92,12 @@ describe('<App />', () => {
}));
jest.spyOn(auth, 'isAuthenticated').mockReturnValue(true);

let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<ProtectedRoute>
<div id="protected-child">foo</div>
</ProtectedRoute>
);
});
expect(wrapper.find('#protected-child').length).toBeGreaterThan(0);
jest.restoreAllMocks();
renderWithContexts(
<ProtectedRoute>
<div id="protected-child">foo</div>
</ProtectedRoute>
);

expect(await screen.findByText('foo')).toBeInTheDocument();
});
});
36 changes: 20 additions & 16 deletions awx/ui/src/util/omitProps.test.js
Original file line number Diff line number Diff line change
@@ -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(<Omit foo="one" bar="two" />);
const { container } = render(<Omit foo="one" bar="two" />);

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(<Omit foo="one" bar="two" />);
const { container } = render(<Omit foo="one" bar="two" />);

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(<Omit foo="one" bar="two" />);
const { container } = render(<Omit foo="one" bar="two" />);

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