From 6bee3120d1126249ea9b8a7f0e7a67e317e03891 Mon Sep 17 00:00:00 2001 From: saimedhi Date: Wed, 18 Sep 2024 10:14:25 -0700 Subject: [PATCH] Expanded functionality tests for New Workflow and Workflow List components. Signed-off-by: saimedhi --- .../workflow_detail/workflow_detail.test.tsx | 2 +- .../import_workflow/import_workflow_modal.tsx | 5 +- .../new_workflow/new_workflow.test.tsx | 65 +++++++- .../new_workflow/quick_configure_modal.tsx | 6 +- .../pages/workflows/new_workflow/use_case.tsx | 1 + .../workflow_list/delete_workflow_modal.tsx | 6 +- .../workflow_list/workflow_list.test.tsx | 140 +++++++++++++++++- public/pages/workflows/workflows.test.tsx | 32 +++- public/pages/workflows/workflows.tsx | 3 + test/utils.ts | 45 ++++-- 10 files changed, 279 insertions(+), 26 deletions(-) diff --git a/public/pages/workflow_detail/workflow_detail.test.tsx b/public/pages/workflow_detail/workflow_detail.test.tsx index 65953214..825668dc 100644 --- a/public/pages/workflow_detail/workflow_detail.test.tsx +++ b/public/pages/workflow_detail/workflow_detail.test.tsx @@ -41,7 +41,7 @@ const renderWithRouter = ( return { ...render( - + - onModalClose()}> + onModalClose()} + data-testid="cancelImportButton" + > Cancel { const { mockCoreServices } = require('../../../../test'); @@ -19,6 +23,18 @@ jest.mock('../../../services', () => { }; }); +const mockStore = configureStore([]); +const initialState = { + ml: INITIAL_ML_STATE, + presets: { + loading: false, + presetWorkflows: loadPresetTemplates(), + }, +}; +const store = mockStore(initialState); + +const mockDispatch = jest.fn(); + const renderWithRouter = () => render( @@ -31,8 +47,47 @@ const renderWithRouter = () => ); describe('NewWorkflow', () => { - test('renders the search bar', () => { - const { getByPlaceholderText } = renderWithRouter(); + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(ReactReduxHooks, 'useAppDispatch').mockReturnValue(mockDispatch); + }); + + test('renders the preset workflow templates', () => { + const { getByPlaceholderText, getAllByText } = renderWithRouter(); expect(getByPlaceholderText('Search')).toBeInTheDocument(); + expect(getAllByText('Custom')).toHaveLength(1); + expect(getAllByText('Hybrid Search')).toHaveLength(1); + expect(getAllByText('Multimodal Search')).toHaveLength(1); + expect(getAllByText('Semantic Search')).toHaveLength(1); + }); + + test('renders the quick configure for preset workflow templates', async () => { + const { + getAllByTestId, + getAllByText, + getByTestId, + queryByText, + } = renderWithRouter(); + + // Click the first "Go" button on the templates and test Quick Configure. + const goButtons = getAllByTestId('goButton'); + userEvent.click(goButtons[0]); + await waitFor(() => + expect(getAllByText('Quick configure')).toHaveLength(1) + ); + + // Verify that the create button is present in the Quick Configure pop-up. + expect(getByTestId('quickConfigureCreateButton')).toBeInTheDocument(); + + // Click the "Cancel" button in the Quick Configure pop-up. + const quickConfigureCancelButton = getByTestId( + 'quickConfigureCancelButton' + ); + userEvent.click(quickConfigureCancelButton); + + // Ensure the quick configure pop-up is closed after canceling. + await waitFor(() => + expect(queryByText('quickConfigureCreateButton')).toBeNull() + ); }); }); diff --git a/public/pages/workflows/new_workflow/quick_configure_modal.tsx b/public/pages/workflows/new_workflow/quick_configure_modal.tsx index 316a13b4..fbb233af 100644 --- a/public/pages/workflows/new_workflow/quick_configure_modal.tsx +++ b/public/pages/workflows/new_workflow/quick_configure_modal.tsx @@ -123,7 +123,10 @@ export function QuickConfigureModal(props: QuickConfigureModalProps) { /> - props.onClose()}> + props.onClose()} + data-testid="quickConfigureCancelButton" + > Cancel diff --git a/public/pages/workflows/new_workflow/use_case.tsx b/public/pages/workflows/new_workflow/use_case.tsx index 66c76725..741b9aa1 100644 --- a/public/pages/workflows/new_workflow/use_case.tsx +++ b/public/pages/workflows/new_workflow/use_case.tsx @@ -55,6 +55,7 @@ export function UseCase(props: UseCaseProps) { onClick={() => { setIsNameModalOpen(true); }} + data-testid="goButton" > Go diff --git a/public/pages/workflows/workflow_list/delete_workflow_modal.tsx b/public/pages/workflows/workflow_list/delete_workflow_modal.tsx index f0bf94f3..649e5723 100644 --- a/public/pages/workflows/workflow_list/delete_workflow_modal.tsx +++ b/public/pages/workflows/workflow_list/delete_workflow_modal.tsx @@ -98,7 +98,10 @@ export function DeleteWorkflowModal(props: DeleteWorkflowModalProps) { - props.clearDeleteState()}> + props.clearDeleteState()} + data-testid="cancelDeleteWorkflowButton" + > {' '} Cancel @@ -135,6 +138,7 @@ export function DeleteWorkflowModal(props: DeleteWorkflowModalProps) { setIsDeleting(false); props.clearDeleteState(); }} + data-testid="deleteWorkflowButton" fill={true} color="danger" > diff --git a/public/pages/workflows/workflow_list/workflow_list.test.tsx b/public/pages/workflows/workflow_list/workflow_list.test.tsx index 1e39ca72..a2488b0f 100644 --- a/public/pages/workflows/workflow_list/workflow_list.test.tsx +++ b/public/pages/workflows/workflow_list/workflow_list.test.tsx @@ -4,11 +4,15 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; import { Provider } from 'react-redux'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; -import { store } from '../../../store'; import { WorkflowList } from './workflow_list'; +import { mockStore } from '../../../../test/utils'; +import { WORKFLOW_TYPE } from '../../../../common'; +import configureStore from 'redux-mock-store'; jest.mock('../../../services', () => { const { mockCoreServices } = require('../../../../test'); @@ -18,6 +22,33 @@ jest.mock('../../../services', () => { }; }); +const workflows = new Array(20).fill(null).map((_, index) => ({ + id: `workflow_id_${index}`, + name: `workflow_name_${index}`, + type: Object.values(WORKFLOW_TYPE)[ + Math.floor(Math.random() * Object.values(WORKFLOW_TYPE).length) + ], +})); +const workflowSet: [ + string, + string, + WORKFLOW_TYPE +][] = workflows.map(({ id, name, type }): [string, string, WORKFLOW_TYPE] => [ + id, + name, + type, +]); + +const mockStore1 = configureStore([]); +const initialState = { + workflows: { + loading: false, + errorMessage: '', + workflows: mockStore(...workflowSet).getState().workflows.workflows, + }, +}; +const store = mockStore1(initialState); + const renderWithRouter = () => render( @@ -33,5 +64,110 @@ describe('WorkflowList', () => { test('renders the page', () => { const { getAllByText } = renderWithRouter(); expect(getAllByText('Manage existing workflows').length).toBeGreaterThan(0); + expect(getAllByText('Name').length).toBeGreaterThan(0); + expect(getAllByText('Type').length).toBeGreaterThan(0); + expect(getAllByText('Last saved').length).toBeGreaterThan(0); + expect(getAllByText('Actions').length).toBeGreaterThan(0); + expect(getAllByText('workflow_name_0').length).toBeGreaterThan(0); + }); + + test('sorting functionality', async () => { + const { container, getAllByText, queryByText } = renderWithRouter(); + expect(getAllByText('workflow_name_0').length).toBeGreaterThan(0); + + const sortButtons = container.querySelectorAll( + '[data-test-subj="tableHeaderSortButton"]' + ); + + // Sort workflows list by Name + expect(sortButtons[0]).toBeInTheDocument(); + userEvent.click(sortButtons[0]!); + await new Promise((resolve) => setTimeout(resolve, 200)); + expect(queryByText('workflow_name_19')).toBeInTheDocument(); + expect(queryByText('workflow_name_0')).toBeNull(); + userEvent.click(sortButtons[0]!); + await new Promise((resolve) => setTimeout(resolve, 200)); + expect(queryByText('workflow_name_0')).toBeInTheDocument(); + expect(queryByText('workflow_name_9')).toBeInTheDocument(); + expect(queryByText('workflow_name_10')).toBeNull(); + expect(queryByText('workflow_name_19')).toBeNull(); + + // Sort workflows list by Type + expect(sortButtons[1]).toBeInTheDocument(); + userEvent.click(sortButtons[1]!); + + await waitFor(() => { + expect(getAllByText('Custom').length).toBeGreaterThan(0); // Ensures at least one 'Custom' element is present + expect(queryByText('Unknown')).toBeNull(); + }); + userEvent.click(sortButtons[1]!); + await waitFor(() => { + expect(queryByText('Unknown')).toBeNull(); + expect(getAllByText('Custom').length).toBeGreaterThan(0); + }); + }); + + test('pagination functionality', async () => { + const { container, getByText, queryByText } = renderWithRouter(); + + // Rows per page 10 + const rowsPerPageButton = container.querySelector( + '[data-test-subj="tablePaginationPopoverButton"]' + ) as HTMLButtonElement; + expect(rowsPerPageButton).toHaveTextContent('Rows per page: 10'); + + // Default view 10 items per page + expect(getByText('workflow_name_0')).toBeInTheDocument(); + //expect(queryByText('workflow_name_11')).toBeNull(); + expect(queryByText('workflow_name_19')).toBeNull(); + + //Navigate to next page + const nextButton = container.querySelector( + '[data-test-subj="pagination-button-next"]' + ) as HTMLButtonElement; + //console.log('nextButton printed', nextButton); + + userEvent.click(nextButton); + + // Check if item from the next page is visible + await waitFor(() => { + expect(getByText('workflow_name_19')).toBeInTheDocument(); + expect(queryByText('workflow_name_0')).toBeNull(); + }); + + // Navigate to previous page + const previousButton = container.querySelector( + '[data-test-subj="pagination-button-previous"]' + ) as HTMLButtonElement; + + userEvent.click(previousButton); + + // Check if item from the previous page is visible + await waitFor(() => { + expect(getByText('workflow_name_0')).toBeInTheDocument(); + expect(queryByText('workflow_name_19')).toBeNull(); + }); + }); + test('delete action functionality', async () => { + const { getByText, getByTestId, getAllByLabelText } = renderWithRouter(); + const deleteButtons = getAllByLabelText('Delete'); + userEvent.click(deleteButtons[0]); + await waitFor(() => { + expect(getByText('Delete associated resources')).toBeInTheDocument(); + }); + expect(getByTestId('deleteWorkflowButton')).toBeInTheDocument(); + const cancelDeleteWorkflowButton = getByTestId( + 'cancelDeleteWorkflowButton' + ); + expect(cancelDeleteWorkflowButton).toBeInTheDocument(); + userEvent.click(cancelDeleteWorkflowButton); + }); + test('view resources functionality', async () => { + const { getByText, getAllByLabelText } = renderWithRouter(); + const viewResourcesButtons = getAllByLabelText('View resources'); + userEvent.click(viewResourcesButtons[0]); + await waitFor(() => { + expect(getByText('No existing resources found')).toBeInTheDocument(); + }); }); }); diff --git a/public/pages/workflows/workflows.test.tsx b/public/pages/workflows/workflows.test.tsx index 5d82ce9e..6a84edc6 100644 --- a/public/pages/workflows/workflows.test.tsx +++ b/public/pages/workflows/workflows.test.tsx @@ -4,7 +4,9 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; import { @@ -41,8 +43,32 @@ const renderWithRouter = () => ({ }); describe('Workflows', () => { - test('renders the page', () => { - const { getAllByText } = renderWithRouter(); + test('renders the page', async () => { + const { getAllByText, getByTestId, queryByText } = renderWithRouter(); + + // The "Manage Workflows" tab is displayed by default + + // Import Workflow Testing expect(getAllByText('Workflows').length).toBeGreaterThan(0); + const importWorkflowButton = getByTestId('importWorkflowButton'); + await waitFor(() => userEvent.click(importWorkflowButton)); + expect( + getAllByText('Select or drag and drop a file').length + ).toBeGreaterThan(0); + + // Closing or canceling the import + const cancelImportButton = getByTestId('cancelImportButton'); + await waitFor(() => userEvent.click(cancelImportButton)); + expect( + queryByText('Select or drag and drop a file') + ).not.toBeInTheDocument(); + expect(getAllByText('Manage existing workflows').length).toBeGreaterThan(0); + + // When the "Create Workflow" button is clicked, the "New workflow" tab opens + // Create Workflow Testing + const createWorkflowButton = getByTestId('createWorkflowButton'); + expect(createWorkflowButton).toBeInTheDocument(); + await waitFor(() => userEvent.click(createWorkflowButton)); + expect(getAllByText('Create from a template').length).toBeGreaterThan(0); }); }); diff --git a/public/pages/workflows/workflows.tsx b/public/pages/workflows/workflows.tsx index 9239fedc..54d45680 100644 --- a/public/pages/workflows/workflows.tsx +++ b/public/pages/workflows/workflows.tsx @@ -284,6 +284,7 @@ export function Workflows(props: WorkflowsProps) { onClick={() => { setSelectedTabId(WORKFLOWS_TAB.CREATE); }} + data-testid="createWorkflowButton" > Create workflow , @@ -292,6 +293,7 @@ export function Workflows(props: WorkflowsProps) { onClick={() => { setIsImportModalOpen(true); }} + data-testid="importWorkflowButton" > Import workflow , @@ -302,6 +304,7 @@ export function Workflows(props: WorkflowsProps) { onClick={() => { setIsImportModalOpen(true); }} + data-testid="importWorkflowButton" > Import workflow , diff --git a/test/utils.ts b/test/utils.ts index 68c9af5b..10059fa7 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -17,25 +17,30 @@ import { fetchMultimodalSearchMetadata, fetchSemanticSearchMetadata, } from '../public/pages/workflows/new_workflow/utils'; +import fs from 'fs'; +import path from 'path'; -export function mockStore( - workflowId: string, - workflowName: string, - workflowType: WORKFLOW_TYPE -) { +export function mockStore(...workflowSets: [string, string, WORKFLOW_TYPE][]) { return { getState: () => ({ opensearch: INITIAL_OPENSEARCH_STATE, ml: INITIAL_ML_STATE, workflows: { ...INITIAL_WORKFLOWS_STATE, - workflows: { - [workflowId]: generateWorkflow( - workflowId, - workflowName, - workflowType - ), - }, + workflows: workflowSets.reduce( + ( + acc: Record, + [workflowId, workflowName, workflowType] + ) => { + acc[workflowId] = generateWorkflow( + workflowId, + workflowName, + workflowType + ); + return acc; + }, + {} + ), }, presets: INITIAL_PRESETS_STATE, }), @@ -58,6 +63,7 @@ function generateWorkflow( ui_metadata: getConfig(workflowType), }; } + function getConfig(workflowType: WORKFLOW_TYPE) { let uiMetadata = {} as UIState; switch (workflowType) { @@ -81,6 +87,21 @@ function getConfig(workflowType: WORKFLOW_TYPE) { return uiMetadata; } +const templatesDir = path.resolve( + __dirname, + '..', + 'server', + 'resources', + 'templates' +); +export const loadPresetTemplates = () => + fs + .readdirSync(templatesDir) + .filter((file) => file.endsWith('.json')) + .map((file) => + JSON.parse(fs.readFileSync(path.join(templatesDir, file), 'utf8')) + ); + export const resizeObserverMock = jest.fn().mockImplementation(() => ({ observe: jest.fn(), unobserve: jest.fn(),