fix: breadcrumb preview link

This commit is contained in:
KristinAoki
2025-03-26 16:29:07 -04:00
committed by Feanil Patel
parent cf4bea3604
commit 5b7f76b43d
6 changed files with 308 additions and 236 deletions

View File

@@ -1,134 +0,0 @@
import React from 'react';
import { screen, render } from '@testing-library/react';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';
import { BrowserRouter } from 'react-router-dom';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { useModel, useModels } from '../../generic/model-store';
import CourseBreadcrumbs from './CourseBreadcrumbs';
jest.mock('@edx/frontend-platform');
jest.mock('@edx/frontend-platform/analytics');
// Remove When Fully rolled out>>>
jest.mock('../../generic/model-store');
jest.mock('@edx/frontend-platform/auth');
getConfig.mockImplementation(() => ({ ENABLE_JUMPNAV: 'true' }));
getAuthenticatedUser.mockImplementation(() => ({ administrator: true }));
// ^^^^Remove When Fully rolled out
jest.mock('react-redux', () => ({
connect: (mapStateToProps, mapDispatchToProps) => (ReactComponent) => ({
mapStateToProps,
mapDispatchToProps,
ReactComponent,
}),
Provider: ({ children }) => children,
useSelector: () => 'loaded',
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
Link: jest.fn().mockImplementation(({ to, children }) => (
<a href={to}>{children}</a>
)),
}));
useModels.mockImplementation((name) => {
if (name === 'sections') {
return [
{
courseId: 'course-v1:edX+DemoX+Demo_Course',
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
sequenceIds: ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction'],
title: 'Introduction',
},
{
courseId: 'course-v1:edX+DemoX+Demo_Course',
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
sequenceIds: ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions'],
title: 'Example Week 1: Getting Started',
},
];
}
return [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
title: 'Lesson 1 - Getting Started',
unitIds: [
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
],
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
title: 'Homework - Question Styles',
unitIds: [
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@47dbd5f836544e61877a483c0b75606c',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@54bb9b142c6c4c22afc62bcb628f0e68',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0c92347a5c00',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_1fef54c2b23b',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2889db1677a549abb15eb4d886f95d1c',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e8a5cc2aed424838853defab7be45e42',
],
},
];
});
useModel.mockImplementation(() => ({
sectionIds: ['block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations'],
}));
describe('CourseBreadcrumbs', () => {
jest.spyOn(React, 'useMemo').mockImplementation(() => [
[
{
default: false,
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
label: 'Introduction',
url: 'http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction',
},
{
default: true,
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
label: 'Example Week 1: Getting Started',
url: 'http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course/block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
},
],
[
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@simulations', label: "Lesson 2 - Let's Get Interactive!", default: true, url: 'http://localhost:2000/course/course-v1:edX+DemoX+D…e@vertical+block@d0d804e8863c4a95a659c04d8a2b2bc0',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e', label: 'Homework - Essays', default: false, url: 'http://localhost:2000/course/course-v1:edX+DemoX+D…e@vertical+block@fb79dcbad35b466a8c6364f8ffee9050',
},
],
]);
render(
<IntlProvider>
<BrowserRouter>
<CourseBreadcrumbs
courseId="course-v1:edX+DemoX+Demo_Course"
sectionId="block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations"
sequenceId="block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
isStaff
/>
</BrowserRouter>,
</IntlProvider>,
);
it('renders course breadcrumbs as expected', async () => {
expect(screen.queryAllByRole('link')).toHaveLength(1);
const courseHomeButtonDestination = screen.getAllByRole('link')[0].href;
expect(courseHomeButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home');
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
expect(screen.queryAllByTestId('breadcrumb-item')).toHaveLength(2);
});
});

View File

