From 3650d0310ad7290b1bd3c17c98024eef67b99d89 Mon Sep 17 00:00:00 2001 From: blaipr Date: Tue, 16 Jun 2026 12:06:31 +0200 Subject: [PATCH 1/2] enzyme -> RTL: convert the Dashboard screen suites Migrate the Dashboard screen's test suite off enzyme/mountWithContexts onto renderWithContexts (React Testing Library): Dashboard page, the job-graph panel, and the shared Count component. The d3 LineChart is stubbed (jsdom has no SVG geometry) and coverage kept on the surrounding controls and graph API calls. Behaviour and assertions are preserved. --- .../src/screens/Dashboard/Dashboard.test.js | 89 ++++++++++----- .../screens/Dashboard/DashboardGraph.test.js | 107 +++++++++++------- .../screens/Dashboard/shared/Count.test.js | 17 ++- 3 files changed, 135 insertions(+), 78 deletions(-) diff --git a/awx/ui/src/screens/Dashboard/Dashboard.test.js b/awx/ui/src/screens/Dashboard/Dashboard.test.js index dfc06ff60..70c722799 100644 --- a/awx/ui/src/screens/Dashboard/Dashboard.test.js +++ b/awx/ui/src/screens/Dashboard/Dashboard.test.js @@ -1,45 +1,82 @@ import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { DashboardAPI, RootAPI } from 'api'; -import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import { screen, waitFor } from '@testing-library/react'; +import { + DashboardAPI, + RootAPI, + UnifiedJobTemplatesAPI, + JobTemplatesAPI, + WorkflowJobTemplatesAPI, +} from 'api'; +import { renderWithContexts } from '../../../testUtils/rtlContexts'; import Dashboard from './Dashboard'; jest.mock('../../api'); +// DashboardGraph's LineChart draws with d3, which needs +// SVGPathElement.getTotalLength (absent in jsdom). The chart isn't what these +// tests cover, so stub it and assert on the dashboard's tabs/counts + requests. +jest.mock('./shared/LineChart', () => () =>
); + describe('', () => { - let pageWrapper; let graphRequest; - beforeEach(async () => { - await act(async () => { - DashboardAPI.read.mockResolvedValue({}); - RootAPI.readAssetVariables.mockResolvedValue({ - data: { - BRAND_NAME: 'AWX', - }, - }); - graphRequest = DashboardAPI.readJobGraph; - graphRequest.mockResolvedValue({}); - pageWrapper = mountWithContexts(); + beforeEach(() => { + DashboardAPI.read.mockResolvedValue({}); + RootAPI.readAssetVariables.mockResolvedValue({ + data: { + BRAND_NAME: 'AWX', + }, + }); + graphRequest = DashboardAPI.readJobGraph; + graphRequest.mockResolvedValue({}); + UnifiedJobTemplatesAPI.read.mockResolvedValue({ + data: { count: 0, results: [] }, + }); + UnifiedJobTemplatesAPI.readOptions.mockResolvedValue({ + data: { actions: {}, related_search_fields: [] }, + }); + JobTemplatesAPI.readOptions.mockResolvedValue({ + data: { actions: {} }, }); + WorkflowJobTemplatesAPI.readOptions.mockResolvedValue({ + data: { actions: {} }, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); }); - test('initially renders without crashing', () => { - expect(pageWrapper.length).toBe(1); + test('initially renders without crashing', async () => { + renderWithContexts(); + expect( + await screen.findByRole('tab', { name: 'Job status graph tab' }) + ).toBeInTheDocument(); }); - test('renders dashboard graph by default', () => { - expect(pageWrapper.find('LineChart').length).toBe(1); + test('renders dashboard graph by default', async () => { + renderWithContexts(); + // The Job status tab is active by default, so DashboardGraph mounts and + // requests the default (all/month) job graph data. + await screen.findByRole('tab', { name: 'Job status graph tab' }); + expect(await screen.findByTestId('line-chart')).toBeInTheDocument(); + await waitFor(() => + expect(graphRequest).toHaveBeenCalledWith({ + job_type: 'all', + period: 'month', + }) + ); }); test('renders template list when the active tab is changed', async () => { - expect(pageWrapper.find('DashboardTemplateList').length).toBe(0); - await act(async () => { - pageWrapper - .find('button[aria-label="Recent Templates list tab"]') - .simulate('click'); + const { user } = renderWithContexts(); + const templatesTab = await screen.findByRole('tab', { + name: 'Recent Templates list tab', }); - pageWrapper.update(); - expect(pageWrapper.find('TemplateList').length).toBe(1); + await user.click(templatesTab); + // TemplateList mounts and fetches; with an empty result it renders its + // empty-state, confirming the list (not the graph) is now shown. + expect(await screen.findByText('No Templates Found')).toBeInTheDocument(); + await waitFor(() => expect(UnifiedJobTemplatesAPI.read).toHaveBeenCalled()); }); }); diff --git a/awx/ui/src/screens/Dashboard/DashboardGraph.test.js b/awx/ui/src/screens/Dashboard/DashboardGraph.test.js index 606b068da..15abf8e02 100644 --- a/awx/ui/src/screens/Dashboard/DashboardGraph.test.js +++ b/awx/ui/src/screens/Dashboard/DashboardGraph.test.js @@ -1,61 +1,82 @@ import React from 'react'; -import { act } from 'react-dom/test-utils'; +import { screen, waitFor, within } from '@testing-library/react'; import { DashboardAPI } from 'api'; -import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import { renderWithContexts } from '../../../testUtils/rtlContexts'; import DashboardGraph from './DashboardGraph'; jest.mock('../../api'); +// LineChart renders via d3, which relies on SVGPathElement.getTotalLength — +// not implemented by jsdom — so the real chart throws while drawing. The chart +// itself isn't under test here (the filter controls and the data request are), +// so stub it out and keep the assertions on the surrounding UI + API calls. +jest.mock('./shared/LineChart', () => () =>
); + +// The three PF Select toggles all expose the accessible name "Options menu", +// so they can't be told apart by role+name. They carry distinct classNames +// (periodSelect / jobTypeSelect / jobStatusSelect) wired up in the source, so +// scope to each select wrapper and grab its toggle button. +function getToggle(container, className) { + return container.querySelector(`.${className} button.pf-c-select__toggle`); +} + describe('', () => { - let pageWrapper; let graphRequest; - beforeEach(async () => { - await act(async () => { - DashboardAPI.read.mockResolvedValue({}); - graphRequest = DashboardAPI.readJobGraph; - graphRequest.mockResolvedValue({}); - pageWrapper = mountWithContexts(); - }); + beforeEach(() => { + DashboardAPI.read.mockResolvedValue({}); + graphRequest = DashboardAPI.readJobGraph; + graphRequest.mockResolvedValue({}); + }); + + afterEach(() => { + jest.clearAllMocks(); }); - test('renders month-based/all job type chart by default', () => { - expect(graphRequest).toHaveBeenCalledWith({ - job_type: 'all', - period: 'month', - }); + test('renders month-based/all job type chart by default', async () => { + renderWithContexts(); + await waitFor(() => + expect(graphRequest).toHaveBeenCalledWith({ + job_type: 'all', + period: 'month', + }) + ); }); test('should render all three line chart filters with correct number of options', async () => { - expect(pageWrapper.find('Select[variant="single"]')).toHaveLength(3); - await act(async () => { - pageWrapper - .find('Select[placeholderText="Select job type"]') - .prop('onToggle')(true); - }); - pageWrapper.update(); - expect(pageWrapper.find('SelectOption')).toHaveLength(4); - await act(async () => { - pageWrapper - .find('Select[placeholderText="Select job type"]') - .prop('onToggle')(false); - pageWrapper - .find('Select[placeholderText="Select period"]') - .prop('onToggle')(true); - }); - pageWrapper.update(); - expect(pageWrapper.find('SelectOption')).toHaveLength(4); - await act(async () => { - pageWrapper - .find('Select[placeholderText="Select period"]') - .prop('onToggle')(false); - pageWrapper - .find('Select[placeholderText="Select status"]') - .prop('onToggle')(true); - }); - pageWrapper.update(); - expect(pageWrapper.find('SelectOption')).toHaveLength(3); + const { user, container } = renderWithContexts(); + + await waitFor(() => expect(graphRequest).toHaveBeenCalled()); + + const periodToggle = getToggle(container, 'periodSelect'); + const jobTypeToggle = getToggle(container, 'jobTypeSelect'); + const statusToggle = getToggle(container, 'jobStatusSelect'); + expect(periodToggle).toBeInTheDocument(); + expect(jobTypeToggle).toBeInTheDocument(); + expect(statusToggle).toBeInTheDocument(); + + await user.click(jobTypeToggle); + let listbox = await screen.findByRole('listbox'); + expect(within(listbox).getAllByRole('option')).toHaveLength(4); + + await user.click(jobTypeToggle); + await waitFor(() => + expect(screen.queryByRole('listbox')).not.toBeInTheDocument() + ); + + await user.click(periodToggle); + listbox = await screen.findByRole('listbox'); + expect(within(listbox).getAllByRole('option')).toHaveLength(4); + + await user.click(periodToggle); + await waitFor(() => + expect(screen.queryByRole('listbox')).not.toBeInTheDocument() + ); + + await user.click(statusToggle); + listbox = await screen.findByRole('listbox'); + expect(within(listbox).getAllByRole('option')).toHaveLength(3); }); }); diff --git a/awx/ui/src/screens/Dashboard/shared/Count.test.js b/awx/ui/src/screens/Dashboard/shared/Count.test.js index ded7ad951..95f8252cd 100644 --- a/awx/ui/src/screens/Dashboard/shared/Count.test.js +++ b/awx/ui/src/screens/Dashboard/shared/Count.test.js @@ -1,24 +1,23 @@ import React from 'react'; -import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { renderWithContexts } from '../../../../testUtils/rtlContexts'; import Count from './Count'; describe('', () => { - let pageWrapper; - test('initially renders without crashing', () => { - pageWrapper = mountWithContexts(); - expect(pageWrapper.length).toBe(1); + const { container } = renderWithContexts(); + expect(container).toBeInTheDocument(); + expect(container.querySelector('h2')).toBeInTheDocument(); }); test('renders non-failed version of count without prop', () => { - pageWrapper = mountWithContexts(); - expect(pageWrapper.find('h2').hasClass('failed')).toBe(false); + const { container } = renderWithContexts(); + expect(container.querySelector('h2')).not.toHaveClass('failed'); }); test('renders failed version of count with appropriate prop', () => { - pageWrapper = mountWithContexts(); - expect(pageWrapper.find('h2').hasClass('failed')).toBe(true); + const { container } = renderWithContexts(); + expect(container.querySelector('h2')).toHaveClass('failed'); }); }); From 933ba4899d8de975700fd3c936dd03eff1caec61 Mon Sep 17 00:00:00 2001 From: blaipr Date: Wed, 17 Jun 2026 09:56:47 +0200 Subject: [PATCH 2/2] Address Copilot review comments --- awx/ui/src/screens/Dashboard/Dashboard.test.js | 15 ++++++++++++++- .../src/screens/Dashboard/DashboardGraph.test.js | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/awx/ui/src/screens/Dashboard/Dashboard.test.js b/awx/ui/src/screens/Dashboard/Dashboard.test.js index 70c722799..605063b05 100644 --- a/awx/ui/src/screens/Dashboard/Dashboard.test.js +++ b/awx/ui/src/screens/Dashboard/Dashboard.test.js @@ -28,7 +28,20 @@ describe('', () => { }, }); graphRequest = DashboardAPI.readJobGraph; - graphRequest.mockResolvedValue({}); + graphRequest.mockResolvedValue({ + data: { + jobs: { + successful: [ + [1609459200, 2], + [1609545600, 4], + ], + failed: [ + [1609459200, 1], + [1609545600, 0], + ], + }, + }, + }); UnifiedJobTemplatesAPI.read.mockResolvedValue({ data: { count: 0, results: [] }, }); diff --git a/awx/ui/src/screens/Dashboard/DashboardGraph.test.js b/awx/ui/src/screens/Dashboard/DashboardGraph.test.js index 15abf8e02..299e6243c 100644 --- a/awx/ui/src/screens/Dashboard/DashboardGraph.test.js +++ b/awx/ui/src/screens/Dashboard/DashboardGraph.test.js @@ -28,7 +28,20 @@ describe('', () => { beforeEach(() => { DashboardAPI.read.mockResolvedValue({}); graphRequest = DashboardAPI.readJobGraph; - graphRequest.mockResolvedValue({}); + graphRequest.mockResolvedValue({ + data: { + jobs: { + successful: [ + [1609459200, 2], + [1609545600, 4], + ], + failed: [ + [1609459200, 1], + [1609545600, 0], + ], + }, + }, + }); }); afterEach(() => {