Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: added test cases for new sidebar #1267

Merged
merged 3 commits into from
Jan 11, 2024
Merged
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
34 changes: 6 additions & 28 deletions src/courseware/course/Course.test.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import React from 'react';

import { Factory } from 'rosie';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import MockAdapter from 'axios-mock-adapter';

import { breakpoints } from '@edx/paragon';

import {
act, fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor,
} from '../../setupTest';
import { buildTopicsFromUnits } from '../data/__factories__/discussionTopics.factory';
import { handleNextSectionCelebration } from './celebration';
import * as celebrationUtils from './celebration/utils';
import { handleNextSectionCelebration } from './celebration';
import Course from './Course';
import { executeThunk } from '../../utils';
import * as thunks from '../data/thunks';
import setupDiscussionSidebar from './test-utils';

jest.mock('@edx/frontend-platform/analytics');
jest.mock('@edx/frontend-lib-special-exams/dist/data/thunks.js', () => ({
Expand Down Expand Up @@ -51,26 +49,6 @@ describe('Course', () => {
setItemSpy.mockRestore();
});

const setupDiscussionSidebar = async () => {
const courseHomeMetadata = Factory.build('courseHomeMetadata', { verified_mode: null });
const testStore = await initializeTestStore({ provider: 'openedx', courseHomeMetadata });
const state = testStore.getState();
const { courseware: { courseId } } = state;
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/${courseId}`).reply(200, { provider: 'openedx' });
const topicsResponse = buildTopicsFromUnits(state.models.units);
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v2/course_topics/${courseId}`)
.reply(200, topicsResponse);

await executeThunk(thunks.getCourseDiscussionTopics(courseId), testStore.dispatch);
const [firstUnitId] = Object.keys(state.models.units);
mockData.unitId = firstUnitId;
const [firstSequenceId] = Object.keys(state.models.sequences);
mockData.sequenceId = firstSequenceId;

await render(<Course {...mockData} />, { store: testStore, wrapWithRouter: true });
};

it('loads learning sequence', async () => {
render(<Course {...mockData} />, { wrapWithRouter: true });
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
Expand Down Expand Up @@ -183,7 +161,7 @@ describe('Course', () => {
});

it('handles click to open/close notification tray', async () => {
render(<Course {...mockData} />, { wrapWithRouter: true });
await setupDiscussionSidebar();
const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i });
expect(screen.queryByRole('region', { name: /notification tray/i })).toHaveClass('d-none');
fireEvent.click(notificationShowButton);
Expand Down
3 changes: 2 additions & 1 deletion src/courseware/course/new-sidebar/common/SidebarBase.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const SidebarBase = ({
};

SidebarBase.propTypes = {
title: PropTypes.string.isRequired,
title: PropTypes.string,
ariaLabel: PropTypes.string.isRequired,
sidebarId: PropTypes.string.isRequired,
className: PropTypes.string,
Expand All @@ -103,6 +103,7 @@ SidebarBase.propTypes = {
};

SidebarBase.defaultProps = {
title: '',
width: '50rem',
allowFullHeight: false,
showTitleBar: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { executeThunk } from '../../../../../../utils';
import { buildTopicsFromUnits } from '../../../../../data/__factories__/discussionTopics.factory';
import { getCourseDiscussionTopics } from '../../../../../data/thunks';
import SidebarContext from '../../../SidebarContext';
import DiscussionsNotificationsSidebar from '../DiscussionsNotificationsSidebar';
import DiscussionsWidget from './DiscussionsWidget';

initializeMockApp();
Expand Down Expand Up @@ -51,24 +52,29 @@ describe('DiscussionsWidget', () => {
await executeThunk(getCourseDiscussionTopics(courseId), store.dispatch);
});

function renderWithProvider(testData = {}) {
function renderWithProvider(Component, testData = {}) {
const { container } = render(
<SidebarContext.Provider value={{ ...mockData, ...testData }}>
<DiscussionsWidget />
<Component />
</SidebarContext.Provider>,
);
return container;
}

it('should show up if unit discussions associated with it', async () => {
renderWithProvider();
renderWithProvider(DiscussionsWidget);
expect(screen.queryByTitle('Discussions')).toBeInTheDocument();
expect(screen.queryByTitle('Discussions'))
.toHaveAttribute('src', `http://localhost:2002/${courseId}/category/${unitId}?inContextSidebar`);
});

it('should show nothing if unit has no discussions associated with it', async () => {
renderWithProvider({ isDiscussionbarAvailable: false });
renderWithProvider(DiscussionsWidget, { isDiscussionbarAvailable: false });
expect(screen.queryByTitle('Discussions')).not.toBeInTheDocument();
});

it('should display the Back to course button on small screens.', async () => {
renderWithProvider(DiscussionsNotificationsSidebar, { shouldDisplayFullScreen: true });
expect(screen.queryByText('Back to course')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const NotificationsWidget = () => {
if (hideNotificationbar || !isNotificationbarAvailable) { return null; }

return (
<div className="border border-light-400 rounded-sm">
<div className="border border-light-400 rounded-sm" data-testid="notification-widget">
<UpgradeNotification
offer={offer}
verifiedMode={verifiedMode}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import React from 'react';
import MockAdapter from 'axios-mock-adapter';
import { Factory } from 'rosie';

import { getConfig } from '@edx/frontend-platform';
import { mergeConfig, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { breakpoints } from '@edx/paragon';

import { initializeMockApp, render, screen } from '../../../../../../setupTest';
import {
initializeMockApp, render, screen, within, act, fireEvent, waitFor,
} from '../../../../../../setupTest';
import initializeStore from '../../../../../../store';
import { appendBrowserTimezoneToUrl, executeThunk } from '../../../../../../utils';
import { fetchCourse } from '../../../../../data';
import SidebarContext from '../../../SidebarContext';
import NotificationsWidget from './NotificationsWidget';
import setupDiscussionSidebar from '../../../../test-utils';

initializeMockApp();
jest.mock('@edx/frontend-platform/analytics');
Expand All @@ -22,7 +25,6 @@ describe('NotificationsWidget', () => {
let axiosMock;
let store;
const ID = 'NEWSIDEBAR';

const defaultMetadata = Factory.build('courseMetadata');
const courseId = defaultMetadata.id;
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/course/${defaultMetadata.id}`;
Expand All @@ -47,6 +49,35 @@ describe('NotificationsWidget', () => {
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
axiosMock.onGet(courseHomeMetadataUrl).reply(200, courseHomeMetadata);
mergeConfig({ ENABLE_NEW_SIDEBAR: 'true' }, 'Custom app config');
});

it('successfully Open/Hide sidebar tray.', async () => {
const userVerifiedMode = Factory.build('verifiedMode');

await setupDiscussionSidebar(userVerifiedMode);

const sidebarButton = await screen.getByRole('button', { name: /Show sidebar tray/i });

await act(async () => {
fireEvent.click(sidebarButton);
});

await waitFor(async () => {
expect(screen.queryByTestId('sidebar-DISCUSSIONS_NOTIFICATIONS')).toBeInTheDocument();
expect(screen.queryByTestId('notification-widget')).toBeInTheDocument();
expect(screen.queryByTitle('Discussions')).toBeInTheDocument();
});

await act(async () => {
fireEvent.click(sidebarButton);
});

await waitFor(async () => {
expect(screen.queryByTestId('sidebar-DISCUSSIONS_NOTIFICATIONS')).not.toBeInTheDocument();
expect(screen.queryByTestId('notification-widget')).not.toBeInTheDocument();
expect(screen.queryByTitle('Discussions')).not.toBeInTheDocument();
});
});

it('renders upgrade card', async () => {
Expand Down Expand Up @@ -90,6 +121,41 @@ describe('NotificationsWidget', () => {
.toBeInTheDocument();
});

it.each([
{
description: 'close the notification widget.',
enabledInContext: true,
testId:
'notification-widget',
},
{
description: 'close the sidebar when the notification widget is closed, and the discussion widget is unavailable.',
enabledInContext: false,
testId: 'sidebar-DISCUSSIONS_NOTIFICATIONS',
},
])('successfully %s', async ({ enabledInContext, testId }) => {
const userVerifiedMode = Factory.build('verifiedMode');

await setupDiscussionSidebar(userVerifiedMode, enabledInContext);

const sidebarButton = screen.getByRole('button', { name: /Show sidebar tray/i });

await act(async () => {
fireEvent.click(sidebarButton);
});

const notificationWidget = await waitFor(() => screen.getByTestId('notification-widget'));
const closeNotificationButton = within(notificationWidget).getByRole('button', { name: /Close/i });

await act(async () => {
fireEvent.click(closeNotificationButton);
});

await waitFor(() => {
expect(screen.queryByTestId(testId)).not.toBeInTheDocument();
});
});

it('marks notification as seen 3 seconds later', async () => {
jest.useFakeTimers();
const onNotificationSeen = jest.fn();
Expand Down
49 changes: 49 additions & 0 deletions src/courseware/course/test-utils.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';
import { Factory } from 'rosie';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import MockAdapter from 'axios-mock-adapter';
import { breakpoints } from '@edx/paragon';
import { initializeTestStore, render } from '../../setupTest';
import { buildTopicsFromUnits } from '../data/__factories__/discussionTopics.factory';
import { executeThunk } from '../../utils';
import * as thunks from '../data/thunks';
import Course from './Course';

const mockData = {
nextSequenceHandler: () => {},
previousSequenceHandler: () => {},
unitNavigationHandler: () => {},

Check warning on line 16 in src/courseware/course/test-utils.jsx

View check run for this annotation

Codecov / codecov/patch

src/courseware/course/test-utils.jsx#L14-L16

Added lines #L14 - L16 were not covered by tests
};

const setupDiscussionSidebar = async (verifiedMode = null, enabledInContext = true) => {
const store = await initializeTestStore();
const { courseware, models } = store.getState();
const { courseId, sequenceId } = courseware;
Object.assign(mockData, {
courseId,
sequenceId,
unitId: Object.values(models.units)[0].id,
});
global.innerWidth = breakpoints.extraLarge.minWidth;

const courseHomeMetadata = Factory.build('courseHomeMetadata', { verified_mode: verifiedMode });
const testStore = await initializeTestStore({ provider: 'openedx', courseHomeMetadata });
const state = testStore.getState();
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/${courseId}`).reply(200, { provider: 'openedx' });
const topicsResponse = buildTopicsFromUnits(state.models.units, enabledInContext);
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v2/course_topics/${courseId}`)
.reply(200, topicsResponse);

await executeThunk(thunks.getCourseDiscussionTopics(courseId), testStore.dispatch);
const [firstUnitId] = Object.keys(state.models.units);
mockData.unitId = firstUnitId;
const [firstSequenceId] = Object.keys(state.models.sequences);
mockData.sequenceId = firstSequenceId;

const wrapper = await render(<Course {...mockData} />, { store: testStore, wrapWithRouter: true });
return wrapper;
};

export default setupDiscussionSidebar;
14 changes: 11 additions & 3 deletions src/courseware/data/__factories__/discussionTopics.factory.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
/* eslint-disable import/prefer-default-export */
import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies

Factory.define('verifiedMode')
.attr('currency', 'USD')
.attr('currencySymbol', '$')
.attr('price', '$149')
.attr('sku', '8CF08E5')
.attr('upgradeUrl', 'http://localhost:18130/basket/add/?sku=8CF08E5');

Factory.define('discussionTopic')
.option('topicPrefix', null, '')
.option('courseId', null, 'course-v1:edX+DemoX+Demo_Course')
Expand All @@ -11,13 +18,14 @@ Factory.define('discussionTopic')
['id', 'courseId'],
(idx, id, courseId) => `block-v1:${courseId.replace('course-v1:', '')}+type@vertical+block@${id}`,
)
.attr('enabled_in_context', null, true)
.attr('enabled_in_context', ['enabled_in_context'], (enabledInContext) => Boolean(enabledInContext))

.attr('thread_counts', [], {
discussion: 0,
question: 0,
});

// Given a pre-build units state, build topics from it.
export function buildTopicsFromUnits(units) {
return Object.values(units).map(unit => Factory.build('discussionTopic', { usage_key: unit.id }));
export function buildTopicsFromUnits(units, enabledInContext = true) {
return Object.values(units).map(unit => Factory.build('discussionTopic', { usage_key: unit.id, enabled_in_context: enabledInContext }));
}