@@ -0,0 +1,103 @@
import React, { useState } from 'react';
import { getConfig } from '@edx/frontend-platform';
import {
useToggle,
ModalPopup,
Menu,
} from '@openedx/paragon';
import { Link, useLocation } from 'react-router-dom';
import JumpNavMenuItem from '../JumpNavMenuItem';
interface Props {
content: {
default: boolean,
id: string,
label: string,
sequences: {
id: string,
}[],
} [];
withSeparator: boolean | false,
separator: string | '';
courseId: string;
sequenceId: string | '';
unitId: string | '';
isStaff: boolean | false;
}
const BreadcrumbItem: React.FC<Props> = ({
content,
withSeparator,
separator,
courseId,
sequenceId,
unitId,
isStaff,
}) => {
const defaultContent = content.filter(
(destination: { default: boolean }) => destination.default,
)[0] || { id: courseId, label: '', sequences: [] };
const showRegularLink = getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !isStaff;
const [isOpen, open, close] = useToggle(false);
const [target, setTarget] = useState(null);
const { pathname } = useLocation();
const isPreview = pathname.startsWith('/preview');
const baseUrl = defaultContent.sequences.length
? `/course/${courseId}/${defaultContent.sequences[0].id}`
: `/course/${courseId}/${defaultContent.id}`;
const link = isPreview ? `/preview${baseUrl}` : baseUrl;
return (
<>
{withSeparator && separator && (
<li className="col-auto p-0 mx-2 text-primary-500 text-truncate text-nowrap" role="presentation" aria-hidden>{separator}</li>
)}
<li
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
data-testid="breadcrumb-item"
>
{showRegularLink ? (
<Link
className="text-primary-500"
to={link}
>
{defaultContent.label}
</Link>
) : (
<>
{
// @ts-ignore
<a className="text-primary-500" variant="link" onClick={open} ref={setTarget}>
{defaultContent.label}
</a>
}
<ModalPopup positionRef={target} isOpen={isOpen} onClose={close}>
<Menu>
{content.map((item) => (
<JumpNavMenuItem
key={item.label}
isDefault={item.default}
sequences={item.sequences}
courseId={courseId}
title={item.label}
currentSequence={sequenceId}
currentUnit={unitId}
onClick={close}
/>
))}
</Menu>
</ModalPopup>
</>
)}
</li>
</>
);
};
export default BreadcrumbItem;

View File

