Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,4 @@ const config = createConfig('jest', {

// delete config.testURL;

config.reporters = [...(config.reporters || []), ["jest-console-group-reporter", {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some context as to why we are removing this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After regenerating package-lock.json, the dependency updates caused the jest-console-group-reporter to fail with a TypeError: Cannot read properties of undefined (reading 'isSet') during the test run.
Since this is just a reporter utility for grouping console logs and its failure was blocking the test suite execution, I removed it to restore stability to the CI pipeline.

// change this setting if need to see less details for each test
// reportType: "summary" | "details",
// enable: true | false,
afterEachTest: {
enable: true,
filePaths: false,
reportType: "details",
},
afterAllTests: {
reportType: "summary",
enable: true,
filePaths: true,
},
}]];

module.exports = config;
21,838 changes: 12,642 additions & 9,196 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { breakpoints } from '@openedx/paragon';

export const DECODE_ROUTES = {
ACCESS_DENIED: '/course/:courseId/access-denied',
HOME: '/course/:courseId/home',
Expand Down Expand Up @@ -72,3 +74,8 @@ export const LOADED = 'loaded';
export const FAILED = 'failed';
export const DENIED = 'denied';
export type StatusValue = typeof LOADING | typeof LOADED | typeof FAILED | typeof DENIED;

export const BREAKPOINTS = {
LARGE: breakpoints.large.minWidth ?? 992,
MEDIUM: breakpoints.medium.minWidth ?? 768,
} as const;
25 changes: 12 additions & 13 deletions src/courseware/course/Course.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ describe('Course', () => {
it('handles click to open/close discussions sidebar', async () => {
await setupDiscussionSidebar();

waitFor(() => {
await waitFor(() => {
expect(screen.getByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument();
expect(screen.getByTestId('sidebar-DISCUSSIONS')).not.toHaveClass('d-none');

Expand Down Expand Up @@ -191,22 +191,22 @@ describe('Course', () => {
const { rerender } = render(<Course {...testData} />, { store: testStore });
loadUnit();

waitFor(() => {
expect(screen.findByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument();
expect(screen.findByTestId('sidebar-DISCUSSIONS')).not.toHaveClass('d-none');
await waitFor(() => {
expect(screen.getByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument();
expect(screen.getByTestId('sidebar-DISCUSSIONS')).not.toHaveClass('d-none');
});

rerender(null);
});

it('handles click to open/close notification tray', async () => {
await setupDiscussionSidebar();
waitFor(() => {
const notificationShowButton = screen.findByRole('button', { name: /Show notification tray/i });
await waitFor(() => {
const notificationShowButton = screen.getByRole('button', { name: /Show notification tray/i });
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toBeInTheDocument();
fireEvent.click(notificationShowButton);
expect(screen.queryByRole('region', { name: /notification tray/i })).toBeInTheDocument();
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toHaveClass('d-none');
expect(screen.getByRole('region', { name: /notification tray/i })).toBeInTheDocument();
expect(screen.getByRole('region', { name: /notification tray/i })).not.toHaveClass('d-none');
});
});

Expand Down Expand Up @@ -296,7 +296,7 @@ describe('Course', () => {
sequenceId: sequenceBlocks[0].id,
};
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
waitFor(() => expect(screen.findByText('Some random banner text to display.')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('Some random banner text to display.')).toBeInTheDocument());
});

it('renders Entrance Exam alert with passing score', async () => {
Expand Down Expand Up @@ -330,7 +330,7 @@ describe('Course', () => {
sequenceId: sequenceBlocks[0].id,
};
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
waitFor(() => expect(screen.findByText('Your score is 100%. You have passed the entrance exam.')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('Your score is 100%. You have passed the entrance exam.')).toBeInTheDocument());
});

it('renders Entrance Exam alert with non-passing score', async () => {
Expand Down Expand Up @@ -364,7 +364,7 @@ describe('Course', () => {
sequenceId: sequenceBlocks[0].id,
};
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
waitFor(() => expect(screen.findByText('To access course materials, you must score 70% or higher on this exam. Your current score is 30%.')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('To access course materials, you must score 70% or higher on this exam. Your current score is 30%.')).toBeInTheDocument());
});
});

Expand All @@ -382,8 +382,7 @@ describe('Course', () => {
unitId: Object.values(models.units)[0].id,
};
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
const learnerTools = screen.queryByTestId(mockLearnerToolsTestId);
await waitFor(() => expect(learnerTools).toBeInTheDocument());
await waitFor(() => expect(screen.queryByTestId(mockLearnerToolsTestId)).toBeInTheDocument());
});

it('does not display learner tools when screen is too narrow (mobile)', async () => {
Expand Down
6 changes: 3 additions & 3 deletions src/courseware/course/new-sidebar/SidebarContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { breakpoints, useWindowSize } from '@openedx/paragon';

import { getLocalStorage, setLocalStorage } from '../../../data/localStorage';
import { useModel } from '../../../generic/model-store';
import { WIDGETS } from '../../../constants';
import { WIDGETS, BREAKPOINTS } from '../../../constants';
import SidebarContext from './SidebarContext';
import { SIDEBARS } from './sidebars';

Expand All @@ -26,8 +26,8 @@ const SidebarProvider: React.FC<Props> = ({
const { verifiedMode } = useModel('courseHomeMeta', courseId);
const topic = useModel('discussionTopics', unitId);
const windowWidth = useWindowSize().width ?? window.innerWidth;
const shouldDisplayFullScreen = windowWidth < breakpoints.large.minWidth;
const shouldDisplaySidebarOpen = windowWidth > breakpoints.medium.minWidth;
const shouldDisplayFullScreen = windowWidth < (breakpoints.large.minWidth ?? BREAKPOINTS.LARGE);
const shouldDisplaySidebarOpen = windowWidth > (breakpoints.medium.minWidth ?? BREAKPOINTS.MEDIUM);
const query = new URLSearchParams(window.location.search);
const isInitiallySidebarOpen = shouldDisplaySidebarOpen || query.get('sidebar') === 'true';
const sidebarKey = `sidebar.${courseId}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { breakpoints } from '@openedx/paragon';

import { BREAKPOINTS } from '@src/constants';
import {
initializeMockApp, render, screen, act, fireEvent, waitFor,
} from '../../../../../../setupTest';
Expand Down Expand Up @@ -44,7 +45,7 @@ describe('NotificationsWidget', () => {
}

beforeEach(async () => {
global.innerWidth = breakpoints.large.minWidth;
global.innerWidth = breakpoints.large.minWidth ?? BREAKPOINTS.LARGE;
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
Expand Down
51 changes: 51 additions & 0 deletions src/courseware/course/sequence/Unit/ContentIFrame.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,57 @@ jest.mock('@edx/frontend-platform/react', () => ({ ErrorPage: () => <div>ErrorPa

jest.mock('@src/generic/PageLoading', () => jest.fn(() => <div>PageLoading</div>));

jest.mock('@openedx/paragon', () => {
const actual = jest.requireActual('@openedx/paragon');
const PropTypes = jest.requireActual('prop-types');

const MockModalDialog = ({ children, isOpen, onClose }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[important]: Is it possible to abandon the mock of this Paragon component?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this mock resolve third issue about Paragon modal issues in tests in this ticket

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this mock simulates the behavior of a modal window with data-testid="modal-backdrop". However, is it now clear why we’re getting the error TestingLibraryElementError: Unable to find an element by: [data-testid="modal-backdrop"]?

if (!isOpen) { return null; }

return (
<div role="dialog" aria-modal="true" className="mock-modal">
<button
type="button"
data-testid="modal-backdrop"
onClick={onClose}
aria-label="Close"
>
</button>
<div className="mock-modal-content">
{children}
</div>
</div>
);
};

MockModalDialog.propTypes = {
children: PropTypes.node,
isOpen: PropTypes.bool,
onClose: PropTypes.func,
};

const createSubComponent = (baseClass) => {
const Component = ({ children, className }) => (
<div className={`${baseClass} ${className || ''}`}>{children}</div>
);
Component.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
};
return Component;
};

MockModalDialog.Body = createSubComponent('mock-modal-body');
MockModalDialog.Header = createSubComponent('mock-modal-header');
MockModalDialog.Footer = createSubComponent('mock-modal-footer');

return {
...actual,
ModalDialog: MockModalDialog,
};
});

jest.mock('./hooks', () => ({
useIFrameBehavior: jest.fn(),
useModalIFrameData: jest.fn(),
Expand Down
8 changes: 2 additions & 6 deletions src/plugin-slots/LearnerToolsSlot/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ import * as auth from '@edx/frontend-platform/auth';

import { LearnerToolsSlot } from './index';

jest.mock('@openedx/frontend-plugin-framework', () => ({
PluginSlot: jest.fn(() => <div data-testid="plugin-slot">Plugin Slot</div>),
}));

jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(),
}));
Expand Down Expand Up @@ -98,7 +94,7 @@ describe('LearnerToolsSlot', () => {

render(<LearnerToolsSlot {...defaultProps} />);

// The portal should render to document.body
expect(document.body.querySelector('[data-testid="plugin-slot"]')).toBeInTheDocument();
// The portal should render to document.body with the id as testid
expect(document.body.querySelector('[data-testid="org.openedx.frontend.learning.learner_tools.v1"]')).toBeInTheDocument();
});
});
17 changes: 17 additions & 0 deletions src/product-tours/ProductTours.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ import { buildOutlineFromBlocks } from '../courseware/data/__factories__/learnin

import { UserMessagesProvider } from '../generic/user-messages';
import { DECODE_ROUTES } from '../constants';
import {
DismissButtonFormattedMessage,
NextButtonFormattedMessage,
OkayButtonFormattedMessage,
} from './GenericTourFormattedMessages';

initializeMockApp();
jest.mock('@edx/frontend-platform/analytics');
Expand Down Expand Up @@ -158,6 +163,18 @@ describe('Course Home Tours', () => {
expect(await screen.queryByRole('dialog')).not.toBeInTheDocument();
},
);

describe('GenericTourFormattedMessages', () => {
it('renders all formatted message components to satisfy coverage', () => {
render(<DismissButtonFormattedMessage />);
render(<NextButtonFormattedMessage />);
render(<OkayButtonFormattedMessage />);

expect(screen.getByText('Dismiss')).toBeInTheDocument();
expect(screen.getByText('Next')).toBeInTheDocument();
expect(screen.getByText('Okay')).toBeInTheDocument();
});
});
});

jest.mock(
Expand Down
14 changes: 8 additions & 6 deletions src/setupTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ import { getCourseOutlineStructure } from './courseware/data/thunks';
import { appendBrowserTimezoneToUrl, executeThunk } from './utils';
import buildSimpleCourseAndSequenceMetadata from './courseware/data/__factories__/sequenceMetadata.factory';
import { buildOutlineFromBlocks } from './courseware/data/__factories__/learningSequencesOutline.factory';
import MockedPluginSlot from './tests/MockedPluginSlot';

jest.mock('@openedx/frontend-plugin-framework', () => ({
...jest.requireActual('@openedx/frontend-plugin-framework'),
Plugin: () => 'Plugin',
PluginSlot: MockedPluginSlot,
}));
jest.mock('@openedx/frontend-plugin-framework', () => {
const MockedPluginSlot = jest.requireActual('./tests/MockedPluginSlot').default;

return {
Plugin: () => 'Plugin',
PluginSlot: jest.fn(MockedPluginSlot),
};
});

jest.mock('@src/generic/plugin-store', () => ({
...jest.requireActual('@src/generic/plugin-store'),
Expand Down
4 changes: 2 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ export const executeThunk = async (thunk, dispatch, getState = undefined) => {
*/
export const appendBrowserTimezoneToUrl = (url: string) => {
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const urlObject = new URL(url);
const urlObject = new URL(url, url.startsWith('http') ? undefined : 'http://localhost');
if (browserTimezone) {
urlObject.searchParams.append('browser_timezone', browserTimezone);
}
return urlObject.href;
return url.startsWith('http') ? urlObject.href : `${urlObject.pathname}${urlObject.search}`;
};
9 changes: 9 additions & 0 deletions webpack.dev.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,13 @@ config.resolve.alias = {
'@src': path.resolve(__dirname, 'src'),
};

// Fix for react-focus-on webpack 5 compatibility issue
// The package has ES modules without file extensions in imports
config.module.rules.push({
test: /\.m?js$/,
resolve: {
fullySpecified: false,
},
});

module.exports = config;
9 changes: 9 additions & 0 deletions webpack.prod.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,13 @@ config.resolve.alias = {
'@src': path.resolve(__dirname, 'src'),
};

// Fix for react-focus-on webpack 5 compatibility issue
// The package has ES modules without file extensions in imports
config.module.rules.push({
test: /\.m?js$/,
resolve: {
fullySpecified: false,
},
});

module.exports = config;