From 8719fad0916fb81047331bb6e0a84f7934e109fe Mon Sep 17 00:00:00 2001
From: Agrendalath
Date: Mon, 29 Jun 2020 03:56:42 +0200
Subject: [PATCH] [TNL-7268] Add high priority tests
---
.../course/sequence/Sequence.test.jsx | 240 +++++++++
.../course/sequence/SequenceContent.test.jsx | 59 +--
src/courseware/course/sequence/Unit.test.jsx | 80 +++
.../__snapshots__/Sequence.test.jsx.snap | 475 ++++++++++++++++++
.../SequenceContent.test.jsx.snap | 146 ------
.../sequence/__snapshots__/Unit.test.jsx.snap | 203 ++++++++
.../SequenceNavigation.test.jsx | 44 +-
.../SequenceNavigationDropdown.test.jsx | 22 +-
.../SequenceNavigationTabs.test.jsx | 2 +-
.../sequence-navigation/UnitButton.test.jsx | 2 +-
.../sequence-navigation/UnitIcon.test.jsx | 2 +-
.../UnitNavigation.test.jsx | 43 +-
src/setupTest.js | 143 ++++++
src/test/test-utils.js | 49 --
14 files changed, 1155 insertions(+), 355 deletions(-)
create mode 100644 src/courseware/course/sequence/Sequence.test.jsx
create mode 100644 src/courseware/course/sequence/Unit.test.jsx
create mode 100644 src/courseware/course/sequence/__snapshots__/Sequence.test.jsx.snap
create mode 100644 src/courseware/course/sequence/__snapshots__/Unit.test.jsx.snap
delete mode 100644 src/test/test-utils.js
diff --git a/src/courseware/course/sequence/Sequence.test.jsx b/src/courseware/course/sequence/Sequence.test.jsx
new file mode 100644
index 00000000..edc1f528
--- /dev/null
+++ b/src/courseware/course/sequence/Sequence.test.jsx
@@ -0,0 +1,240 @@
+import React from 'react';
+import { fireEvent, waitFor } from '@testing-library/dom';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { cloneDeep } from 'lodash';
+import { sendTrackEvent } from '@edx/frontend-platform/analytics';
+import {
+ initialState,
+ messageEvent, render, screen, testUnits,
+} from '../../../setupTest';
+import Sequence from './Sequence';
+
+jest.mock('@edx/frontend-platform/analytics');
+
+describe('Sequence', () => {
+ const mockData = {
+ unitId: '3',
+ sequenceId: '1',
+ courseId: '1',
+ unitNavigationHandler: () => {},
+ nextSequenceHandler: () => {},
+ previousSequenceHandler: () => {},
+ intl: {},
+ };
+
+ it('renders correctly without data', () => {
+ const { asFragment } = render(
+ , { initialState: {} },
+ );
+ expect(screen.getByText('Loading learning sequence...')).toBeInTheDocument();
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
+ expect(asFragment()).toMatchSnapshot();
+ });
+
+ it('renders correctly for gated content', async () => {
+ const { asFragment } = render();
+ expect(screen.getByText('Loading locked content messaging...')).toBeInTheDocument();
+ // Only `Previous`, `Next` and `Bookmark` buttons.
+ expect(screen.getAllByRole('button').length).toEqual(3);
+
+ const beforeLoadingUnit = asFragment();
+ expect(beforeLoadingUnit).toMatchSnapshot();
+
+ window.postMessage(messageEvent, '*');
+ await waitFor(() => expect(screen.getByText(/You must complete the prerequisite/)).toBeInTheDocument());
+ expect(beforeLoadingUnit).toMatchDiffSnapshot(asFragment());
+ });
+
+ it('displays error message on sequence load failure', () => {
+ const testState = cloneDeep(initialState);
+ testState.courseware.sequenceStatus = 'failed';
+ const { asFragment } = render(, { initialState: testState });
+
+ expect(screen.getByText('There was an error loading this course.')).toBeInTheDocument();
+ expect(asFragment()).toMatchSnapshot();
+ });
+
+ it('handles loading unit', async () => {
+ const { asFragment } = render();
+ expect(screen.getByText('Loading learning sequence...')).toBeInTheDocument();
+ // Renders navigation buttons plus one button for each unit.
+ expect(screen.getAllByRole('button').length).toEqual(3 + testUnits.length);
+
+ const beforeLoadingUnit = asFragment();
+ expect(beforeLoadingUnit).toMatchSnapshot();
+
+ window.postMessage(messageEvent, '*');
+ await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
+ // At this point there will be 2 `Previous` and 2 `Next` buttons.
+ expect(screen.getAllByRole('button', { name: /previous|next/i }).length).toEqual(4);
+ expect(beforeLoadingUnit).toMatchDiffSnapshot(asFragment());
+ });
+
+ it('navigates to the previous sequence if the unit is the first in the sequence', async () => {
+ sendTrackEvent.mockClear();
+ const unitId = '1';
+ const sequenceId = '2';
+ const previousSequenceHandler = jest.fn();
+ render();
+
+ const sequencePreviousButton = screen.getByRole('button', { name: /previous/i });
+ fireEvent.click(sequencePreviousButton);
+ expect(previousSequenceHandler).toHaveBeenCalledTimes(1);
+ expect(sendTrackEvent).toHaveBeenCalledTimes(1);
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.sequence.previous_selected', {
+ current_tab: Number(unitId), id: unitId, tab_count: testUnits.length, widget_placement: 'top',
+ });
+
+ window.postMessage(messageEvent, '*');
+ await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
+ const unitPreviousButton = screen.getAllByRole('button', { name: /previous/i })
+ .filter(button => button !== sequencePreviousButton)[0];
+ fireEvent.click(unitPreviousButton);
+ expect(previousSequenceHandler).toHaveBeenCalledTimes(2);
+ expect(sendTrackEvent).toHaveBeenCalledTimes(2);
+ expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.ui.lms.sequence.previous_selected', {
+ current_tab: Number(unitId), id: unitId, tab_count: testUnits.length, widget_placement: 'bottom',
+ });
+ });
+
+ it('navigates to the next sequence if the unit is the last in the sequence', async () => {
+ sendTrackEvent.mockClear();
+ const unitId = String(testUnits.length);
+ const sequenceId = '1';
+ const nextSequenceHandler = jest.fn();
+ render();
+
+ const sequenceNextButton = screen.getByRole('button', { name: /next/i });
+ fireEvent.click(sequenceNextButton);
+ expect(nextSequenceHandler).toHaveBeenCalledTimes(1);
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.sequence.next_selected', {
+ current_tab: Number(unitId), id: unitId, tab_count: testUnits.length, widget_placement: 'top',
+ });
+
+ window.postMessage(messageEvent, '*');
+ await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
+ const unitNextButton = screen.getAllByRole('button', { name: /next/i })
+ .filter(button => button !== sequenceNextButton)[0];
+ fireEvent.click(unitNextButton);
+ expect(nextSequenceHandler).toHaveBeenCalledTimes(2);
+ expect(sendTrackEvent).toHaveBeenCalledTimes(2);
+ expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.ui.lms.sequence.next_selected', {
+ current_tab: Number(unitId), id: unitId, tab_count: testUnits.length, widget_placement: 'bottom',
+ });
+ });
+
+ it('navigates to the previous/next unit if the unit is not in the corner of the sequence', () => {
+ sendTrackEvent.mockClear();
+ const unitNavigationHandler = jest.fn();
+ const previousSequenceHandler = jest.fn();
+ const nextSequenceHandler = jest.fn();
+ render();
+
+ fireEvent.click(screen.getByRole('button', { name: /previous/i }));
+ expect(previousSequenceHandler).not.toHaveBeenCalled();
+ expect(unitNavigationHandler).toHaveBeenCalledWith(String(Number(mockData.unitId) - 1));
+
+ fireEvent.click(screen.getByRole('button', { name: /next/i }));
+ expect(nextSequenceHandler).not.toHaveBeenCalled();
+ expect(unitNavigationHandler).toHaveBeenNthCalledWith(2, String(Number(mockData.unitId) + 1));
+
+ expect(sendTrackEvent).toHaveBeenCalledTimes(2);
+ });
+
+ it('handles the `Previous` buttons for the first unit in the first sequence', async () => {
+ sendTrackEvent.mockClear();
+ const unitNavigationHandler = jest.fn();
+ const previousSequenceHandler = jest.fn();
+ const unitId = '1';
+ render();
+ window.postMessage(messageEvent, '*');
+ await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
+
+ screen.getAllByRole('button', { name: /previous/i }).forEach(button => fireEvent.click(button));
+
+ expect(previousSequenceHandler).not.toHaveBeenCalled();
+ expect(unitNavigationHandler).not.toHaveBeenCalled();
+ expect(sendTrackEvent).not.toHaveBeenCalled();
+ });
+
+ it('handles the `Next` buttons for the last unit in the last sequence', async () => {
+ sendTrackEvent.mockClear();
+ const unitNavigationHandler = jest.fn();
+ const nextSequenceHandler = jest.fn();
+ const unitId = String(testUnits.length);
+ const sequenceId = String(Object.keys(initialState.models.sequences).length);
+ render();
+ window.postMessage(messageEvent, '*');
+ await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
+
+ screen.getAllByRole('button', { name: /next/i }).forEach(button => fireEvent.click(button));
+
+ expect(nextSequenceHandler).toHaveBeenCalledTimes(1);
+ expect(unitNavigationHandler).not.toHaveBeenCalled();
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.sequence.next_selected', {
+ current_tab: Number(unitId), id: unitId, tab_count: testUnits.length, widget_placement: 'top',
+ });
+ });
+
+ it('handles the navigation buttons for empty sequence', async () => {
+ sendTrackEvent.mockClear();
+ const testState = cloneDeep(initialState);
+ testState.models.sequences['1'].unitIds = [];
+
+ const unitNavigationHandler = jest.fn();
+ const previousSequenceHandler = jest.fn();
+ const nextSequenceHandler = jest.fn();
+ render(, { initialState: testState });
+ window.postMessage(messageEvent, '*');
+ await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
+
+ screen.getAllByRole('button', { name: /previous/i }).forEach(button => fireEvent.click(button));
+ expect(previousSequenceHandler).toHaveBeenCalledTimes(2);
+ expect(unitNavigationHandler).not.toHaveBeenCalled();
+
+ screen.getAllByRole('button', { name: /next/i }).forEach(button => fireEvent.click(button));
+ expect(nextSequenceHandler).toHaveBeenCalledTimes(2);
+ expect(unitNavigationHandler).not.toHaveBeenCalled();
+
+ expect(sendTrackEvent).toHaveBeenNthCalledWith(1, 'edx.ui.lms.sequence.previous_selected', {
+ current_tab: 1, id: mockData.unitId, tab_count: 0, widget_placement: 'top',
+ });
+ expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.ui.lms.sequence.previous_selected', {
+ current_tab: 1, id: mockData.unitId, tab_count: 0, widget_placement: 'bottom',
+ });
+ expect(sendTrackEvent).toHaveBeenNthCalledWith(3, 'edx.ui.lms.sequence.next_selected', {
+ current_tab: 1, id: mockData.unitId, tab_count: 0, widget_placement: 'top',
+ });
+ expect(sendTrackEvent).toHaveBeenNthCalledWith(4, 'edx.ui.lms.sequence.next_selected', {
+ current_tab: 1, id: mockData.unitId, tab_count: 0, widget_placement: 'bottom',
+ });
+ });
+
+ it('handles unit navigation button', () => {
+ sendTrackEvent.mockClear();
+ const unitNavigationHandler = jest.fn();
+ const targetUnit = '4';
+ render();
+
+ fireEvent.click(screen.getByRole('button', { name: targetUnit }));
+ expect(unitNavigationHandler).toHaveBeenCalledWith(targetUnit);
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.sequence.tab_selected', {
+ current_tab: Number(mockData.unitId), id: mockData.unitId, target_tab: Number(targetUnit), tab_count: testUnits.length, widget_placement: 'top',
+ });
+ });
+});
diff --git a/src/courseware/course/sequence/SequenceContent.test.jsx b/src/courseware/course/sequence/SequenceContent.test.jsx
index f0bbf2e3..a0aa14c2 100644
--- a/src/courseware/course/sequence/SequenceContent.test.jsx
+++ b/src/courseware/course/sequence/SequenceContent.test.jsx
@@ -1,65 +1,8 @@
import React from 'react';
-import { render, screen } from '../../../test/test-utils';
+import { initialState, render, screen } from '../../../setupTest';
import SequenceContent from './SequenceContent';
describe('Sequence Content', () => {
- window.scrollTo = jest.fn();
- // HACK: Mock the MutationObserver as it's breaking async testing.
- // According to StackOverflow it should be fixed in `jest-environment-jsdom` v16,
- // but upgrading `jest` to v26 didn't fix this problem.
- // ref: https://stackoverflow.com/questions/61036156/react-typescript-testing-typeerror-mutationobserver-is-not-a-constructor
- global.MutationObserver = class {
- // eslint-disable-next-line no-unused-vars,no-useless-constructor,no-empty-function
- constructor(callback) {}
-
- disconnect() {}
-
- // eslint-disable-next-line no-unused-vars
- observe(element, initObject) {}
- };
-
- const testUnits = [...Array(10).keys()].map(i => String(i + 1));
- const initialState = {
- courseware: {
- sequenceStatus: 'loaded',
- courseStatus: 'loaded',
- courseId: '1',
- },
- models: {
- courses: {
- 1: {
- sectionIds: ['1'],
- },
- },
- sections: {
- 1: {
- sequenceIds: ['1', '2'],
- },
- },
- sequences: {
- 1: {
- unitIds: testUnits,
- showCompletion: true,
- title: 'test-sequence',
- gatedContent: {
- prereqId: '1',
- gatedSectionName: 'test-gated-section',
- },
- },
- },
- units: testUnits.reduce(
- (acc, unitId) => Object.assign(acc, {
- [unitId]: {
- id: unitId,
- contentType: 'other',
- title: unitId,
- },
- }),
- {},
- ),
- },
- };
-
const mockData = {
gated: false,
courseId: '1',
diff --git a/src/courseware/course/sequence/Unit.test.jsx b/src/courseware/course/sequence/Unit.test.jsx
new file mode 100644
index 00000000..02ec4998
--- /dev/null
+++ b/src/courseware/course/sequence/Unit.test.jsx
@@ -0,0 +1,80 @@
+import React from 'react';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { cloneDeep } from 'lodash';
+import { waitFor } from '@testing-library/dom';
+import {
+ initialState, messageEvent, render, screen,
+} from '../../../setupTest';
+import Unit from './Unit';
+
+describe('Unit', () => {
+ const mockData = {
+ id: '3',
+ courseId: '1',
+ intl: {},
+ };
+
+ it('renders correctly', () => {
+ const { asFragment } = render(, { initialState });
+
+ expect(screen.getByText('Loading learning sequence...')).toBeInTheDocument();
+ expect(screen.getByTitle(mockData.id)).toHaveAttribute('height', String(0));
+ expect(asFragment()).toMatchSnapshot();
+ });
+
+ it('renders proper message for gated content', () => {
+ // Clone initialState.
+ const testState = cloneDeep(initialState);
+ testState.models.units[mockData.id].graded = true;
+ const { asFragment } = render(, { initialState: testState });
+
+ expect(screen.getByText('Loading locked content messaging...')).toBeInTheDocument();
+ expect(asFragment()).toMatchSnapshot();
+ });
+
+ it('handles receiving MessageEvent', async () => {
+ const { asFragment } = render(, { initialState });
+ const beforePostingMessage = asFragment();
+
+ window.postMessage(messageEvent, '*');
+ // Loading message is gone now.
+ await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
+ // Iframe's height is set via message.
+ expect(screen.getByTitle(mockData.id)).toHaveAttribute('height', String(messageEvent.payload.height));
+ expect(beforePostingMessage).toMatchDiffSnapshot(asFragment());
+ });
+
+ it('handles onLoaded after receiving MessageEvent', async () => {
+ const onLoaded = jest.fn();
+ render(, { initialState });
+
+ window.postMessage(messageEvent, '*');
+ await waitFor(() => expect(onLoaded).toHaveBeenCalledTimes(1));
+ });
+
+ it('resizes iframe on second MessageEvent, does not call onLoaded again', async () => {
+ const onLoaded = jest.fn();
+ // Clone message and set different height.
+ const testMessageWithOtherHeight = { ...messageEvent, payload: { height: 200 } };
+ render(, { initialState });
+
+ window.postMessage(messageEvent, '*');
+ await waitFor(() => expect(screen.getByTitle(mockData.id)).toHaveAttribute('height', String(messageEvent.payload.height)));
+ window.postMessage(testMessageWithOtherHeight, '*');
+ await waitFor(() => expect(screen.getByTitle(mockData.id)).toHaveAttribute('height', String(testMessageWithOtherHeight.payload.height)));
+ expect(onLoaded).toHaveBeenCalledTimes(1);
+ });
+
+ it('ignores MessageEvent with unhandled type', async () => {
+ // Clone message and set different type.
+ const testMessageWithUnhandledType = { ...messageEvent, type: 'wrong type' };
+ render(, { initialState });
+
+ window.postMessage(testMessageWithUnhandledType, '*');
+ // 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(screen.getByTitle(mockData.id)).toHaveAttribute('height', String(testMessageWithUnhandledType.payload.height)),
+ { timeout: 100 },
+ )).rejects.toThrowErrorMatchingSnapshot();
+ });
+});
diff --git a/src/courseware/course/sequence/__snapshots__/Sequence.test.jsx.snap b/src/courseware/course/sequence/__snapshots__/Sequence.test.jsx.snap
new file mode 100644
index 00000000..19ba71c7
--- /dev/null
+++ b/src/courseware/course/sequence/__snapshots__/Sequence.test.jsx.snap
@@ -0,0 +1,475 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Sequence displays error message on sequence load failure 1`] = `
+
+
+ There was an error loading this course.
+
+
+`;
+
+exports[`Sequence handles loading unit 1`] = `
+
+
+
+
+
+
+
+ 3
+
+
+
+
+
+
+ Loading learning sequence...
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Sequence handles loading unit 2`] = `
+"Snapshot Diff:
+- First value
++ Second value
+
+@@ -190,40 +190,53 @@
+
+ Bookmark this page
+
+
+
+-
+-
+-
+-
+- Loading learning sequence...
+-
+-
+-
+-
+
+
+
++
++
++
++
+
+
+
+
+ "
+`;
+
+exports[`Sequence renders correctly for gated content 1`] = `
+
+
+
+
+
+
+
+
+
+ Loading locked content messaging...
+
+
+
+
+
+
+
+
+`;
+
+exports[`Sequence renders correctly for gated content 2`] = `
+"Snapshot Diff:
+- First value
++ Second value
+
+@@ -47,26 +47,31 @@
+
+
+
+-
+-
++
![\\"fa-lock\\"]()
++ test-sequence-3
++
++
++ Content Locked
++
++
++ You must complete the prerequisite: 'test-gated-section' to access this content.
++
++
++
+-
++ Go To Prerequisite Section
++
++
+
+
+
+ "
+`;
+
+exports[`Sequence renders correctly without data 1`] = `
+
+
+
+
+
+ Loading learning sequence...
+
+
+
+
+
+`;
diff --git a/src/courseware/course/sequence/__snapshots__/SequenceContent.test.jsx.snap b/src/courseware/course/sequence/__snapshots__/SequenceContent.test.jsx.snap
index a3d7a4b6..0018bd69 100644
--- a/src/courseware/course/sequence/__snapshots__/SequenceContent.test.jsx.snap
+++ b/src/courseware/course/sequence/__snapshots__/SequenceContent.test.jsx.snap
@@ -123,149 +123,3 @@ exports[`Sequence Content displays messages for the locked content 2`] = `
`;
-
-exports[`Unit Navigation displays loading message 1`] = `
-
-
-
- 1
-
-
-
-
-
-
- Loading learning sequence...
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`Unit Navigation displays message for no content 1`] = `
-
-
- There is no content here.
-
-
-`;
-
-exports[`Unit Navigation displays message for the locked content 1`] = `
-
-
-
-
-
- Loading locked content messaging...
-
-
-
-
-
-`;
-
-exports[`Unit Navigation displays messages for the locked content 1`] = `
-
-
-
-
-
- Loading locked content messaging...
-
-
-
-
-
-`;
-
-exports[`Unit Navigation displays messages for the locked content 2`] = `
-
-
-
- test-sequence
-
-
- Content Locked
-
-
- You must complete the prerequisite: 'test-gated-section' to access this content.
-
-
-
-
-
-`;
diff --git a/src/courseware/course/sequence/__snapshots__/Unit.test.jsx.snap b/src/courseware/course/sequence/__snapshots__/Unit.test.jsx.snap
new file mode 100644
index 00000000..cf09688c
--- /dev/null
+++ b/src/courseware/course/sequence/__snapshots__/Unit.test.jsx.snap
@@ -0,0 +1,203 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Unit handles receiving MessageEvent 1`] = `
+"Snapshot Diff:
+- First value
++ Second value
+
+@@ -28,33 +28,16 @@
+
+ Bookmark this page
+
+
+
+-
+
+-
+-
+- Loading learning sequence...
+-
+-
+-
+-
+-