@@ -1,107 +1,12 @@
import React, { useMemo, useState } from 'react';
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faHome } from '@fortawesome/free-solid-svg-icons';
import { useSelector } from 'react-redux';
import { useToggle, ModalPopup, Menu } from '@openedx/paragon';
import { Link } from 'react-router-dom';
import { useModel, useModels } from '../../generic/model-store';
import JumpNavMenuItem from './JumpNavMenuItem';
const CourseBreadcrumb = ({
content,
withSeparator,
courseId,
sequenceId,
unitId,
isStaff,
}) => {
const defaultContent = content.filter(
(destination) => destination.default,
)[0] || { id: courseId, label: '', sequences: [] };
const showRegularLink = getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !isStaff;
const [isOpen, open, close] = useToggle(false);
const [target, setTarget] = useState(null);
return (
<>
{withSeparator && (
<li className="col-auto p-0 mx-2 text-primary-500 text-truncate text-nowrap" role="presentation" aria-hidden>/</li>
)}
<li
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
data-testid="breadcrumb-item"
>
{showRegularLink ? (
<Link
className="text-primary-500"
to={
defaultContent.sequences.length
? `/course/${courseId}/${defaultContent.sequences[0].id}`
: `/course/${courseId}/${defaultContent.id}`
}
>
{defaultContent.label}
</Link>
) : (
<>
{
// eslint-disable-next-line
<a className="text-primary-500" onClick={open} ref={setTarget}>
{defaultContent.label}
</a>
}
<ModalPopup positionRef={target} isOpen={isOpen} onClose={close}>
<Menu>
{content.map((item) => (
<JumpNavMenuItem
key={item.label}
isDefault={item.default}
sequences={item.sequences}
courseId={courseId}
title={item.label}
currentSequence={sequenceId}
currentUnit={unitId}
onClick={close}
/>
))}
</Menu>
</ModalPopup>
</>
)}
</li>
</>
);
};
CourseBreadcrumb.propTypes = {
content: PropTypes.arrayOf(
PropTypes.shape({
default: PropTypes.bool,
id: PropTypes.string,
label: PropTypes.string,
}),
).isRequired,
sequenceId: PropTypes.string,
unitId: PropTypes.string,
withSeparator: PropTypes.bool,
courseId: PropTypes.string,
isStaff: PropTypes.bool,
};
CourseBreadcrumb.defaultProps = {
withSeparator: false,
sequenceId: null,
unitId: null,
courseId: null,
isStaff: null,
};
import { useModel, useModels } from '../../../generic/model-store';
import BreadcrumbItem from './BreadcrumbItem';
const CourseBreadcrumbs = ({
courseId,
@@ -110,14 +15,16 @@ const CourseBreadcrumbs = ({
unitId,
isStaff,
}) => {
const course = useModel('coursewareMeta', courseId);
const course = useModel('coursewareMeta', courseId);
const courseStatus = useSelector((state) => state.courseware.courseStatus);
const sequenceStatus = useSelector(
(state) => state.courseware.sequenceStatus,
);
console.log( useModels('sections', course.sectionIds));
const allSequencesInSections = Object.fromEntries(
useModels('sections', course.sectionIds).map((section) => [
useModels('sections', course.sectionIds)?.map((section) => [
section.id,
{
default: section.id === sectionId,
@@ -152,6 +59,8 @@ const CourseBreadcrumbs = ({
}
return [chapters, sequentials];
}, [courseStatus, sequenceStatus, allSequencesInSections]);
console.log(links);
return (
<nav aria-label="breadcrumb" className="d-inline-block col-sm-10 mb-3">
@@ -171,7 +80,7 @@ const CourseBreadcrumbs = ({
</Link>
</li>
{links.map((content, i) => (
<CourseBreadcrumb
<BreadcrumbItem
// eslint-disable-next-line react/no-array-index-key
key={i}
courseId={courseId}
@@ -179,6 +88,7 @@ const CourseBreadcrumbs = ({
content={content}
unitId={unitId}
withSeparator
separator="/"
isStaff={isStaff}
/>
))}

View File

@@ -0,0 +1,190 @@
import { Factory } from 'rosie';
import { AppProvider } from '@edx/frontend-platform/react';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp, initializeTestStore } from '@src/setupTest';
import CourseBreadcrumbs from './CourseBreadcrumbs';
// import { useModel, useModels } from '../../../generic/model-store';
// jest.mock('@edx/frontend-platform');
// jest.mock('@edx/frontend-platform/analytics');
// // Remove When Fully rolled out>>>
// jest.mock('../../../generic/model-store');
// jest.mock('@edx/frontend-platform/auth');
// getConfig.mockImplementation(() => ({ ENABLE_JUMPNAV: 'true' }));
// getAuthenticatedUser.mockImplementation(() => ({ administrator: true }));
// // ^^^^Remove When Fully rolled out
// jest.mock('react-redux', () => ({
// connect: (mapStateToProps, mapDispatchToProps) => (ReactComponent) => ({
// mapStateToProps,
// mapDispatchToProps,
// ReactComponent,
// }),
// Provider: ({ children }) => children,
// useSelector: () => 'loaded',
// }));
// jest.mock('react-router-dom', () => ({
// ...jest.requireActual('react-router-dom'),
// Link: jest.fn().mockImplementation(({ to, children }) => (
// <a href={to}>{children}</a>
// )),
// }));
// useModels.mockImplementation((name) => {
// if (name === 'sections') {
// return [
// {
// courseId: 'course-v1:edX+DemoX+Demo_Course',
// id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
// sequenceIds: ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction'],
// title: 'Introduction',
// },
// {
// courseId: 'course-v1:edX+DemoX+Demo_Course',
// id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
// sequenceIds: ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
// 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions'],
// title: 'Example Week 1: Getting Started',
// },
// ];
// }
// return [
// {
// id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
// sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
// title: 'Lesson 1 - Getting Started',
// unitIds: [
// 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
// 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
// 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
// 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
// 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
// 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
// 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
// ],
// },
// {
// id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
// sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
// title: 'Homework - Question Styles',
// unitIds: [
// 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
// 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@47dbd5f836544e61877a483c0b75606c',
// 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@54bb9b142c6c4c22afc62bcb628f0e68',
// 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0c92347a5c00',
// 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_1fef54c2b23b',
// 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2889db1677a549abb15eb4d886f95d1c',
// 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e8a5cc2aed424838853defab7be45e42',
// ],
// },
// ];
// });
// useModel.mockImplementation(() => ({
// sectionIds: ['block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
// 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations'],
// }));
const props = {
courseId: "course-v1:edX+DemoX+Demo_Course",
sectionId: "block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations",
sequenceId: "block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions",
isStaff: true,
}
const courseMetadata = Factory.build('courseMetadata', { courseId: props.courseId, sectionIds: [props.sectionId] });
const sequenceBlocks = [Factory.build(
'block',
{ type: 'sequential', id: props.sequenceId, title: 'Subsection' },
{ courseId: props.courseId },
)];
const sectionBlocks = [Factory.build(
'block',
{ type: 'chapter',
id: props.sectionId,
title: 'Section',
children: [{ id: props.sequenceId}],
},
{ courseId: props.courseId },
)];
initializeMockApp();
describe('CourseBreadcrumbs', () => {
let store = {};
let unit;
let sequenceId;
const initTestStore = async () => {
const courseBlocks = { sectionBlocks, sequenceBlocks };
console.log(courseBlocks);
store = await initializeTestStore({ courseMetadata, ...courseBlocks });
const state = store.getState();
[sequenceId] = Object.keys(state.courseware.courseOutline.sequences);
const sequence = state.courseware.courseOutline.sequences[sequenceId];
unit = state.courseware.courseOutline.units[sequence.unitIds[0]];
};
function renderWithProvider (pathname = '/course') {
const { container } = render(
<AppProvider store={store} wrapWithRouter={false}>
<IntlProvider locale="en">
<MemoryRouter initialEntries={[{ pathname }]}>
<CourseBreadcrumbs {...props} />
</MemoryRouter>
</IntlProvider>
</AppProvider>,
);
return container;
}
describe('in live view', () => {
it('renders course breadcrumbs as expected', async () => {
await initTestStore();
renderWithProvider();
const courseHomeButtonDestination = screen.getAllByRole('link')[0].href;
expect(courseHomeButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home');
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
expect(screen.queryAllByTestId('breadcrumb-item')).toHaveLength(2);
});
it('section link does not include /preview/', async () => {
await initTestStore();
renderWithProvider();
const sectionBreadcrumb = screen.getByText(sectionBlocks[0].block_id);
const sectionLink = sectionBreadcrumb.closest('a').href;
expect(sectionLink.includes('/preview/')).toBeFalsy();
});
});
describe('in live view', () => {
it('renders course breadcrumbs as expected', async () => {
await initTestStore();
renderWithProvider('/preview/courses');
const courseHomeButtonDestination = screen.getAllByRole('link')[0].href;
expect(courseHomeButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home');
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
expect(screen.queryAllByTestId('breadcrumb-item')).toHaveLength(2);
});
it('section link does includes /preview/', async () => {
await initTestStore();
renderWithProvider('/preview/courses');
const sectionBreadcrumb = screen.getByText(sectionBlocks[0].block_id);
const sectionLink = sectionBreadcrumb.closest('a').href;
expect(sectionLink.includes('/preview/')).toBeTruthy();
});
});
});

View File

@@ -0,0 +1,3 @@
import CourseBreadcrumbs from './CourseBreadcrumbs';
export default CourseBreadcrumbs;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import CourseBreadcrumbs from '../../courseware/course/CourseBreadcrumbs';
import CourseBreadcrumbs from '../../courseware/course/breadcrumbs';
interface Props {
courseId: string;