diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.test.jsx
new file mode 100644
index 0000000000..d09cf92ae1
--- /dev/null
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.test.jsx
@@ -0,0 +1,81 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import { InventorySourcesAPI } from '@api';
+import InventorySourcesList from './InventorySourcesList';
+
+jest.mock('@api/models/InventorySources');
+
+const nodeResource = {
+ id: 1,
+ name: 'Test Inventory Source',
+ unified_job_type: 'workflow_approval',
+};
+const onUpdateNodeResource = jest.fn();
+
+describe('InventorySourcesList', () => {
+ let wrapper;
+ afterEach(() => {
+ wrapper.unmount();
+ });
+ test('Row selected when nodeResource id matches row id and clicking new row makes expected callback', async () => {
+ InventorySourcesAPI.read.mockResolvedValueOnce({
+ data: {
+ count: 2,
+ results: [
+ {
+ id: 1,
+ name: 'Test Inventory Source',
+ type: 'inventory_source',
+ url: '/api/v2/inventory_sources/1',
+ },
+ {
+ id: 2,
+ name: 'Test Inventory Source 2',
+ type: 'inventory_source',
+ url: '/api/v2/inventory_sources/2',
+ },
+ ],
+ },
+ });
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
+ expect(
+ wrapper.find('CheckboxListItem[name="Test Inventory Source"]').props()
+ .isSelected
+ ).toBe(true);
+ expect(
+ wrapper.find('CheckboxListItem[name="Test Inventory Source 2"]').props()
+ .isSelected
+ ).toBe(false);
+ wrapper
+ .find('CheckboxListItem[name="Test Inventory Source 2"]')
+ .simulate('click');
+ expect(onUpdateNodeResource).toHaveBeenCalledWith({
+ id: 2,
+ name: 'Test Inventory Source 2',
+ type: 'inventory_source',
+ url: '/api/v2/inventory_sources/2',
+ });
+ });
+ test('Error shown when read() request errors', async () => {
+ InventorySourcesAPI.read.mockRejectedValue(new Error());
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
+ expect(wrapper.find('ErrorDetail').length).toBe(1);
+ });
+});
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.test.jsx
new file mode 100644
index 0000000000..d5d8097313
--- /dev/null
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.test.jsx
@@ -0,0 +1,81 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import { JobTemplatesAPI } from '@api';
+import JobTemplatesList from './JobTemplatesList';
+
+jest.mock('@api/models/JobTemplates');
+
+const nodeResource = {
+ id: 1,
+ name: 'Test Job Template',
+ unified_job_type: 'job',
+};
+const onUpdateNodeResource = jest.fn();
+
+describe('JobTemplatesList', () => {
+ let wrapper;
+ afterEach(() => {
+ wrapper.unmount();
+ });
+ test('Row selected when nodeResource id matches row id and clicking new row makes expected callback', async () => {
+ JobTemplatesAPI.read.mockResolvedValueOnce({
+ data: {
+ count: 2,
+ results: [
+ {
+ id: 1,
+ name: 'Test Job Template',
+ type: 'job_template',
+ url: '/api/v2/job_templates/1',
+ },
+ {
+ id: 2,
+ name: 'Test Job Template 2',
+ type: 'job_template',
+ url: '/api/v2/job_templates/2',
+ },
+ ],
+ },
+ });
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
+ expect(
+ wrapper.find('CheckboxListItem[name="Test Job Template"]').props()
+ .isSelected
+ ).toBe(true);
+ expect(
+ wrapper.find('CheckboxListItem[name="Test Job Template 2"]').props()
+ .isSelected
+ ).toBe(false);
+ wrapper
+ .find('CheckboxListItem[name="Test Job Template 2"]')
+ .simulate('click');
+ expect(onUpdateNodeResource).toHaveBeenCalledWith({
+ id: 2,
+ name: 'Test Job Template 2',
+ type: 'job_template',
+ url: '/api/v2/job_templates/2',
+ });
+ });
+ test('Error shown when read() request errors', async () => {
+ JobTemplatesAPI.read.mockRejectedValue(new Error());
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
+ expect(wrapper.find('ErrorDetail').length).toBe(1);
+ });
+});
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.test.jsx
new file mode 100644
index 0000000000..c4a1306fb4
--- /dev/null
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.test.jsx
@@ -0,0 +1,239 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import {
+ InventorySourcesAPI,
+ JobTemplatesAPI,
+ ProjectsAPI,
+ WorkflowJobTemplatesAPI,
+} from '@api';
+import NodeTypeStep from './NodeTypeStep';
+
+jest.mock('@api/models/InventorySources');
+jest.mock('@api/models/JobTemplates');
+jest.mock('@api/models/Projects');
+jest.mock('@api/models/WorkflowJobTemplates');
+
+const onUpdateDescription = jest.fn();
+const onUpdateName = jest.fn();
+const onUpdateNodeResource = jest.fn();
+const onUpdateNodeType = jest.fn();
+const onUpdateTimeout = jest.fn();
+
+describe('NodeTypeStep', () => {
+ beforeAll(() => {
+ JobTemplatesAPI.read.mockResolvedValue({
+ data: {
+ count: 1,
+ results: [
+ {
+ id: 1,
+ name: 'Test Job Template',
+ type: 'job_template',
+ url: '/api/v2/job_templates/1',
+ },
+ ],
+ },
+ });
+ ProjectsAPI.read.mockResolvedValue({
+ data: {
+ count: 1,
+ results: [
+ {
+ id: 1,
+ name: 'Test Project',
+ type: 'project',
+ url: '/api/v2/projects/1',
+ },
+ ],
+ },
+ });
+ InventorySourcesAPI.read.mockResolvedValue({
+ data: {
+ count: 1,
+ results: [
+ {
+ id: 1,
+ name: 'Test Inventory Source',
+ type: 'inventory_source',
+ url: '/api/v2/inventory_sources/1',
+ },
+ ],
+ },
+ });
+ WorkflowJobTemplatesAPI.read.mockResolvedValue({
+ data: {
+ count: 1,
+ results: [
+ {
+ id: 1,
+ name: 'Test Workflow Job Template',
+ type: 'workflow_job_template',
+ url: '/api/v2/workflow_job_templates/1',
+ },
+ ],
+ },
+ });
+ });
+ afterAll(() => {
+ jest.clearAllMocks();
+ });
+ test('It shows the job template list by default', async () => {
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
+ expect(wrapper.find('AnsibleSelect').prop('value')).toBe('job_template');
+ expect(wrapper.find('JobTemplatesList').length).toBe(1);
+ wrapper.find('DataListRadio').simulate('click');
+ expect(onUpdateNodeResource).toHaveBeenCalledWith({
+ id: 1,
+ name: 'Test Job Template',
+ type: 'job_template',
+ url: '/api/v2/job_templates/1',
+ });
+ });
+ test('It shows the project list when node type is project sync', async () => {
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
+ expect(wrapper.find('AnsibleSelect').prop('value')).toBe('project_sync');
+ expect(wrapper.find('ProjectsList').length).toBe(1);
+ wrapper.find('DataListRadio').simulate('click');
+ expect(onUpdateNodeResource).toHaveBeenCalledWith({
+ id: 1,
+ name: 'Test Project',
+ type: 'project',
+ url: '/api/v2/projects/1',
+ });
+ });
+ test('It shows the inventory source list when node type is inventory source sync', async () => {
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
+ expect(wrapper.find('AnsibleSelect').prop('value')).toBe(
+ 'inventory_source_sync'
+ );
+ expect(wrapper.find('InventorySourcesList').length).toBe(1);
+ wrapper.find('DataListRadio').simulate('click');
+ expect(onUpdateNodeResource).toHaveBeenCalledWith({
+ id: 1,
+ name: 'Test Inventory Source',
+ type: 'inventory_source',
+ url: '/api/v2/inventory_sources/1',
+ });
+ });
+ test('It shows the workflow job template list when node type is workflow job template', async () => {
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
+ expect(wrapper.find('AnsibleSelect').prop('value')).toBe(
+ 'workflow_job_template'
+ );
+ expect(wrapper.find('WorkflowJobTemplatesList').length).toBe(1);
+ wrapper.find('DataListRadio').simulate('click');
+ expect(onUpdateNodeResource).toHaveBeenCalledWith({
+ id: 1,
+ name: 'Test Workflow Job Template',
+ type: 'workflow_job_template',
+ url: '/api/v2/workflow_job_templates/1',
+ });
+ });
+ test('It shows the approval form fields when node type is approval', async () => {
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
+ expect(wrapper.find('AnsibleSelect').prop('value')).toBe('approval');
+ expect(wrapper.find('input#approval-name').length).toBe(1);
+ expect(wrapper.find('input#approval-description').length).toBe(1);
+ expect(wrapper.find('input#approval-timeout-minutes').length).toBe(1);
+ expect(wrapper.find('input#approval-timeout-seconds').length).toBe(1);
+
+ await act(async () => {
+ wrapper.find('input#approval-name').simulate('change', {
+ target: { value: 'Test Approval', name: 'name' },
+ });
+ });
+
+ expect(onUpdateName).toHaveBeenCalledWith('Test Approval');
+
+ await act(async () => {
+ wrapper.find('input#approval-description').simulate('change', {
+ target: { value: 'Test Approval Description', name: 'description' },
+ });
+ });
+
+ expect(onUpdateDescription).toHaveBeenCalledWith(
+ 'Test Approval Description'
+ );
+
+ await act(async () => {
+ wrapper.find('input#approval-timeout-minutes').simulate('change', {
+ target: { value: 5, name: 'timeoutMinutes' },
+ });
+ });
+
+ expect(onUpdateTimeout).toHaveBeenCalledWith(300);
+
+ await act(async () => {
+ wrapper.find('input#approval-timeout-seconds').simulate('change', {
+ target: { value: 30, name: 'timeoutSeconds' },
+ });
+ });
+
+ expect(onUpdateTimeout).toHaveBeenCalledWith(330);
+ });
+});
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.test.jsx
new file mode 100644
index 0000000000..be4b588ce2
--- /dev/null
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.test.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import { ProjectsAPI } from '@api';
+import ProjectsList from './ProjectsList';
+
+jest.mock('@api/models/Projects');
+
+const nodeResource = {
+ id: 1,
+ name: 'Test Project',
+ unified_job_type: 'project_update',
+};
+const onUpdateNodeResource = jest.fn();
+
+describe('ProjectsList', () => {
+ let wrapper;
+ afterEach(() => {
+ wrapper.unmount();
+ });
+ test('Row selected when nodeResource id matches row id and clicking new row makes expected callback', async () => {
+ ProjectsAPI.read.mockResolvedValueOnce({
+ data: {
+ count: 2,
+ results: [
+ {
+ id: 1,
+ name: 'Test Project',
+ type: 'project',
+ url: '/api/v2/projects/1',
+ },
+ {
+ id: 2,
+ name: 'Test Project 2',
+ type: 'project',
+ url: '/api/v2/projects/2',
+ },
+ ],
+ },
+ });
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
+ expect(
+ wrapper.find('CheckboxListItem[name="Test Project"]').props().isSelected
+ ).toBe(true);
+ expect(
+ wrapper.find('CheckboxListItem[name="Test Project 2"]').props().isSelected
+ ).toBe(false);
+ wrapper.find('CheckboxListItem[name="Test Project 2"]').simulate('click');
+ expect(onUpdateNodeResource).toHaveBeenCalledWith({
+ id: 2,
+ name: 'Test Project 2',
+ type: 'project',
+ url: '/api/v2/projects/2',
+ });
+ });
+ test('Error shown when read() request errors', async () => {
+ ProjectsAPI.read.mockRejectedValue(new Error());
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
+ expect(wrapper.find('ErrorDetail').length).toBe(1);
+ });
+});
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/WorkflowJobTemplatesList.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/WorkflowJobTemplatesList.test.jsx
new file mode 100644
index 0000000000..69b63dd7d9
--- /dev/null
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/WorkflowJobTemplatesList.test.jsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import { WorkflowJobTemplatesAPI } from '@api';
+import WorkflowJobTemplatesList from './WorkflowJobTemplatesList';
+
+jest.mock('@api/models/WorkflowJobTemplates');
+
+const nodeResource = {
+ id: 1,
+ name: 'Test Workflow Job Template',
+ unified_job_type: 'workflow_job',
+};
+const onUpdateNodeResource = jest.fn();
+
+describe('WorkflowJobTemplatesList', () => {
+ let wrapper;
+ afterEach(() => {
+ wrapper.unmount();
+ });
+ test('Row selected when nodeResource id matches row id and clicking new row makes expected callback', async () => {
+ WorkflowJobTemplatesAPI.read.mockResolvedValueOnce({
+ data: {
+ count: 2,
+ results: [
+ {
+ id: 1,
+ name: 'Test Workflow Job Template',
+ type: 'workflow_job_template',
+ url: '/api/v2/workflow_job_templates/1',
+ },
+ {
+ id: 2,
+ name: 'Test Workflow Job Template 2',
+ type: 'workflow_job_template',
+ url: '/api/v2/workflow_job_templates/2',
+ },
+ ],
+ },
+ });
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
+ expect(
+ wrapper
+ .find('CheckboxListItem[name="Test Workflow Job Template"]')
+ .props().isSelected
+ ).toBe(true);
+ expect(
+ wrapper
+ .find('CheckboxListItem[name="Test Workflow Job Template 2"]')
+ .props().isSelected
+ ).toBe(false);
+ wrapper
+ .find('CheckboxListItem[name="Test Workflow Job Template 2"]')
+ .simulate('click');
+ expect(onUpdateNodeResource).toHaveBeenCalledWith({
+ id: 2,
+ name: 'Test Workflow Job Template 2',
+ type: 'workflow_job_template',
+ url: '/api/v2/workflow_job_templates/2',
+ });
+ });
+ test('Error shown when read() request errors', async () => {
+ WorkflowJobTemplatesAPI.read.mockRejectedValue(new Error());
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
+ expect(wrapper.find('ErrorDetail').length).toBe(1);
+ });
+});