From d51781540a3ed5a6366b9c360bc10074f189a903 Mon Sep 17 00:00:00 2001 From: Maham Akif <113524403+mahamakifdar19@users.noreply.github.com> Date: Thu, 13 Jun 2024 15:55:01 +0500 Subject: [PATCH] fix: added unit tests for AI Curation (#418) Co-authored-by: Maham Akif --- jest.config.js | 2 +- src/components/aiCuration/AskXpert.jsx | 1 + .../aiCuration/AskXpertQueryField.jsx | 1 + .../aiCuration/tests/AskXpert.test.jsx | 130 ++++++++++++++++++ .../tests/AskXpertQueryField.test.jsx | 57 ++++++++ .../aiCuration/tests/XpertResultCard.test.jsx | 124 +++++++++++++++++ .../aiCuration/tests/data/hooks.test.jsx | 121 ++++++++++++++++ .../xpertResultCard/XpertResultCard.jsx | 2 + 8 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 src/components/aiCuration/tests/AskXpert.test.jsx create mode 100644 src/components/aiCuration/tests/AskXpertQueryField.test.jsx create mode 100644 src/components/aiCuration/tests/XpertResultCard.test.jsx create mode 100644 src/components/aiCuration/tests/data/hooks.test.jsx diff --git a/jest.config.js b/jest.config.js index 1dc0bbce..15e7d2d9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,5 +2,5 @@ const { createConfig } = require('@openedx/frontend-build'); module.exports = createConfig('jest', { setupFiles: ['/src/setupTest.js'], - coveragePathIgnorePatterns: ['src/setupTest.js', 'src/i18n', 'src/components/aiCuration', 'src/index.jsx'], + coveragePathIgnorePatterns: ['src/setupTest.js', 'src/i18n', 'src/index.jsx'], }); diff --git a/src/components/aiCuration/AskXpert.jsx b/src/components/aiCuration/AskXpert.jsx index 4f2195e0..0a8c66c7 100644 --- a/src/components/aiCuration/AskXpert.jsx +++ b/src/components/aiCuration/AskXpert.jsx @@ -181,6 +181,7 @@ const AskXpert = ({ src={Close} iconAs={Icon} onClick={() => onClose()} + aria-label="Close Ask Xpert Modal" />

{ onSubmit(textInputValue); } }} + aria-label="Enter Ask Xpert Query" /> )} /> diff --git a/src/components/aiCuration/tests/AskXpert.test.jsx b/src/components/aiCuration/tests/AskXpert.test.jsx new file mode 100644 index 00000000..8ef91dc0 --- /dev/null +++ b/src/components/aiCuration/tests/AskXpert.test.jsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { + render, screen, fireEvent, +} from '@testing-library/react'; +import axios from 'axios'; +import '@testing-library/jest-dom/extend-expect'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import AskXpert from '../AskXpert'; +import EnterpriseCatalogAiCurationApiService from '../data/service'; + +jest.mock('axios'); + +const messages = { + en: { + 'catalogPage.askXpert.title': 'Xpert', + 'catalogPage.askXpert.description': 'Use AI to narrow your search.', + 'catalogPage.askXpert.loadingMessage': 'Xpert is thinking! Wait with us while we generate a catalog for “{query}”...', + 'catalogPage.askXpert.cancel': 'Cancel', + }, +}; + +describe('AskXpert Component', () => { + const mockCatalogName = 'Mock Catalog'; + const mockOnClose = jest.fn(); + const mockOnXpertData = jest.fn(); + const mockTaskId = 'mock-task-id'; + const mockThreshold = 0.5; + const mockResponseData = { status: 200, data: { result: {} } }; + const mockErrorResponse = { status: 404, data: { error: 'Not Found' } }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('renders AskXpert component correctly', () => { + render( + + + , + ); + + expect(screen.getByText(/Xpert/i)).toBeInTheDocument(); + expect(screen.getByText(/Use AI to narrow your search/i)).toBeInTheDocument(); + }); + + test('calls onClose when close button is clicked', () => { + render( + + + , + ); + + fireEvent.click(screen.getByLabelText('Close Ask Xpert Modal')); + expect(mockOnClose).toHaveBeenCalled(); + }); + + test('postXpertQuery makes a POST request to the correct endpoint', async () => { + const mockQuery = 'mock query'; + + axios.post.mockResolvedValueOnce(mockResponseData); + + const response = await EnterpriseCatalogAiCurationApiService.postXpertQuery(mockQuery, mockCatalogName); + + expect(axios.post).toHaveBeenCalledTimes(1); + expect(axios.post).toHaveBeenCalledWith( + `${EnterpriseCatalogAiCurationApiService.enterpriseCatalogAiCurationServiceUrl}`, + { query: mockQuery, catalog_name: mockCatalogName }, + ); + expect(response).toEqual(mockResponseData); + }); + + test('postXpertQuery handles errors and returns appropriate response', async () => { + const mockQuery = 'mock query'; + + axios.post.mockRejectedValueOnce({ response: mockErrorResponse }); + + const response = await EnterpriseCatalogAiCurationApiService.postXpertQuery(mockQuery, mockCatalogName); + + expect(axios.post).toHaveBeenCalledTimes(1); + expect(axios.post).toHaveBeenCalledWith( + `${EnterpriseCatalogAiCurationApiService.enterpriseCatalogAiCurationServiceUrl}`, + { query: mockQuery, catalog_name: mockCatalogName }, + ); + expect(response).toEqual(mockErrorResponse); + }); + + test('getXpertResults makes a GET request to the correct endpoint with taskId', async () => { + axios.get.mockResolvedValueOnce(mockResponseData); + + const response = await EnterpriseCatalogAiCurationApiService.getXpertResults(mockTaskId); + + expect(axios.get).toHaveBeenCalledTimes(1); + expect(axios.get).toHaveBeenCalledWith( + `${EnterpriseCatalogAiCurationApiService.enterpriseCatalogAiCurationServiceUrl}?task_id=${mockTaskId}`, + ); + expect(response).toEqual(mockResponseData); + }); + + test('getXpertResults makes a GET request with taskId and threshold', async () => { + axios.get.mockResolvedValueOnce(mockResponseData); + + const response = await EnterpriseCatalogAiCurationApiService.getXpertResults(mockTaskId, mockThreshold); + + expect(axios.get).toHaveBeenCalledTimes(1); + expect(axios.get).toHaveBeenCalledWith( + `${EnterpriseCatalogAiCurationApiService.enterpriseCatalogAiCurationServiceUrl}?task_id=${mockTaskId}&threshold=${mockThreshold}`, + ); + expect(response).toEqual(mockResponseData); + }); + + test('getXpertResults handles errors and returns appropriate response', async () => { + axios.get.mockRejectedValueOnce({ response: mockErrorResponse }); + + const response = await EnterpriseCatalogAiCurationApiService.getXpertResults(mockTaskId); + + expect(axios.get).toHaveBeenCalledTimes(1); + expect(axios.get).toHaveBeenCalledWith( + `${EnterpriseCatalogAiCurationApiService.enterpriseCatalogAiCurationServiceUrl}?task_id=${mockTaskId}`, + ); + expect(response).toEqual(mockErrorResponse); + }); +}); diff --git a/src/components/aiCuration/tests/AskXpertQueryField.test.jsx b/src/components/aiCuration/tests/AskXpertQueryField.test.jsx new file mode 100644 index 00000000..8a256b88 --- /dev/null +++ b/src/components/aiCuration/tests/AskXpertQueryField.test.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { + render, screen, fireEvent, waitFor, +} from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import AskXpertQueryField from '../AskXpertQueryField'; + +describe('AskXpertQueryField Component', () => { + const mockOnSubmit = jest.fn(); + const mockIsDisabled = false; + const placeholderText = 'Describe a skill, competency or job title you are trying to train'; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('renders AskXpertQueryField component correctly', () => { + render(); + + const inputElement = screen.getByPlaceholderText(placeholderText); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toBeEnabled(); + }); + + test('triggers onSubmit callback when Send button is clicked', async () => { + render(); + + const inputElement = screen.getByPlaceholderText(placeholderText); + fireEvent.change(inputElement, { target: { value: 'Mock query' } }); + fireEvent.keyDown(inputElement, { key: 'Enter', code: 'Enter' }); + + await waitFor(() => { + expect(mockOnSubmit).toHaveBeenCalledTimes(1); + }); + }); + + test('does not trigger onSubmit callback when input is empty', () => { + render(); + + const inputElement = screen.getByPlaceholderText(placeholderText); + + fireEvent.change(inputElement, { target: { value: '' } }); + fireEvent.keyDown(inputElement, { key: 'Enter', code: 'Enter' }); + + expect(mockOnSubmit).not.toHaveBeenCalled(); + }); + + test('does not trigger onSubmit callback when component is disabled', () => { + render(); + + const inputElement = screen.getByPlaceholderText(placeholderText); + fireEvent.keyDown(inputElement, { key: 'Enter', code: 'Enter' }); + fireEvent.click(screen.getByLabelText('Enter Ask Xpert Query')); + + expect(mockOnSubmit).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/aiCuration/tests/XpertResultCard.test.jsx b/src/components/aiCuration/tests/XpertResultCard.test.jsx new file mode 100644 index 00000000..2aedc28d --- /dev/null +++ b/src/components/aiCuration/tests/XpertResultCard.test.jsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { + render, screen, fireEvent, waitFor, +} from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import XpertResultCard from '../xpertResultCard/XpertResultCard'; +import { useXpertResultsWithThreshold } from '../data/hooks'; +import { CONTENT_TYPE_COURSE, CONTENT_TYPE_PROGRAM, EXEC_ED_TITLE } from '../../../constants'; + +jest.mock('../data/hooks', () => ({ + useXpertResultsWithThreshold: jest.fn(), +})); + +const mockGetXpertResultsWithThreshold = jest.fn(); + +const messages = { + 'catalogs.askXpert.result.card.heading': 'Xpert', + 'catalogs.askXpert.result.card.label.for.results': 'Results: {query}', + 'catalogs.askXpert.result.card.label.for.self.paced.courses': 'Self-paced courses', + 'catalogs.askXpert.result.card.label.for.self.paced.programs': 'Self-paced programs', + 'catalogs.askXpert.result.card.label.for.executive.education.courses': 'Executive education courses', + 'catalogs.askXpert.result.card.error.label': 'An error occurred. Please try a new search', +}; + +const defaultProps = { + taskId: 'task-id', + query: 'test query', + results: { + ocm_courses: [], + exec_ed_courses: [], + programs: [], + }, + onClose: jest.fn(), + onXpertResults: jest.fn(), +}; + +const renderXpertResultCard = (props = {}) => render( + + + , +); + +describe('XpertResultCard', () => { + beforeEach(() => { + useXpertResultsWithThreshold.mockReturnValue({ + loading: false, + error: null, + xpertResultsData: {}, + getXpertResultsWithThreshold: mockGetXpertResultsWithThreshold, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly with default props', () => { + renderXpertResultCard(); + expect(screen.getByText('Xpert')).toBeInTheDocument(); + expect(screen.getByText('Results:')).toBeInTheDocument(); + expect(screen.getByText('Self-paced courses')).toBeInTheDocument(); + expect(screen.getByText('Self-paced programs')).toBeInTheDocument(); + expect(screen.getByText('Executive education courses')).toBeInTheDocument(); + }); + + it('calls onClose when New Search button is clicked', () => { + renderXpertResultCard(); + fireEvent.click(screen.getByText('New Search')); + expect(defaultProps.onClose).toHaveBeenCalledTimes(1); + }); + + it('calls onClose when Close icon is clicked', () => { + renderXpertResultCard(); + fireEvent.click(screen.getByLabelText('Close XpertResultCard')); + expect(defaultProps.onClose).toHaveBeenCalledTimes(1); + }); + + it('updates the threshold value and triggers API call on slider change', async () => { + renderXpertResultCard(); + const slider = screen.getByLabelText('Xpert result card slider'); + fireEvent.change(slider, { target: { value: '0.4' } }); + + await waitFor(() => expect(mockGetXpertResultsWithThreshold).toHaveBeenCalledTimes(1), { timeout: 1500 }); + expect(mockGetXpertResultsWithThreshold).toHaveBeenCalledWith('task-id', 0.4); + }); + + it('displays error message when there is an error', () => { + useXpertResultsWithThreshold.mockReturnValue({ + loading: false, + error: 'An error occurred', + xpertResultsData: {}, + getXpertResultsWithThreshold: mockGetXpertResultsWithThreshold, + }); + + renderXpertResultCard(); + expect(screen.getByText('An error occurred. Please try a new search')).toBeInTheDocument(); + }); + + it('updates xpertResults and calls onXpertResults when xpertResultsData changes', async () => { + const newXpertResultsData = { + ocm_courses: [{ aggregation_key: 'key1' }], + exec_ed_courses: [{ aggregation_key: 'key2' }], + programs: [{ aggregation_key: 'key3' }], + }; + + useXpertResultsWithThreshold.mockReturnValue({ + loading: false, + error: null, + xpertResultsData: newXpertResultsData, + getXpertResultsWithThreshold: mockGetXpertResultsWithThreshold, + }); + + renderXpertResultCard(); + + await waitFor(() => { + expect(defaultProps.onXpertResults).toHaveBeenCalledWith({ + [CONTENT_TYPE_COURSE]: ['key1'], + [EXEC_ED_TITLE]: ['key2'], + [CONTENT_TYPE_PROGRAM]: ['key3'], + }); + }); + }); +}); diff --git a/src/components/aiCuration/tests/data/hooks.test.jsx b/src/components/aiCuration/tests/data/hooks.test.jsx new file mode 100644 index 00000000..fcb3b3d2 --- /dev/null +++ b/src/components/aiCuration/tests/data/hooks.test.jsx @@ -0,0 +1,121 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import useInterval, { useXpertResultsWithThreshold } from '../../data/hooks'; +import EnterpriseCatalogAiCurationApiService from '../../data/service'; + +jest.mock('../../data/service'); +jest.mock('@edx/frontend-platform/logging'); + +describe('useInterval', () => { + jest.useFakeTimers(); + + it('should call callback after specified delay', () => { + const callback = jest.fn(); + + renderHook(() => useInterval(callback, 1000)); + + expect(callback).not.toBeCalled(); + + jest.advanceTimersByTime(1000); + + expect(callback).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(1000); + + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('should not call callback if delay is null', () => { + const callback = jest.fn(); + + renderHook(() => useInterval(callback, null)); + + jest.advanceTimersByTime(1000); + + expect(callback).not.toBeCalled(); + }); + + it('should clear interval on unmount', () => { + const callback = jest.fn(); + + const { unmount } = renderHook(() => useInterval(callback, 1000)); + + unmount(); + + jest.advanceTimersByTime(1000); + + expect(callback).not.toBeCalled(); + }); +}); + +describe('useXpertResultsWithThreshold', () => { + it('should initialize with correct default values', () => { + const { result } = renderHook(() => useXpertResultsWithThreshold()); + + expect(result.current.loading).toBe(false); + expect(result.current.error).toBe(null); + expect(result.current.xpertResultsData).toEqual([]); + }); + + it('should fetch results and update state correctly on success', async () => { + const mockResponse = { + status: 200, + data: { result: { ocm_courses: [], exec_ed_courses: [], programs: [] } }, + }; + EnterpriseCatalogAiCurationApiService.getXpertResults.mockResolvedValue(mockResponse); + + const { result, waitForNextUpdate } = renderHook(() => useXpertResultsWithThreshold()); + + act(() => { + result.current.getXpertResultsWithThreshold('task-id', 0.5); + }); + + expect(result.current.loading).toBe(true); + + await waitForNextUpdate(); + + expect(result.current.loading).toBe(false); + expect(result.current.error).toBe(null); + expect(result.current.xpertResultsData).toEqual(mockResponse.data.result); + }); + + it('should handle errors correctly', async () => { + const mockError = new Error('Network Error'); + EnterpriseCatalogAiCurationApiService.getXpertResults.mockRejectedValue(mockError); + + const { result, waitForNextUpdate } = renderHook(() => useXpertResultsWithThreshold()); + + act(() => { + result.current.getXpertResultsWithThreshold('task-id', 0.5); + }); + + expect(result.current.loading).toBe(true); + + await waitForNextUpdate(); + + expect(result.current.loading).toBe(false); + expect(result.current.error).toBe(mockError); + expect(result.current.xpertResultsData).toEqual({}); + }); + + it('should handle HTTP errors correctly', async () => { + const mockResponse = { + status: 500, + data: { error: 'Internal Server Error' }, + }; + EnterpriseCatalogAiCurationApiService.getXpertResults.mockResolvedValue(mockResponse); + + const { result, waitForNextUpdate } = renderHook(() => useXpertResultsWithThreshold()); + + act(() => { + result.current.getXpertResultsWithThreshold('task-id', 0.5); + }); + + expect(result.current.loading).toBe(true); + + await waitForNextUpdate(); + + expect(result.current.loading).toBe(false); + expect(result.current.error).toBe(mockResponse.data.error); + expect(result.current.xpertResultsData).toEqual({}); + }); +}); diff --git a/src/components/aiCuration/xpertResultCard/XpertResultCard.jsx b/src/components/aiCuration/xpertResultCard/XpertResultCard.jsx index 858748b3..443ec1a3 100644 --- a/src/components/aiCuration/xpertResultCard/XpertResultCard.jsx +++ b/src/components/aiCuration/xpertResultCard/XpertResultCard.jsx @@ -164,6 +164,7 @@ const XpertResultCard = ({ aria-valuemin="0" aria-valuemax="0.8" className="m-0" + aria-label="Xpert result card slider" /> @@ -174,6 +175,7 @@ const XpertResultCard = ({