From 069d934432127922a48426dd3287593cd8cdbe78 Mon Sep 17 00:00:00 2001 From: Peter Kulko Date: Mon, 7 Oct 2024 20:33:43 +0300 Subject: [PATCH] refactor: added tests --- .../ChecklistSection/ChecklistItemBody.jsx | 4 +- .../ChecklistSection.test.jsx | 299 +++++------------- src/course-outline/status-bar/StatusBar.jsx | 2 +- src/generic/help-sidebar/HelpSidebar.jsx | 2 +- src/generic/help-sidebar/HelpSidebarLink.jsx | 4 +- .../help-sidebar/HelpSidebarLink.test.jsx | 46 +++ src/header/Header.tsx | 2 +- src/header/hooks.js | 2 - src/hooks.test.ts | 119 +++++++ .../PagesAndResources.test.jsx | 1 - .../pages/PageSettingButton.jsx | 6 +- .../pages/PageSettingButton.test.jsx | 80 +++++ src/utils.test.js | 119 ++++++- 13 files changed, 453 insertions(+), 233 deletions(-) create mode 100644 src/generic/help-sidebar/HelpSidebarLink.test.jsx create mode 100644 src/hooks.test.ts create mode 100644 src/pages-and-resources/pages/PageSettingButton.test.jsx diff --git a/src/course-checklist/ChecklistSection/ChecklistItemBody.jsx b/src/course-checklist/ChecklistSection/ChecklistItemBody.jsx index 1d523416e4..96a143e80c 100644 --- a/src/course-checklist/ChecklistSection/ChecklistItemBody.jsx +++ b/src/course-checklist/ChecklistSection/ChecklistItemBody.jsx @@ -21,13 +21,13 @@ const ChecklistItemBody = ({ // injected intl, }) => { - const { waffleFlags } = useSelector(getStudioHomeData); + const studioHomeData = useSelector(getStudioHomeData); const navigate = useNavigate(); const handleClick = (e, url) => { e.preventDefault(); - if (waffleFlags?.ENABLE_NEW_COURSE_UPDATES_PAGE) { + if (studioHomeData?.waffleFlags?.ENABLE_NEW_COURSE_UPDATES_PAGE) { navigate(`/course/${courseId}/course_info`); } else { window.location.href = url; diff --git a/src/course-checklist/ChecklistSection/ChecklistSection.test.jsx b/src/course-checklist/ChecklistSection/ChecklistSection.test.jsx index 1c8317c903..61a87d60a8 100644 --- a/src/course-checklist/ChecklistSection/ChecklistSection.test.jsx +++ b/src/course-checklist/ChecklistSection/ChecklistSection.test.jsx @@ -1,255 +1,118 @@ -/* eslint-disable */ -import { - render, - within, - screen, -} from '@testing-library/react'; -import '@testing-library/jest-dom'; -import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform'; -import { AppProvider } from '@edx/frontend-platform/react'; +import { useNavigate } from 'react-router'; +import { useSelector } from 'react-redux'; import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; -import initializeStore from '../../store'; -import { initialState,generateCourseLaunchData } from '../factories/mockApiResponses'; +import ChecklistItemBody from './ChecklistItemBody'; import messages from './messages'; -import ChecklistSection from './index'; -import { checklistItems } from './utils/courseChecklistData'; -import getUpdateLinks from '../utils'; -const testData = camelCaseObject(generateCourseLaunchData()); +jest.mock('react-redux', () => ({ + useSelector: jest.fn(), +})); +jest.mock('react-router', () => ({ + useNavigate: jest.fn(), +})); + +const mockNavigate = jest.fn(); +useNavigate.mockReturnValue(mockNavigate); const defaultProps = { - data: testData, - dataHeading: 'Test checklist', - idPrefix: 'launchChecklist', - updateLinks: getUpdateLinks('courseId'), - isLoading: false, + courseId: 'course-v1:edX+DemoX+2024', + checkId: 'welcomeMessage', + isCompleted: false, + updateLink: 'https://example.com/update', + intl: { + formatMessage: jest.fn(({ defaultMessage }) => defaultMessage), + }, }; -const testChecklistData = checklistItems[defaultProps.idPrefix]; - -const completedItemIds = ['welcomeMessage', 'courseDates'] - -const renderComponent = (props) => { - render( - - - - - , - ); +const waffleFlags = { + ENABLE_NEW_COURSE_UPDATES_PAGE: false, }; -let store; - -describe('ChecklistSection', () => { +describe('ChecklistItemBody', () => { beforeEach(() => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: false, - roles: [], - }, - }); - store = initializeStore(initialState); - }); - - it('a heading using the dataHeading prop', () => { - renderComponent(defaultProps); - - expect(screen.getByText(defaultProps.dataHeading)).toBeVisible(); + useSelector.mockReturnValue({ waffleFlags }); }); - it('completion count text', () => { - renderComponent(defaultProps); - const completionText = `${completedItemIds.length}/6 completed`; - expect(screen.getByTestId('completion-subheader').textContent).toEqual(completionText); + afterEach(() => { + jest.clearAllMocks(); }); - it('a loading spinner when isLoading prop is true', () => { - renderComponent({ ...defaultProps, isLoading: true }); - - const completionSubheader = screen.queryByTestId('completion-subheader'); - expect(completionSubheader).toBeNull(); + it('renders uncompleted icon when isCompleted is false', () => { + render( + + + , + ); - const loadingSpinner = screen.getByTestId('loading-spinner'); - expect(loadingSpinner).toBeVisible(); + const uncompletedIcon = screen.getByTestId('uncompleted-icon'); + expect(uncompletedIcon).toBeInTheDocument(); }); - it('the correct number of checks', () => { - renderComponent(defaultProps); + it('renders completed icon when isCompleted is true', () => { + render( + + + , + ); - const listItems = screen.getAllByTestId('checklist-item', { exact: false }); - expect(listItems).toHaveLength(6); + const completedIcon = screen.getByTestId('completed-icon'); + expect(completedIcon).toBeInTheDocument(); }); - it('welcomeMessage comment section should be null', () => { - renderComponent(defaultProps); - - const comment = screen.getByTestId('comment-section-welcomeMessage'); - expect(comment.children).toHaveLength(0); - }); + it('renders short and long descriptions based on checkId', () => { + render( + + + , + ); - it('certificate comment section should be null', () => { - renderComponent(defaultProps); + const shortDescription = screen.getByText(messages.welcomeMessageShortDescription.defaultMessage); + const longDescription = screen.getByText(messages.welcomeMessageLongDescription.defaultMessage); - const comment = screen.getByTestId('comment-section-certificate'); - expect(comment.children).toHaveLength(0); + expect(shortDescription).toBeInTheDocument(); + expect(longDescription).toBeInTheDocument(); }); - it('courseDates comment section should be null', () => { - renderComponent(defaultProps); + it('renders update hyperlink when updateLink is provided', () => { + render( + + + , + ); - const comment = screen.getByTestId('comment-section-courseDates'); - expect(comment.children).toHaveLength(0); + const updateLink = screen.getByTestId('update-hyperlink'); + expect(updateLink).toBeInTheDocument(); }); - it('proctoringEmail comment section should be null', () => { - renderComponent(defaultProps); + it('navigates to internal course page if ENABLE_NEW_COURSE_UPDATES_PAGE flag is enabled', () => { + useSelector.mockReturnValue({ waffleFlags: { ENABLE_NEW_COURSE_UPDATES_PAGE: true } }); - const comment = screen.getByTestId('comment-section-proctoringEmail'); - expect(comment.children).toHaveLength(0); - }); - - describe('gradingPolicy comment section', () => { - it('should be null if sum of weights is equal to 1', () => { - const props = { - ...defaultProps, - data: { - ...defaultProps.data, - grades: { - ...defaultProps.data.grades, - sumOfWeights: 1, - } - }, - }; - renderComponent(props); - - const comment = screen.getByTestId('comment-section-gradingPolicy'); - expect(comment.children).toHaveLength(0); - }); - - it('should have comment section', () => { - renderComponent(defaultProps); - - const comment = screen.getByTestId('comment-section-gradingPolicy'); - expect(comment.children).toHaveLength(1); - - expect(screen.getByText( - 'Your current grading policy adds up to', - { exact: false }, - )).toBeVisible(); - }); - }); - - describe('assignmentDeadlines comment section', () => { - it('should be null if assignments with dates before start and after end are empty', () => { - const props = { - ...defaultProps, - data: { - ...defaultProps.data, - assignments: { - ...defaultProps.data.assignments, - assignmentsWithDatesAfterEnd: [], - assignmentsWithOraDatesBeforeStart: [], - } - }, - }; - renderComponent(props); - - const comment = screen.getByTestId('comment-section-assignmentDeadlines'); - expect(comment.children).toHaveLength(0); - }); + render( + + + , + ); - it('should have comment section', () => { - renderComponent(defaultProps); + const updateLink = screen.getByTestId('update-hyperlink'); + fireEvent.click(updateLink); - const comment = screen.getByTestId('comment-section-assignmentDeadlines'); - const assigmentLinks = within(comment).getAllByRole('link'); - - expect(comment.children).toHaveLength(1); - - expect(screen.getByText( - messages.assignmentDeadlinesComment.defaultMessage, - { exact: false }, - )).toBeVisible(); + expect(mockNavigate).toHaveBeenCalledWith(`/course/${defaultProps.courseId}/course_info`); + }); - expect(assigmentLinks).toHaveLength(2); + it('redirects to external link if ENABLE_NEW_COURSE_UPDATES_PAGE flag is disabled', () => { + render( + + + , + ); - expect(assigmentLinks[0].textContent).toEqual('Subsection'); + const updateLink = screen.getByTestId('update-hyperlink'); + fireEvent.click(updateLink); - expect(assigmentLinks[1].textContent).toEqual('ORA subsection'); - }); - }); -}); - -testChecklistData.forEach((check) => { - describe(`check with id '${check.id}'`, () => { - let checkItem; - beforeEach(() => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: false, - roles: [], - }, - }); - store = initializeStore(initialState); - renderComponent(defaultProps); - checkItem = screen.getAllByTestId(`checklist-item-${check.id}`); - }); - - it('renders', () => { - expect(checkItem).toHaveLength(1); - }); - - it('has correct icon', () => { - const icon = screen.getAllByTestId(`icon-${check.id}`) - - expect(icon).toHaveLength(1); - - const { queryByTestId } = within(icon[0]); - if (completedItemIds.includes(check.id)) { - expect(queryByTestId('completed-icon')).not.toBeNull(); - } else { - expect(queryByTestId('uncompleted-icon')).not.toBeNull(); - } - }); - - it('has correct short description', () => { - const { getByText } = within(checkItem[0]); - const shortDescription = messages[`${check.id}ShortDescription`].defaultMessage; - expect(getByText(shortDescription)).toBeVisible(); - }); - - it('has correct long description', () => { - const { getByText } = within(checkItem[0]); - const longDescription = messages[`${check.id}LongDescription`].defaultMessage; - expect(getByText(longDescription)).toBeVisible(); - }); - - describe('has correct link', () => { - const links = getUpdateLinks('courseId') - const shouldShowLink = Object.keys(links).includes(check.id); - - if (shouldShowLink) { - it('with a Hyperlink', () => { - const { getByRole, getByText } = within(checkItem[0]); - - expect(getByText('Update')).toBeVisible(); - - expect(getByRole('link').href).toMatch(links[check.id]); - }); - } else { - it('without a Hyperlink', () => { - const { queryByText } = within(checkItem[0]); - - expect(queryByText('Update')).toBeNull(); - }); - } - }); + expect(window.location.href).toBe('http://localhost/'); }); }); diff --git a/src/course-outline/status-bar/StatusBar.jsx b/src/course-outline/status-bar/StatusBar.jsx index cfae93734e..9d671530e1 100644 --- a/src/course-outline/status-bar/StatusBar.jsx +++ b/src/course-outline/status-bar/StatusBar.jsx @@ -9,13 +9,13 @@ import { import { Link } from 'react-router-dom'; import { AppContext } from '@edx/frontend-platform/react'; import { useSelector } from 'react-redux'; -import { getStudioHomeData } from '../../studio-home/data/selectors'; import { ContentTagsDrawerSheet } from '../../content-tags-drawer'; import TagCount from '../../generic/tag-count'; import { useHelpUrls } from '../../help-urls/hooks'; import { VIDEO_SHARING_OPTIONS } from '../constants'; import { useContentTagsCount } from '../../generic/data/apiHooks'; +import { getStudioHomeData } from '../../studio-home/data/selectors'; import messages from './messages'; import { getVideoSharingOptionText } from '../utils'; diff --git a/src/generic/help-sidebar/HelpSidebar.jsx b/src/generic/help-sidebar/HelpSidebar.jsx index a4782f9161..9458e6519f 100644 --- a/src/generic/help-sidebar/HelpSidebar.jsx +++ b/src/generic/help-sidebar/HelpSidebar.jsx @@ -4,8 +4,8 @@ import { useLocation } from 'react-router-dom'; import classNames from 'classnames'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { getConfig } from '@edx/frontend-platform'; -import { getStudioHomeData } from '../../studio-home/data/selectors'; +import { getStudioHomeData } from '../../studio-home/data/selectors'; import { otherLinkURLParams } from './constants'; import messages from './messages'; import HelpSidebarLink from './HelpSidebarLink'; diff --git a/src/generic/help-sidebar/HelpSidebarLink.jsx b/src/generic/help-sidebar/HelpSidebarLink.jsx index 3a3bb07238..b7e7fe5ab8 100644 --- a/src/generic/help-sidebar/HelpSidebarLink.jsx +++ b/src/generic/help-sidebar/HelpSidebarLink.jsx @@ -9,9 +9,7 @@ const HelpSidebarLink = ({ if (isNewPage) { return ( - + {title} diff --git a/src/generic/help-sidebar/HelpSidebarLink.test.jsx b/src/generic/help-sidebar/HelpSidebarLink.test.jsx new file mode 100644 index 0000000000..94d2030605 --- /dev/null +++ b/src/generic/help-sidebar/HelpSidebarLink.test.jsx @@ -0,0 +1,46 @@ +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { BrowserRouter as Router } from 'react-router-dom'; + +import HelpSidebarLink from './HelpSidebarLink'; + +describe('HelpSidebarLink Component', () => { + const defaultProps = { + isNewPage: true, + pathToPage: '/test-page', + title: 'Test Title', + as: 'li', + }; + + it('renders a React Router Link when isNewPage is true', () => { + const { getByText } = render( + + + , + ); + + const linkElement = getByText('Test Title'); + expect(linkElement.closest('a')).toHaveAttribute('href', '/test-page'); + }); + + it('renders a Hyperlink when isNewPage is false', () => { + const props = { ...defaultProps, isNewPage: false, pathToPage: 'https://example.com' }; + const { getByText } = render(); + + const hyperlinkElement = getByText('Test Title'); + expect(hyperlinkElement.closest('a')).toHaveAttribute('href', 'https://example.com'); + expect(hyperlinkElement.closest('a')).toHaveAttribute('target', '_blank'); + }); + + it('renders the correct tag element specified by "as" prop', () => { + const props = { ...defaultProps, as: 'div' }; + const { container } = render( + + + , + ); + + const tagElement = container.querySelector('div.sidebar-link'); + expect(tagElement).toBeInTheDocument(); + }); +}); diff --git a/src/header/Header.tsx b/src/header/Header.tsx index d99e219059..eeae921704 100644 --- a/src/header/Header.tsx +++ b/src/header/Header.tsx @@ -4,8 +4,8 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { StudioHeader } from '@edx/frontend-component-header'; import { type Container, useToggle } from '@openedx/paragon'; import { generatePath, useHref, useNavigate } from 'react-router-dom'; -import { getStudioHomeData } from '../studio-home/data/selectors'; +import { getStudioHomeData } from '../studio-home/data/selectors'; import { SearchModal } from '../search-modal'; import { useContentMenuItems, useSettingMenuItems, useToolsMenuItems } from './hooks'; import messages from './messages'; diff --git a/src/header/hooks.js b/src/header/hooks.js index 9c794541c1..13bbf57bc0 100644 --- a/src/header/hooks.js +++ b/src/header/hooks.js @@ -10,8 +10,6 @@ export const useContentMenuItems = courseId => { const studioBaseUrl = getConfig().STUDIO_BASE_URL; const { waffleFlags } = useSelector(getStudioHomeData); - // console.log('================= waffleFlags', waffleFlags); - const items = [ { href: waffleFlags?.ENABLE_NEW_COURSE_OUTLINE_PAGE ? `/course/${courseId}` : `${studioBaseUrl}/course/${courseId}`, diff --git a/src/hooks.test.ts b/src/hooks.test.ts new file mode 100644 index 0000000000..06ad2f8a35 --- /dev/null +++ b/src/hooks.test.ts @@ -0,0 +1,119 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { fireEvent } from '@testing-library/react'; +import { useLocation } from 'react-router-dom'; +import { history } from '@edx/frontend-platform'; + +import { useScrollToHashElement, useEscapeClick, useLoadOnScroll } from './hooks'; + +jest.mock('react-router-dom', () => ({ + useLocation: jest.fn(), +})); + +jest.mock('@edx/frontend-platform', () => ({ + history: { + replace: jest.fn(), + }, +})); + +describe('Custom Hooks', () => { + describe('useScrollToHashElement', () => { + beforeEach(() => { + window.location.hash = '#test'; + document.body.innerHTML = '
Test Element
'; + + Element.prototype.scrollIntoView = jest.fn(); + + jest.mocked(useLocation).mockReturnValue({ + pathname: '/test', + state: null, + search: '', + hash: '', + key: 'default', + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + window.location.hash = ''; + }); + + it('scrolls to element with the hash and replaces the URL hash', () => { + renderHook(() => useScrollToHashElement({ isLoading: false })); + const element = document.getElementById('test'); + + expect(element).toBeInTheDocument(); + expect(element?.scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth' }); + expect(history.replace).toHaveBeenCalledWith({ pathname: '/test', hash: '' }); + }); + }); + + describe('useEscapeClick', () => { + it('calls onEscape when Escape key is pressed', () => { + const onEscape = jest.fn(); + + renderHook(() => useEscapeClick({ onEscape, dependency: [] })); + + fireEvent.keyDown(window, { key: 'Escape' }); + + expect(onEscape).toHaveBeenCalledTimes(1); + }); + + it('does not call onEscape for other keys', () => { + const onEscape = jest.fn(); + + renderHook(() => useEscapeClick({ onEscape, dependency: [] })); + + fireEvent.keyDown(window, { key: 'Enter' }); + + expect(onEscape).not.toHaveBeenCalled(); + }); + }); + + describe('useLoadOnScroll', () => { + const fetchNextPage = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('calls fetchNextPage when scrolled near the bottom', () => { + renderHook(() => useLoadOnScroll(true, false, fetchNextPage, true)); + + Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: 1000 }); + Object.defineProperty(document.body, 'scrollHeight', { writable: true, configurable: true, value: 1500 }); + window.scrollY = 1200; + + fireEvent.scroll(window); + + expect(fetchNextPage).toHaveBeenCalledTimes(1); + }); + + it('does not call fetchNextPage if not near the bottom', () => { + renderHook(() => useLoadOnScroll(true, false, fetchNextPage, true)); + + Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: 1000 }); + Object.defineProperty(document.body, 'scrollHeight', { writable: true, configurable: true, value: 2000 }); + window.scrollY = 500; + + fireEvent.scroll(window); + + expect(fetchNextPage).not.toHaveBeenCalled(); + }); + + it('does not call fetchNextPage if fetching is in progress', () => { + renderHook(() => useLoadOnScroll(true, true, fetchNextPage, true)); + + fireEvent.scroll(window); + + expect(fetchNextPage).not.toHaveBeenCalled(); + }); + + it('does not call fetchNextPage if hasNextPage is false', () => { + renderHook(() => useLoadOnScroll(false, false, fetchNextPage, true)); + + fireEvent.scroll(window); + + expect(fetchNextPage).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/pages-and-resources/PagesAndResources.test.jsx b/src/pages-and-resources/PagesAndResources.test.jsx index 68b9f8cb1f..180c3f8674 100644 --- a/src/pages-and-resources/PagesAndResources.test.jsx +++ b/src/pages-and-resources/PagesAndResources.test.jsx @@ -27,7 +27,6 @@ describe('PagesAndResources', () => { expect(screen.queryByRole('heading', { name: 'Content permissions' })).not.toBeInTheDocument(); }); - it('show content permissions section if Learning Assistant app is enabled', async () => { const initialState = { models: { diff --git a/src/pages-and-resources/pages/PageSettingButton.jsx b/src/pages-and-resources/pages/PageSettingButton.jsx index 63f6f8521e..4e3b6ba2eb 100644 --- a/src/pages-and-resources/pages/PageSettingButton.jsx +++ b/src/pages-and-resources/pages/PageSettingButton.jsx @@ -22,7 +22,7 @@ const PageSettingButton = ({ const navigate = useNavigate(); const studioHomeData = useSelector(getStudioHomeData); - const linkTo = useMemo(() => { + const determineLinkDestination = useMemo(() => { if (!legacyLink) { return null; } if (legacyLink.includes('textbooks')) { @@ -42,9 +42,9 @@ const PageSettingButton = ({ const canConfigureOrEnable = allowedOperations?.configure || allowedOperations?.enable; - if (linkTo) { + if (determineLinkDestination) { return ( - + { + render( + + + + + + + , + ); +}; + +describe('PageSettingButton', () => { + let store; + + beforeEach(() => { + store = mockStore({ + studioHomeData: { + waffleFlags: { + ENABLE_NEW_TEXTBOOKS_PAGE: true, + ENABLE_NEW_CUSTOM_PAGES: true, + }, + }, + }); + }); + + it('renders a link when legacyLink is provided and matches textbooks condition', () => { + renderComponent({ + id: 'page_1', + courseId: 'course_1', + legacyLink: 'textbooks', + allowedOperations: {}, + }, store); + + expect(screen.getByRole('link')).toHaveAttribute('href', '/textbooks'); + }); + + it('renders a link when legacyLink is provided and matches tabs condition', () => { + renderComponent({ + id: 'page_2', + courseId: 'course_2', + legacyLink: 'tabs', + allowedOperations: {}, + }, store); + + expect(screen.getByRole('link')).toHaveAttribute('href', '/tabs'); + }); + + it('renders an IconButton when allowedOperations allows configuration or enabling', () => { + renderComponent({ + id: 'page_3', + courseId: 'course_3', + allowedOperations: { configure: true }, + }, store); + + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + it('does not render anything when neither legacyLink nor allowedOperations allows configuration or enabling', () => { + renderComponent({ + id: 'page_4', + courseId: 'course_4', + allowedOperations: {}, + }, store); + + expect(screen.queryByRole('link')).not.toBeInTheDocument(); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); +}); diff --git a/src/utils.test.js b/src/utils.test.js index 05e07ddc1a..31d895f219 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -1,6 +1,17 @@ import { getConfig, getPath } from '@edx/frontend-platform'; -import { getFileSizeToClosestByte, createCorrectInternalRoute } from './utils'; +import { + getFileSizeToClosestByte, + createCorrectInternalRoute, + convertObjectToSnakeCase, + deepConvertingKeysToCamelCase, + deepConvertingKeysToSnakeCase, + transformKeysToCamelCase, + parseArrayOrObjectValues, + convertToDateFromString, + convertToStringFromDate, + isValidDate, +} from './utils'; jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn(), @@ -41,6 +52,7 @@ describe('FilesAndUploads utils', () => { expect(expectedSize).toEqual(actualSize); }); }); + describe('createCorrectInternalRoute', () => { beforeEach(() => { getConfig.mockReset(); @@ -77,4 +89,109 @@ describe('FilesAndUploads utils', () => { expect(result).toBe('/course-authoring/some/path'); }); }); + + describe('convertObjectToSnakeCase', () => { + it('converts object keys to snake_case', () => { + const input = { firstName: 'John', lastName: 'Doe' }; + const expectedOutput = { first_name: { value: 'John' }, last_name: { value: 'Doe' } }; + expect(convertObjectToSnakeCase(input)).toEqual(expectedOutput); + }); + + it('converts object keys to snake_case with unpacked values', () => { + const input = { firstName: 'John', lastName: 'Doe' }; + const expectedOutput = { first_name: 'John', last_name: 'Doe' }; + expect(convertObjectToSnakeCase(input, true)).toEqual(expectedOutput); + }); + }); + + describe('deepConvertingKeysToCamelCase', () => { + it('converts object keys to camelCase', () => { + const input = { first_name: 'John', last_name: 'Doe' }; + const expectedOutput = { firstName: 'John', lastName: 'Doe' }; + expect(deepConvertingKeysToCamelCase(input)).toEqual(expectedOutput); + }); + + it('converts nested object keys to camelCase', () => { + const input = { user_info: { first_name: 'John', last_name: 'Doe' } }; + const expectedOutput = { userInfo: { firstName: 'John', lastName: 'Doe' } }; + expect(deepConvertingKeysToCamelCase(input)).toEqual(expectedOutput); + }); + }); + + describe('deepConvertingKeysToSnakeCase', () => { + it('converts object keys to snake_case', () => { + const input = { firstName: 'John', lastName: 'Doe' }; + const expectedOutput = { first_name: 'John', last_name: 'Doe' }; + expect(deepConvertingKeysToSnakeCase(input)).toEqual(expectedOutput); + }); + + it('converts nested object keys to snake_case', () => { + const input = { userInfo: { firstName: 'John', lastName: 'Doe' } }; + const expectedOutput = { user_info: { first_name: 'John', last_name: 'Doe' } }; + expect(deepConvertingKeysToSnakeCase(input)).toEqual(expectedOutput); + }); + }); + + describe('transformKeysToCamelCase', () => { + it('transforms a single key to camelCase', () => { + const input = { key: 'first_name' }; + const expectedOutput = 'firstName'; + expect(transformKeysToCamelCase(input)).toEqual(expectedOutput); + }); + }); + + describe('parseArrayOrObjectValues', () => { + it('parses stringified JSON values', () => { + const input = { key1: '123', key2: '{"name":"John"}' }; + const expectedOutput = { key1: '123', key2: { name: 'John' } }; + expect(parseArrayOrObjectValues(input)).toEqual(expectedOutput); + }); + + it('returns non-JSON values as is', () => { + const input = { key1: '123', key2: 'John' }; + const expectedOutput = { key1: '123', key2: 'John' }; + expect(parseArrayOrObjectValues(input)).toEqual(expectedOutput); + }); + }); + + describe('convertToDateFromString', () => { + it('converts a date string to a Date object', () => { + const dateStr = '2023-10-01T12:00:00Z'; + const date = convertToDateFromString(dateStr); + expect(date).toBeInstanceOf(Date); + expect(date.toISOString()).toBe('2023-10-01T12:00:00.000Z'); + }); + + it('returns an empty string for invalid date strings', () => { + const dateStr = ''; + const date = convertToDateFromString(dateStr); + expect(date).toBe(''); + }); + }); + + describe('convertToStringFromDate', () => { + it('converts a Date object to a date string', () => { + const date = new Date('2023-10-01T12:00:00Z'); + const dateStr = convertToStringFromDate(date); + expect(dateStr).toBe('2023-10-01T12:00:00Z'); + }); + + it('returns an empty string for invalid Date objects', () => { + const date = null; + const dateStr = convertToStringFromDate(date); + expect(dateStr).toBe(''); + }); + }); + + describe('isValidDate', () => { + it('returns true for valid dates', () => { + const date = new Date('2023-10-01T12:00:00Z'); + expect(isValidDate(date)).toBe(true); + }); + + it('returns false for invalid dates', () => { + const date = new Date('invalid-date'); + expect(isValidDate(date)).toBe(false); + }); + }); });