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);
+ });
+ });
});