Agrendalath/bb 2599 low priority tests (#214)

* [TNL-7269] WIP low priority tests

* [TNL-7269] Add low priority tests

* [TNL-7269] Fix failing EnrollmentAlert tests

* [TNL-7269] Address review comments

* Fixing test errors on rebase with master.

Co-authored-by: Agrendalath <piotr@surowiec.it>
This commit is contained in:
David Joy
2020-09-18 09:27:41 -04:00
committed by GitHub
parent 25e5d39a72
commit 927d424d33
34 changed files with 1222 additions and 37 deletions

View File

@@ -0,0 +1,157 @@
import React from 'react';
import { Factory } from 'rosie';
import {
loadUnit, render, screen, waitFor, getByRole, initializeTestStore, fireEvent,
} from '../../setupTest';
import Course from './Course';
import { handleNextSectionCelebration } from './celebration';
import * as celebrationUtils from './celebration/utils';
jest.mock('@edx/frontend-platform/analytics');
const recordFirstSectionCelebration = jest.fn();
celebrationUtils.recordFirstSectionCelebration = recordFirstSectionCelebration;
describe('Course', () => {
let store;
const mockData = {
nextSequenceHandler: () => {},
previousSequenceHandler: () => {},
unitNavigationHandler: () => {},
};
beforeAll(async () => {
store = await initializeTestStore();
const { courseware, models } = store.getState();
const { courseId, sequenceId } = courseware;
Object.assign(mockData, {
courseId,
sequenceId,
unitId: Object.values(models.units)[0].id,
});
});
it('loads learning sequence', async () => {
render(<Course {...mockData} />);
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
expect(screen.getByText('Loading learning sequence...')).toBeInTheDocument();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Learn About Verified Certificates' })).not.toBeInTheDocument();
loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
const { models } = store.getState();
const sequence = models.sequences[mockData.sequenceId];
const section = models.sections[sequence.sectionId];
const course = models.courses[mockData.courseId];
expect(document.title).toMatch(
`${sequence.title} | ${section.title} | ${course.title} | edX`,
);
});
it('displays celebration modal', async () => {
// TODO: Remove these console mocks after merging https://github.com/edx/paragon/pull/526.
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
// Mock media queries, because `Celebration` modal uses `react-break` for responsive breakpoints.
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
const courseMetadata = Factory.build('courseMetadata', { celebrations: { firstSection: true } });
const testStore = await initializeTestStore({ courseMetadata }, false);
const { courseware, models } = testStore.getState();
const { courseId, sequenceId } = courseware;
const testData = {
...mockData,
courseId,
sequenceId,
unitId: Object.values(models.units)[0].id,
};
// Set up LocalStorage for testing.
handleNextSectionCelebration(sequenceId, sequenceId, testData.unitId);
render(<Course {...testData} />, { store: testStore });
const celebrationModal = screen.getByRole('dialog');
expect(celebrationModal).toBeInTheDocument();
expect(getByRole(celebrationModal, 'heading', { name: 'Congratulations!' })).toBeInTheDocument();
});
it('displays upgrade sock', async () => {
const courseMetadata = Factory.build('courseMetadata', { can_show_upgrade_sock: true });
const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false);
render(<Course {...mockData} courseId={courseMetadata.id} />, { store: testStore });
expect(screen.getByRole('button', { name: 'Learn About Verified Certificates' })).toBeInTheDocument();
});
it('displays offer and expiration alert', async () => {
const offerText = 'test-offer';
const offerId = `${offerText}-id`;
const offerHtml = `<div data-testid="${offerId}">${offerText}</div>`;
const expirationText = 'test-expiration';
const expirationId = `${expirationText}-id`;
const expirationHtml = `<div data-testid="${expirationId}">${expirationText}</div>`;
const courseMetadata = Factory.build('courseMetadata', {
offer_html: offerHtml,
course_expired_message: expirationHtml,
});
const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false);
render(<Course {...mockData} courseId={courseMetadata.id} />, { store: testStore });
expect(await screen.findByTestId(offerId)).toHaveTextContent(offerText);
expect(screen.getByTestId(expirationId)).toHaveTextContent(expirationText);
});
it('passes handlers to the sequence', async () => {
const nextSequenceHandler = jest.fn();
const previousSequenceHandler = jest.fn();
const unitNavigationHandler = jest.fn();
const courseMetadata = Factory.build('courseMetadata');
const unitBlocks = Array.from({ length: 3 }).map(() => Factory.build(
'block',
{ type: 'vertical' },
{ courseId: courseMetadata.id },
));
const testStore = await initializeTestStore({ courseMetadata, unitBlocks }, false);
const { courseware, models } = testStore.getState();
const { courseId, sequenceId } = courseware;
const testData = {
...mockData,
courseId,
sequenceId,
unitId: Object.values(models.units)[1].id, // Corner cases are already covered in `Sequence` tests.
nextSequenceHandler,
previousSequenceHandler,
unitNavigationHandler,
};
render(<Course {...testData} />, { store: testStore });
loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
screen.getAllByRole('button', { name: /previous/i }).forEach(button => fireEvent.click(button));
screen.getAllByRole('button', { name: /next/i }).forEach(button => fireEvent.click(button));
// We are in the middle of the sequence, so no
expect(previousSequenceHandler).not.toHaveBeenCalled();
expect(nextSequenceHandler).not.toHaveBeenCalled();
expect(unitNavigationHandler).toHaveBeenCalledTimes(4);
});
});

