Skip to content

Commit

Permalink
Merge branch 'master' into fix/scroll-position-on-exit-from-video-ful…
Browse files Browse the repository at this point in the history
…lscreen-mode-master
  • Loading branch information
ihor-romaniuk authored May 5, 2023
2 parents 40e62d3 + d953979 commit 65ca026
Show file tree
Hide file tree
Showing 25 changed files with 2,451 additions and 130 deletions.
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ LOGO_WHITE_URL=''
LEGACY_THEME_NAME=''
MARKETING_SITE_BASE_URL=''
ORDER_HISTORY_URL=''
PROCTORED_EXAM_FAQ_URL=''
PROCTORED_EXAM_RULES_URL=''
REFRESH_ACCESS_TOKEN_ENDPOINT=''
SEARCH_CATALOG_URL=''
SEGMENT_KEY=''
Expand Down
4 changes: 3 additions & 1 deletion .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ ECOMMERCE_BASE_URL='http://localhost:18130'
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
EXAMS_BASE_URL='http://localhost:18740'
EXAMS_BASE_URL=''
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
IGNORED_ERROR_REGEX=''
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
Expand All @@ -29,6 +29,8 @@ LEGACY_THEME_NAME=''
MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='http://localhost:1996/orders'
PORT=2000
PROCTORED_EXAM_FAQ_URL=''
PROCTORED_EXAM_RULES_URL=''
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEARCH_CATALOG_URL='http://localhost:18000/courses'
SEGMENT_KEY=''
Expand Down
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ LEGACY_THEME_NAME=''
MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='http://localhost:1996/orders'
PORT=2000
PROCTORED_EXAM_FAQ_URL=''
PROCTORED_EXAM_RULES_URL=''
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEARCH_CATALOG_URL='http://localhost:18000/courses'
SEGMENT_KEY=''
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export TRANSIFEX_RESOURCE=frontend-app-learning
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fr_CA"
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fa_IR,fr_CA,it_IT,pt_PT,de_DE"

transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
Expand Down
359 changes: 272 additions & 87 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
},
"dependencies": {
"@edx/brand": "npm:@edx/[email protected]",
"@edx/frontend-component-footer": "11.6.3",
"@edx/frontend-component-header": "3.6.4",
"@edx/frontend-lib-special-exams": "~2.4.0",
"@edx/frontend-platform": "3.4.1",
"@edx/frontend-component-footer": "12.0.0",
"@edx/frontend-component-header": "4.0.0",
"@edx/frontend-lib-special-exams": "2.16.1",
"@edx/frontend-platform": "4.2.0",
"@edx/paragon": "20.28.4",
"@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-brands-svg-icons": "5.15.4",
Expand Down
6 changes: 4 additions & 2 deletions src/courseware/CoursewareRedirectLandingPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { PageRoute } from '@edx/frontend-platform/react';
import queryString from 'query-string';
import PageLoading from '../generic/PageLoading';

import DecodePageRoute from '../decode-page-route';

const CoursewareRedirectLandingPage = () => {
const { path } = useRouteMatch();
return (
Expand All @@ -21,7 +23,7 @@ const CoursewareRedirectLandingPage = () => {
/>

<Switch>
<PageRoute
<DecodePageRoute
path={`${path}/survey/:courseId`}
render={({ match }) => {
global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${match.params.courseId}/survey`);
Expand All @@ -40,7 +42,7 @@ const CoursewareRedirectLandingPage = () => {
global.location.assign(`${getConfig().LMS_BASE_URL}${consentPath}`);
}}
/>
<PageRoute
<DecodePageRoute
path={`${path}/home/:courseId`}
render={({ match }) => {
global.location.assign(`/course/${match.params.courseId}/home`);
Expand Down
61 changes: 60 additions & 1 deletion src/courseware/course/Course.test.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
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 {
fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor,
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 Course from './Course';
import { executeThunk } from '../../utils';
import * as thunks from '../data/thunks';

jest.mock('@edx/frontend-platform/analytics');

Expand Down Expand Up @@ -43,6 +49,28 @@ describe('Course', () => {
setItemSpy.mockRestore();
});

const setupDiscussionSidebar = async (storageValue = false) => {
localStorage.clear();
const testStore = await initializeTestStore({ provider: 'openedx' });
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;
if (storageValue !== null) {
localStorage.setItem('showDiscussionSidebar', storageValue);
}
await render(<Course {...mockData} />, { store: testStore });
};

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

it('displays notification trigger and toggles active class on click', async () => {
localStorage.setItem('showDiscussionSidebar', false);
render(<Course {...mockData} />);

const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i });
Expand All @@ -114,6 +143,7 @@ describe('Course', () => {

it('handles click to open/close notification tray', async () => {
sessionStorage.clear();
localStorage.setItem('showDiscussionSidebar', false);
render(<Course {...mockData} />);
expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"open"');
const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i });
Expand Down Expand Up @@ -144,6 +174,7 @@ describe('Course', () => {

it('handles sessionStorage from a different course for the notification tray', async () => {
sessionStorage.clear();
localStorage.setItem('showDiscussionSidebar', false);
const courseMetadataSecondCourse = Factory.build('courseMetadata', { id: 'second_course' });

// set sessionStorage for a different course before rendering Course
Expand Down Expand Up @@ -186,6 +217,34 @@ describe('Course', () => {
expect(screen.getByText(Object.values(models.sequences)[0].title)).toBeInTheDocument();
});

[
{ value: true, visible: true },
{ value: false, visible: false },
{ value: null, visible: true },
].forEach(async ({ value, visible }) => (
it(`discussion sidebar is ${visible ? 'shown' : 'hidden'} when localstorage value is ${value}`, async () => {
await setupDiscussionSidebar(value);
const element = await waitFor(() => screen.findByTestId('sidebar-DISCUSSIONS'));
if (visible) {
expect(element).not.toHaveClass('d-none');
} else {
expect(element).toHaveClass('d-none');
}
})));

[
{ value: true, result: 'false' },
{ value: false, result: 'true' },
].forEach(async ({ value, result }) => (
it(`Discussion sidebar storage value is ${!value} when sidebar is ${value ? 'closed' : 'open'}`, async () => {
await setupDiscussionSidebar(value);
await act(async () => {
const button = await screen.queryByRole('button', { name: /Show discussions tray/i });
button.click();
});
expect(localStorage.getItem('showDiscussionSidebar')).toBe(result);
})));

it('passes handlers to the sequence', async () => {
const nextSequenceHandler = jest.fn();
const previousSequenceHandler = jest.fn();
Expand Down
1 change: 0 additions & 1 deletion src/courseware/course/content-tools/contentTools.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.content-tools {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
Expand Down
31 changes: 28 additions & 3 deletions src/courseware/course/sequence/Unit.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { logError } from '@edx/frontend-platform/logging';
import { AppContext, ErrorPage } from '@edx/frontend-platform/react';
import { getExamAccess, fetchExamAccess, isExam } from '@edx/frontend-lib-special-exams';
import { Modal } from '@edx/paragon';
import PropTypes from 'prop-types';
import React, {
Expand Down Expand Up @@ -65,6 +67,16 @@ function useLoadBearingHook(id) {
}, [id]);
}

function addExamAccessToIframeUrl(accessToken, iframeUrl) {
let url = iframeUrl;
if (isExam()) {
if (accessToken) {
url += `&exam_access=${accessToken}`;
}
}
return url;
}

export function sendUrlHashToFrame(frame) {
const { hash } = window.location;
if (hash) {
Expand All @@ -83,6 +95,7 @@ const Unit = ({
}) => {
const { authenticatedUser } = useContext(AppContext);
const view = authenticatedUser ? 'student_view' : 'public_view';

let iframeUrl = `${getConfig().LMS_BASE_URL}/xblock/${id}?show_title=0&show_bookmark_button=0&recheck_access=1&view=${view}`;
if (format) {
iframeUrl += `&format=${format}`;
Expand All @@ -94,6 +107,8 @@ const Unit = ({
const [modalOptions, setModalOptions] = useState({ open: false });
const [shouldDisplayHonorCode, setShouldDisplayHonorCode] = useState(false);
const [windowTopOffset, setWindowTopOffset] = useState(null);
const [examAccessToken, setExamAccessToken] = useState('');
const [blockExamAccess, setBlockExamAccess] = useState(isExam());

const unit = useModel('units', id);
const course = useModel('coursewareMeta', courseId);
Expand Down Expand Up @@ -152,6 +167,16 @@ const Unit = ({
sendUrlHashToFrame(document.getElementById('unit-iframe'));
}, [id, setIframeHeight, hasLoaded, iframeHeight, setHasLoaded, onLoaded]);

useEffect(() => {
if (isExam()) {
fetchExamAccess().finally(() => {
const examAccess = getExamAccess();
setExamAccessToken(examAccess);
setBlockExamAccess(false);
}).catch((error) => logError(error));
}
}, [id]);

return (
<div className="unit">
<h1 className="mb-0 h3">{unit.title}</h1>
Expand Down Expand Up @@ -187,7 +212,7 @@ const Unit = ({
<HonorCode courseId={courseId} />
</Suspense>
)}
{!shouldDisplayHonorCode && !hasLoaded && !showError && (
{(!shouldDisplayHonorCode || blockExamAccess) && !hasLoaded && !showError && (
<PageLoading
srMessage={intl.formatMessage(messages.loadingSequence)}
/>
Expand Down Expand Up @@ -220,12 +245,12 @@ const Unit = ({
dialogClassName="modal-lti"
/>
)}
{!shouldDisplayHonorCode && (
{!shouldDisplayHonorCode && !blockExamAccess && (
<div className="unit-iframe-wrapper">
<iframe
id="unit-iframe"
title={unit.title}
src={iframeUrl}
src={addExamAccessToIframeUrl(examAccessToken, iframeUrl)}
allow={IFRAME_FEATURE_POLICY}
allowFullScreen
height={iframeHeight}
Expand Down
42 changes: 42 additions & 0 deletions src/courseware/course/sequence/Unit.test.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import React from 'react';
import { Factory } from 'rosie';
import { fetchExamAccess, getExamAccess, isExam } from '@edx/frontend-lib-special-exams';
import {
initializeTestStore, loadUnit, messageEvent, render, screen, waitFor,
} from '../../../setupTest';
import Unit, { sendUrlHashToFrame } from './Unit';

const originalIsExam = jest.requireActual('@edx/frontend-lib-special-exams').isExam();
const originalFetchExamAccess = jest.requireActual('@edx/frontend-lib-special-exams').fetchExamAccess();
const originalGetExamAccess = jest.requireActual('@edx/frontend-lib-special-exams').getExamAccess();
jest.mock('@edx/frontend-lib-special-exams', () => ({
...jest.requireActual('@edx/frontend-lib-special-exams'),
isExam: jest.fn(),
fetchExamAccess: jest.fn(),
getExamAccess: jest.fn(),
}));
isExam.mockImplementation(() => originalIsExam).mockReturnValue(false);
fetchExamAccess.mockImplementation(() => originalFetchExamAccess).mockResolvedValue();
const examAccessToken = 'EXAMACCESSTOKEN';
getExamAccess.mockImplementation(() => originalGetExamAccess).mockReturnValue(examAccessToken);

describe('Unit', () => {
let mockData;
const courseMetadata = Factory.build(
Expand Down Expand Up @@ -189,4 +204,31 @@ describe('Unit', () => {
expect(React.useEffect).toHaveBeenCalled();
expect(mockHashCheck).toHaveBeenCalled();
});

it('updates url if exam and exam access granted', async () => {
isExam.mockReturnValue(true);

render(<Unit {...mockData} />);
expect(isExam).toHaveBeenCalled();
await waitFor(() => expect(screen.getByTitle(unit.display_name)).toHaveAttribute('src', `http://localhost:18000/xblock/${mockData.id}?show_title=0&show_bookmark_button=0&recheck_access=1&view=student_view&format=${mockData.format}&exam_access=${examAccessToken}`));
});

it('does not update url if exam and exam access not granted', async () => {
isExam.mockReturnValue(true);
fetchExamAccess.mockRejectedValue();
getExamAccess.mockReturnValue('');

render(<Unit {...mockData} />);
expect(isExam).toHaveBeenCalled();
await waitFor(() => expect(screen.getByTitle(unit.display_name)).toHaveAttribute('src', `http://localhost:18000/xblock/${mockData.id}?show_title=0&show_bookmark_button=0&recheck_access=1&view=student_view&format=${mockData.format}`));
});

it('does not update url if not exam', async () => {
isExam.mockReturnValue(false);

render(<Unit {...mockData} />);
expect(isExam).toHaveBeenCalled();
const renderedUnit = screen.getByTitle(unit.display_name);
expect(renderedUnit).toHaveAttribute('src', `http://localhost:18000/xblock/${mockData.id}?show_title=0&show_bookmark_button=0&recheck_access=1&view=student_view&format=${mockData.format}`);
});
});
15 changes: 14 additions & 1 deletion src/courseware/course/sidebar/SidebarContextProvider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ const SidebarProvider = ({
const shouldDisplayFullScreen = useWindowSize().width < breakpoints.large.minWidth;
const shouldDisplaySidebarOpen = useWindowSize().width > breakpoints.medium.minWidth;
const showNotificationsOnLoad = getSessionStorage(`notificationTrayStatus.${courseId}`) !== 'closed';
const initialSidebar = (verifiedMode && shouldDisplaySidebarOpen && showNotificationsOnLoad)
const query = new URLSearchParams(window.location.search);
if (query.get('sidebar') === 'true') {
localStorage.setItem('showDiscussionSidebar', true);
}
const showDiscussionSidebar = localStorage.getItem('showDiscussionSidebar') !== 'false';
const showNotificationSidebar = (verifiedMode && shouldDisplaySidebarOpen && showNotificationsOnLoad)
? SIDEBARS.NOTIFICATIONS.ID
: null;
const initialSidebar = showDiscussionSidebar
? SIDEBARS.DISCUSSIONS.ID
: showNotificationSidebar;
const [currentSidebar, setCurrentSidebar] = useState(initialSidebar);
const [notificationStatus, setNotificationStatus] = useState(getLocalStorage(`notificationStatus.${courseId}`));
const [upgradeNotificationCurrentState, setUpgradeNotificationCurrentState] = useState(getLocalStorage(`upgradeNotificationCurrentState.${courseId}`));
Expand All @@ -41,6 +49,11 @@ const SidebarProvider = ({

const toggleSidebar = useCallback((sidebarId) => {
// Switch to new sidebar or hide the current sidebar
if (currentSidebar === SIDEBARS.DISCUSSIONS.ID) {
localStorage.setItem('showDiscussionSidebar', false);
} else if (sidebarId === SIDEBARS.DISCUSSIONS.ID) {
localStorage.setItem('showDiscussionSidebar', true);
}
setCurrentSidebar(sidebarId === currentSidebar ? null : sidebarId);
}, [currentSidebar]);

Expand Down
1 change: 1 addition & 0 deletions src/courseware/course/sidebar/common/SidebarBase.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const SidebarBase = ({
'min-vh-100': !shouldDisplayFullScreen,
'd-none': currentSidebar !== sidebarId,
}, className)}
data-testid={`sidebar-${sidebarId}`}
style={{ width: shouldDisplayFullScreen ? '100%' : width }}
aria-label={ariaLabel}
>
Expand Down
16 changes: 16 additions & 0 deletions src/decode-page-route/__snapshots__/index.test.jsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`DecodePageRoute should not modify the url if it does not need to be decoded 1`] = `
<div>
PageRoute: {
"computedMatch": {
"path": "/course/:courseId/home",
"url": "/course/course-v1:edX+DemoX+Demo_Course/home",
"isExact": true,
"params": {
"courseId": "course-v1:edX+DemoX+Demo_Course"
}
}
}
</div>
`;
Loading

0 comments on commit 65ca026

Please sign in to comment.