Files
frontend-app-learning/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.test.jsx
Kyle McCormick 353964e75c feat: handle courseware paths more liberally (#395)
Valid courseware URLs currently include:
* /course/:courseId
* /course/:courseId/:sequenceId
* /course/:courseId/:sequenceId/:unitId

In this commit we add support for:
* /course/:courseId/:sectionId
* /course/:courseId/:sectionId/:unitId
* /course/:courseId/:unitId

All URL forms still redirect to:
  /course/:courseId/:sequenceId/:unitId

See ADR #8 for more context.

All changes:
* refactor: allow courseBlocks factory to build multiple sections
* refactor: make CoursewareContainer tests less brittle & stateful
* feat: handle courseware paths more liberally
* refactor: reorder, rename, & comment redirection functions

TNL-7796
2021-04-01 09:10:00 -04:00

166 lines
7.0 KiB
JavaScript

import React from 'react';
import { Factory } from 'rosie';
import {
render, screen, fireEvent, getByText, initializeTestStore,
} from '../../../../setupTest';
import SequenceNavigation from './SequenceNavigation';
import useIndexOfLastVisibleChild from '../../../../generic/tabs/useIndexOfLastVisibleChild';
// Mock the hook to avoid relying on its implementation and mocking `getBoundingClientRect`.
jest.mock('../../../../generic/tabs/useIndexOfLastVisibleChild');
useIndexOfLastVisibleChild.mockReturnValue([0, null, null]);
describe('Sequence Navigation', () => {
let mockData;
const courseMetadata = Factory.build('courseMetadata');
const unitBlocks = Array.from({ length: 3 }).map(() => Factory.build(
'block',
{ type: 'problem' },
{ courseId: courseMetadata.id },
));
beforeAll(async () => {
const store = await initializeTestStore({ courseMetadata, unitBlocks });
const { courseware } = store.getState();
mockData = {
unitId: unitBlocks[1].id,
sequenceId: courseware.sequenceId,
previousSequenceHandler: () => {},
onNavigate: () => {},
nextSequenceHandler: () => {},
goToCourseExitPage: () => {},
};
});
it('is empty while loading', async () => {
const testStore = await initializeTestStore({ excludeFetchSequence: true }, false);
const { container } = render(<SequenceNavigation {...mockData} />, { store: testStore });
expect(container).toBeEmptyDOMElement();
});
it('renders empty div without unitId', () => {
const { container } = render(<SequenceNavigation {...mockData} unitId={undefined} />);
expect(getByText(container, (content, element) => (
element.tagName.toLowerCase() === 'div' && element.getAttribute('style')))).toBeEmptyDOMElement();
});
it('renders locked button for gated content', async () => {
const sequenceBlocks = [Factory.build(
'block',
{ type: 'sequential', children: [unitBlocks.map(block => block.id)] },
{ courseId: courseMetadata.id },
)];
const sequenceMetadata = [Factory.build(
'sequenceMetadata',
{ gated_content: { gated: true } },
{ courseId: courseMetadata.id, unitBlocks, sequenceBlock: sequenceBlocks[0] },
)];
const testStore = await initializeTestStore({ unitBlocks, sequenceBlocks, sequenceMetadata }, false);
const testData = {
...mockData,
sequenceId: sequenceBlocks[0].id,
onNavigate: jest.fn(),
};
render(<SequenceNavigation {...testData} />, { store: testStore });
const unitButton = screen.getByTitle(unitBlocks[1].display_name);
fireEvent.click(unitButton);
// The unit button should not work for gated content.
expect(testData.onNavigate).not.toHaveBeenCalled();
// TODO: Not sure if this is working as expected, because the `contentType="lock"` will be overridden by the value
// from Redux. To make this provide a `fa-icon` lock we could introduce something like `overriddenContentType`.
expect(unitButton.firstChild).toHaveClass('fa-edit');
});
it('renders correctly and handles unit button clicks', () => {
const onNavigate = jest.fn();
render(<SequenceNavigation {...mockData} {...{ onNavigate }} />);
const unitButtons = screen.getAllByRole('button', { name: /\d+/ });
expect(unitButtons).toHaveLength(unitButtons.length);
unitButtons.forEach(button => fireEvent.click(button));
expect(onNavigate).toHaveBeenCalledTimes(unitButtons.length);
});
it('has both navigation buttons enabled for a non-corner unit of the sequence', () => {
render(<SequenceNavigation {...mockData} />);
screen.getAllByRole('button', { name: /previous|next/i }).forEach(button => {
expect(button).toBeEnabled();
});
});
it('has the "Previous" button disabled for the first unit of the sequence', () => {
render(<SequenceNavigation {...mockData} unitId={unitBlocks[0].id} />);
expect(screen.getByRole('button', { name: /previous/i })).toBeDisabled();
expect(screen.getByRole('button', { name: /next/i })).toBeEnabled();
});
it('has the "Next" button disabled for the last unit of the sequence if there is no Exit page', async () => {
const testMetadata = { ...courseMetadata, certificate_data: { cert_status: 'bogus_status' }, user_has_passing_grade: true };
const testStore = await initializeTestStore({ courseMetadata: testMetadata, unitBlocks }, false);
// Have to refetch the sequenceId since the new store generates new sequences
const { courseware } = testStore.getState();
const testData = { ...mockData, sequenceId: courseware.sequenceId };
render(
<SequenceNavigation {...testData} unitId={unitBlocks[unitBlocks.length - 1].id} />,
{ store: testStore },
);
expect(screen.getByRole('button', { name: /previous/i })).toBeEnabled();
expect(screen.getByRole('button', { name: /next/i })).toBeDisabled();
});
it('displays end of course message instead of the "Next" button as needed', async () => {
const testMetadata = { ...courseMetadata, certificate_data: { cert_status: 'notpassing' }, enrollment: { is_active: true } };
const testStore = await initializeTestStore({ courseMetadata: testMetadata, unitBlocks }, false);
// Have to refetch the sequenceId since the new store generates new sequences
const { courseware } = testStore.getState();
const testData = { ...mockData, sequenceId: courseware.sequenceId };
render(
<SequenceNavigation {...testData} unitId={unitBlocks[unitBlocks.length - 1].id} />,
{ store: testStore },
);
expect(screen.getByRole('button', { name: /previous/i })).toBeEnabled();
expect(screen.getByRole('button', { name: /next \(end of course\)/i })).toBeEnabled();
});
it('displays complete course message instead of the "Next" button as needed', async () => {
const testMetadata = {
...courseMetadata,
certificate_data: { cert_status: 'downloadable' },
enrollment: { is_active: true },
user_has_passing_grade: true,
};
const testStore = await initializeTestStore({ courseMetadata: testMetadata, unitBlocks }, false);
// Have to refetch the sequenceId since the new store generates new sequences
const { courseware } = testStore.getState();
const testData = { ...mockData, sequenceId: courseware.sequenceId };
render(
<SequenceNavigation {...testData} unitId={unitBlocks[unitBlocks.length - 1].id} />,
{ store: testStore },
);
expect(screen.getByRole('button', { name: /previous/i })).toBeEnabled();
expect(screen.getByRole('button', { name: /Complete the course/i })).toBeEnabled();
});
it('handles "Previous" and "Next" click', () => {
const previousSequenceHandler = jest.fn();
const nextSequenceHandler = jest.fn();
render(<SequenceNavigation {...mockData} {...{ previousSequenceHandler, nextSequenceHandler }} />);
fireEvent.click(screen.getByRole('button', { name: /previous/i }));
expect(previousSequenceHandler).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByRole('button', { name: /next/i }));
expect(nextSequenceHandler).toHaveBeenCalledTimes(1);
});
});