View File

@@ -0,0 +1,94 @@
import React from 'react';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';
import { Factory } from 'rosie';
import {
render, screen, fireEvent, initializeTestStore, waitFor, authenticatedUser, logUnhandledRequests,
} from '../../../setupTest';
import { BookmarkButton } from './index';
describe('Bookmark Button', () => {
let axiosMock;
let store;
const courseMetadata = Factory.build('courseMetadata');
const mockData = {
isProcessing: false,
};
const nonBookmarkedUnitBlock = Factory.build(
'block',
{ type: 'vertical' },
{ courseId: courseMetadata.id },
);
const bookmarkedUnitBlock = Factory.build(
'block',
{ type: 'vertical', bookmarked: true },
{ courseId: courseMetadata.id },
);
const unitBlocks = [nonBookmarkedUnitBlock, bookmarkedUnitBlock];
beforeEach(async () => {
store = await initializeTestStore({ courseMetadata, unitBlocks });
mockData.unitId = nonBookmarkedUnitBlock.id;
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
const bookmarkUrl = `${getConfig().LMS_BASE_URL}/api/bookmarks/v1/bookmarks/`;
axiosMock.onPost(bookmarkUrl).reply(200, { });
const bookmarkDeleteUrlRegExp = new RegExp(`${bookmarkUrl}*,*`);
axiosMock.onDelete(bookmarkDeleteUrlRegExp).reply(200, { });
logUnhandledRequests(axiosMock);
});
it('handles adding bookmark', async () => {
render(<BookmarkButton {...mockData} />);
const button = screen.getByRole('button', { name: 'Bookmark this page' });
expect(button).not.toHaveClass('disabled');
fireEvent.click(button);
await waitFor(() => expect(axiosMock.history.post).toHaveLength(1));
expect(axiosMock.history.post[0].data).toEqual(JSON.stringify({ usage_id: nonBookmarkedUnitBlock.id }));
expect(store.getState().models.units[nonBookmarkedUnitBlock.id].bookmarked).toBeTruthy();
});
it('does not handle adding bookmark when processing', async () => {
render(<BookmarkButton {...mockData} isProcessing />);
const button = screen.getByRole('button', { name: 'Bookmark this page' });
expect(button).toHaveClass('disabled');
fireEvent.click(button);
// HACK: We don't have a function we could reliably await here, so this test relies on the timeout of `waitFor`.
await expect(waitFor(
() => expect(axiosMock.history.post).toHaveLength(1),
{ timeout: 100 },
)).rejects.toThrowError(/expect.*toHaveLength.*/);
expect(store.getState().models.units[nonBookmarkedUnitBlock.id].bookmarked).toBeFalsy();
});
it('handles removing bookmark', async () => {
render(<BookmarkButton {...mockData} unitId={bookmarkedUnitBlock.id} isBookmarked />);
const button = screen.getByRole('button', { name: 'Bookmarked' });
fireEvent.click(button);
await waitFor(() => expect(axiosMock.history.delete).toHaveLength(1));
expect(axiosMock.history.delete[0].url).toContain(`${authenticatedUser.username},${bookmarkedUnitBlock.id}`);
expect(store.getState().models.units[bookmarkedUnitBlock.id].bookmarked).toBeFalsy();
});
it('does not handle removing bookmark when processing', async () => {
render(<BookmarkButton {...mockData} unitId={bookmarkedUnitBlock.id} isBookmarked isProcessing />);
const button = screen.getByRole('button', { name: 'Bookmarked' });
expect(button).toHaveClass('disabled');
fireEvent.click(button);
// HACK: We don't have a function we could reliably await here, so this test relies on the timeout of `waitFor`.
await expect(waitFor(
() => expect(axiosMock.history.delete).toHaveLength(1),
{ timeout: 100 },
)).rejects.toThrowError(/expect.*toHaveLength.*/);
expect(store.getState().models.units[bookmarkedUnitBlock.id].bookmarked).toBeTruthy();
});
});

View File

@@ -7,7 +7,7 @@ import * as thunks from './thunks';
import executeThunk from '../../../../utils';
import initializeMockApp from '../../../../setupTest';
import { initializeMockApp } from '../../../../setupTest';
import initializeStore from '../../../../store';
const { loggingService } = initializeMockApp();

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { initializeTestStore, render, screen } from '../../../setupTest';
import ContentTools from './ContentTools';
jest.mock('./calculator/Calculator', () => () => <div data-testid="Calculator" />);
jest.mock('./notes-visibility/NotesVisibility', () => () => <div data-testid="NotesVisibility" />);
describe('Content Tools', () => {
const mockData = {
course: {
notes: { enabled: false },
showCalculator: false,
},
};
beforeAll(async () => {
await initializeTestStore({ excludeFetchCourse: true, excludeFetchSequence: true });
});
it('hides content tools', () => {
const { container } = render(<ContentTools {...mockData} />);
expect(container.getElementsByClassName('d-flex')[0]).toBeEmptyDOMElement();
});
it('displays Calculator', () => {
const testData = JSON.parse(JSON.stringify(mockData));
testData.course.showCalculator = true;
render(<ContentTools {...testData} />);
expect(screen.getByTestId('Calculator')).toBeInTheDocument();
expect(screen.queryByTestId('NotesVisibility')).not.toBeInTheDocument();
});
it('displays Notes Visibility', () => {
const testData = JSON.parse(JSON.stringify(mockData));
testData.course.notes.enabled = true;
render(<ContentTools {...testData} />);
expect(screen.getByTestId('NotesVisibility')).toBeInTheDocument();
expect(screen.queryByTestId('Calculator')).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,79 @@
import React from 'react';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';
import Calculator from './Calculator';
import {
initializeTestStore, render, screen, fireEvent, waitFor, logUnhandledRequests,
} from '../../../../setupTest';
describe('Calculator', () => {
let axiosMock;
let equationUrl;
beforeAll(async () => {
await initializeTestStore({ excludeFetchCourse: true, excludeFetchSequence: true });
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
equationUrl = new RegExp(`${getConfig().LMS_BASE_URL}/calculate*`);
});
it('expands on click', () => {
render(<Calculator />);
expect(screen.queryByRole('button', { name: 'Calculate' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Calculator Instructions' })).not.toBeInTheDocument();
const button = screen.getByRole('button', { name: 'Calculator' });
expect(button.querySelector('svg')).toHaveClass('fa-calculator');
fireEvent.click(button);
expect(button.querySelector('svg')).toHaveClass('fa-times-circle');
expect(screen.getByRole('button', { name: 'Calculate' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Calculator Instructions' })).toBeInTheDocument();
fireEvent.click(button);
expect(button.querySelector('svg')).toHaveClass('fa-calculator');
});
it('displays instructions on click', () => {
render(<Calculator />);
const button = screen.getByRole('button', { name: 'Calculator' });
fireEvent.click(button);
const instructionsButton = screen.getByRole('button', { name: 'Calculator Instructions' });
expect(instructionsButton.querySelector('svg')).toHaveClass('fa-question-circle');
expect(screen.queryByText(/For detailed information, see/)).not.toBeInTheDocument();
fireEvent.click(instructionsButton);
expect(instructionsButton.querySelector('svg')).toHaveClass('fa-times-circle');
expect(screen.getByText(/For detailed information, see/)).toBeInTheDocument();
fireEvent.click(instructionsButton);
expect(instructionsButton.querySelector('svg')).toHaveClass('fa-question-circle');
});
it('handles submitting equation', async () => {
const equation = 'log2(2^10)';
const result = '10';
axiosMock.reset();
axiosMock.onGet(equationUrl).reply(200, { result });
logUnhandledRequests(axiosMock);
render(<Calculator />);
fireEvent.click(screen.getByRole('button', { name: 'Calculator' }));
const input = screen.getByRole('textbox', { name: 'Calculator Input' });
const output = screen.getByRole('textbox', { name: 'Calculator Result' });
const submitButton = screen.getByRole('button', { name: 'Calculate' });
fireEvent.change(input, { target: { value: equation } });
fireEvent.click(submitButton);
await waitFor(() => expect(axiosMock.history.get).toHaveLength(1));
expect(axiosMock.history.get[0].url).toContain(escape(equation));
expect(output).toHaveValue(result);
});
});

View File

@@ -57,11 +57,10 @@ class NotesVisibility extends Component {
NotesVisibility.propTypes = {
intl: intlShape.isRequired,
course: PropTypes.shape({
id: PropTypes.string,
id: PropTypes.string.isRequired,
notes: PropTypes.shape({
enabled: PropTypes.bool,
visible: PropTypes.bool,
}),
}).isRequired,
}).isRequired,
};

View File

@@ -0,0 +1,97 @@
import React from 'react';
import { waitFor } from '@testing-library/dom';
import { getConfig } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import {
fireEvent, initializeTestStore, logUnhandledRequests, render, screen,
} from '../../../../setupTest';
import NotesVisibility from './NotesVisibility';
const originalConfig = jest.requireActual('@edx/frontend-platform').getConfig();
jest.mock('@edx/frontend-platform', () => ({
...jest.requireActual('@edx/frontend-platform'),
getConfig: jest.fn(),
}));
describe('Notes Visibility', () => {
let axiosMock;
let visibilityUrl;
const mockData = {
course: {
id: 'test-course',
notes: {
visible: false,
},
},
};
beforeAll(async () => {
await initializeTestStore({ excludeFetchCourse: true, excludeFetchSequence: true });
// Mock `targetOrigin` of the `postMessage`.
getConfig.mockImplementation(() => originalConfig);
const config = { ...originalConfig };
config.LMS_BASE_URL = `${window.location.protocol}//${window.location.host}`;
getConfig.mockImplementation(() => config);
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
visibilityUrl = `${config.LMS_BASE_URL}/courses/${mockData.course.id}/edxnotes/visibility/`;
});
beforeEach(() => {
axiosMock.reset();
axiosMock.onPut(visibilityUrl).reply(200);
logUnhandledRequests(axiosMock);
});
it('hides notes', () => {
render(<NotesVisibility {...mockData} />);
const button = screen.getByRole('switch', { name: 'Show Notes' });
expect(button)
.not.toBeChecked()
.toHaveClass('text-success');
expect(button.querySelector('svg'))
.toHaveClass('fa-pencil-alt')
.toHaveAttribute('aria-hidden', 'true');
});
it('shows notes', () => {
const testData = JSON.parse(JSON.stringify(mockData));
testData.course.notes.visible = true;
render(<NotesVisibility {...testData} />);
const button = screen.getByRole('switch', { name: 'Hide Notes' });
expect(button)
.toBeChecked()
.toHaveClass('text-secondary');
expect(button.querySelector('svg'))
.toHaveClass('fa-pencil-alt')
.toHaveAttribute('aria-hidden', 'true');
});
it('handles click', async () => {
const mockFn = jest.fn();
const frame = document.createElement('iframe');
frame.id = 'unit-iframe';
const { container } = render(<NotesVisibility {...mockData} />);
container.appendChild(frame);
frame.contentWindow.addEventListener('message', e => {
mockFn(e.data);
});
fireEvent.click(screen.getByRole('switch', { name: 'Show Notes' }));
await waitFor(() => expect(mockFn).toHaveBeenCalled());
expect(mockFn)
.toHaveBeenCalledTimes(1)
.toHaveBeenCalledWith('tools.toggleNotes');
expect(axiosMock.history.put).toHaveLength(1);
expect(axiosMock.history.put[0].url).toEqual(visibilityUrl);
expect(axiosMock.history.put[0].data).toEqual(`{"visibility":${mockData.course.notes.visible}}`);
expect(screen.getByRole('switch', { name: 'Hide Notes' })).toBeInTheDocument();
});
});

View File

@@ -191,9 +191,5 @@ CourseSock.propTypes = {
currencySymbol: PropTypes.string,
sku: PropTypes.string,
upgradeUrl: PropTypes.string,
}),
};
CourseSock.defaultProps = {
verifiedMode: null,
}).isRequired,
};

View File

@@ -0,0 +1,41 @@
import React from 'react';
import {
render, screen, fireEvent, initializeMockApp,
} from '../../../setupTest';
import CourseSock from './CourseSock';
describe('Course Sock', () => {
const mockData = {
verifiedMode: {
upgradeUrl: 'test-url',
price: 1234,
currency: 'dollars',
currencySymbol: '$',
},
};
beforeAll(async () => {
// We need to mock AuthService to implicitly use `getAuthenticatedUser` within `AppContext.Provider`.
await initializeMockApp();
});
it('hides upsell information on load', () => {
render(<CourseSock {...mockData} />);
expect(screen.getByRole('button', { name: 'Learn About Verified Certificates' })).toBeInTheDocument();
expect(screen.queryByText('edX Verified Certificate')).not.toBeInTheDocument();
});
it('handles click', () => {
render(<CourseSock {...mockData} />);
const upsellButton = screen.getByRole('button', { name: 'Learn About Verified Certificates' });
fireEvent.click(upsellButton);
expect(screen.getByText('edX Verified Certificate')).toBeInTheDocument();
const { currencySymbol, price, currency } = mockData.verifiedMode;
expect(screen.getByText(`Upgrade (${currencySymbol}${price} ${currency})`)).toBeInTheDocument();
fireEvent.click(upsellButton);
expect(screen.queryByText('edX Verified Certificate')).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { history } from '@edx/frontend-platform';
import {
render, screen, fireEvent, initializeMockApp,
} from '../../../../setupTest';
import ContentLock from './ContentLock';
describe('Content Lock', () => {
const mockData = {
courseId: 'test-course-id',
prereqSectionName: 'test-prerequisite-section-name',
prereqId: 'test-prerequisite-id',
sequenceTitle: 'test-sequence-title',
};
beforeAll(async () => {
// We need to mock AuthService to implicitly use `getAuthenticatedUser` within `AppContext.Provider`.
await initializeMockApp();
});
it('displays sequence title along with lock icon', () => {
const { container } = render(<ContentLock {...mockData} />);
const lockIcon = container.querySelector('svg');
expect(lockIcon).toHaveClass('fa-lock');
expect(lockIcon.parentElement).toHaveTextContent(mockData.sequenceTitle);
});
it('displays prerequisite name', () => {
const prereqText = `You must complete the prerequisite: '${mockData.prereqSectionName}' to access this content.`;
render(<ContentLock {...mockData} />);
expect(screen.getByText(prereqText)).toBeInTheDocument();
});
it('handles click', () => {
history.push = jest.fn();
render(<ContentLock {...mockData} />);
fireEvent.click(screen.getByRole('button'));
expect(history.push).toHaveBeenCalledWith(`/course/${mockData.courseId}/${mockData.prereqId}`);
});
});

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { Factory } from 'rosie';
import { initializeTestStore, render, screen } from '../../../../setupTest';
import LockPaywall from './LockPaywall';
describe('Lock Paywall', () => {
let store;
const mockData = {};
beforeAll(async () => {
store = await initializeTestStore();
const { courseware } = store.getState();
mockData.courseId = courseware.courseId;
});
it('displays message along with lock icon', () => {
const { container } = render(<LockPaywall {...mockData} />);
const lockIcon = container.querySelector('svg');
expect(lockIcon).toHaveClass('fa-lock');
expect(lockIcon.parentElement).toHaveTextContent('Verified Track Access');
});
it('displays unlock link with price', () => {
const { currencySymbol, price, upgradeUrl } = store.getState().models.courses[mockData.courseId].verifiedMode;
render(<LockPaywall {...mockData} />);
const upgradeLink = screen.getByRole('link', { name: `Upgrade to unlock (${currencySymbol}${price})` });
expect(upgradeLink).toHaveAttribute('href', `${upgradeUrl}`);
});
it('does not display anything if course does not have verified mode', async () => {
const courseMetadata = Factory.build('courseMetadata', { verified_mode: null });
const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false);
const { container } = render(<LockPaywall {...mockData} courseId={courseMetadata.id} />, { store: testStore });
expect(container).toBeEmptyDOMElement();
});
});