Skip to content

Commit

Permalink
feat: [AXIMST-11] add functionality and tests for unit page header
Browse files Browse the repository at this point in the history
  • Loading branch information
monteri committed Jan 17, 2024
1 parent bdb4ffe commit 3aa7257
Show file tree
Hide file tree
Showing 46 changed files with 2,130 additions and 21 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ EXAMS_BASE_URL=''
FAVICON_URL=''
LANGUAGE_PREFERENCE_COOKIE_NAME=''
LMS_BASE_URL=''
PREVIEW_BASE_URL=''
LEARNING_BASE_URL=''
LOGIN_URL=''
LOGO_TRADEMARK_URL=''
Expand Down
3 changes: 2 additions & 1 deletion .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ ENABLE_PROGRESS_GRAPH_SETTINGS=false
ENABLE_TEAM_TYPE_SETTING=false
ENABLE_NEW_EDITOR_PAGES=true
ENABLE_NEW_VIDEO_UPLOAD_PAGE = false
ENABLE_UNIT_PAGE = false
ENABLE_UNIT_PAGE = true
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = false
ENABLE_TAGGING_TAXONOMY_PAGES = true
BBB_LEARN_MORE_URL=''
Expand All @@ -43,3 +43,4 @@ HOTJAR_VERSION=6
HOTJAR_DEBUG=true
INVITE_STUDENTS_EMAIL_TO="[email protected]"
AI_TRANSLATIONS_BASE_URL='http://localhost:18760'
PREVIEW_BASE_URL='http://preview.localhost:18000'
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ EXAMS_BASE_URL=
FAVICON_URL='https://edx-cdn.org/v3/default/favicon.ico'
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
PREVIEW_BASE_URL='http://preview.localhost:18000'
LEARNING_BASE_URL='http://localhost:2000'
LOGIN_URL='http://localhost:18000/login'
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
Expand Down
4 changes: 2 additions & 2 deletions src/CourseAuthoringRoutes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
Navigate, Routes, Route, useParams,
} from 'react-router-dom';
import { PageWrap } from '@edx/frontend-platform/react';
import Placeholder from '@edx/frontend-lib-content-components';
import CourseAuthoringPage from './CourseAuthoringPage';
import { PagesAndResources } from './pages-and-resources';
import EditorContainer from './editors/EditorContainer';
Expand All @@ -16,6 +15,7 @@ import ScheduleAndDetails from './schedule-and-details';
import { GradingSettings } from './grading-settings';
import CourseTeam from './course-team/CourseTeam';
import { CourseUpdates } from './course-updates';
import { CourseUnit } from './course-unit';
import CourseExportPage from './export-page/CourseExportPage';
import CourseImportPage from './import-page/CourseImportPage';

Expand Down Expand Up @@ -71,7 +71,7 @@ const CourseAuthoringRoutes = () => {
/>
<Route
path="/container/:blockId"
element={process.env.ENABLE_UNIT_PAGE === 'true' ? <PageWrap><Placeholder /></PageWrap> : null}
element={process.env.ENABLE_UNIT_PAGE === 'true' ? <PageWrap><CourseUnit courseId={courseId} /></PageWrap> : null}
/>
<Route
path="editor/course-videos/:blockId"
Expand Down
107 changes: 107 additions & 0 deletions src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { Container, Layout } from '@edx/paragon';
import { useIntl, injectIntl } from '@edx/frontend-platform/i18n';
import { ErrorAlert } from '@edx/frontend-lib-content-components';

import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
import SubHeader from '../generic/sub-header/SubHeader';
import { RequestStatus } from '../data/constants';
import getPageHeadTitle from '../generic/utils';
import ProcessingNotification from '../generic/processing-notification';
import InternetConnectionAlert from '../generic/internet-connection-alert';
import HeaderTitle from './header-title/HeaderTitle';
import Breadcrumbs from './breadcrumbs/Breadcrumbs';
import HeaderNavigations from './header-navigations/HeaderNavigations';
import { useCourseUnit } from './hooks';
import messages from './messages';

import './CourseUnit.scss';

const CourseUnit = ({ courseId }) => {
const { blockId } = useParams();
const intl = useIntl();
const {
isLoading,
unitTitle,
savingStatus,
isTitleEditFormOpen,
isInternetConnectionAlertFailed,
handleTitleEditSubmit,
headerNavigationsActions,
handleTitleEdit,
handleInternetConnectionFailed,
} = useCourseUnit({ courseId, blockId });

document.title = getPageHeadTitle('', unitTitle);

const {
isShow: isShowProcessingNotification,
title: processingNotificationTitle,
} = useSelector(getProcessingNotification);

if (isLoading) {
return null;
}

return (
<>
<Container size="xl" className="course-unit px-4">
<section className="course-unit-container mb-4 mt-5">
<ErrorAlert hideHeading isError={savingStatus === RequestStatus.FAILED}>
{intl.formatMessage(messages.alertFailedGeneric, { actionName: 'save', type: 'changes' })}
</ErrorAlert>
<SubHeader
hideBorder
title={(
<HeaderTitle
unitTitle={unitTitle}
isTitleEditFormOpen={isTitleEditFormOpen}
handleTitleEdit={handleTitleEdit}
handleTitleEditSubmit={handleTitleEditSubmit}
/>
)}
breadcrumbs={(
<Breadcrumbs
courseId={courseId}
/>
)}
headerActions={(
<HeaderNavigations
headerNavigationsActions={headerNavigationsActions}
/>
)}
/>
<Layout
lg={[{ span: 9 }, { span: 3 }]}
md={[{ span: 9 }, { span: 3 }]}
sm={[{ span: 9 }, { span: 3 }]}
xs={[{ span: 9 }, { span: 3 }]}
xl={[{ span: 9 }, { span: 3 }]}
>
<Layout.Element />
<Layout.Element />
</Layout>
</section>
</Container>
<div className="alert-toast">
<ProcessingNotification
isShow={isShowProcessingNotification}
title={processingNotificationTitle}
/>
<InternetConnectionAlert
isFailed={isInternetConnectionAlertFailed}
isQueryPending={savingStatus === RequestStatus.PENDING}
onInternetConnectionFailed={handleInternetConnectionFailed}
/>
</div>
</>
);
};

CourseUnit.propTypes = {
courseId: PropTypes.string.isRequired,
};

export default injectIntl(CourseUnit);
1 change: 1 addition & 0 deletions src/course-unit/CourseUnit.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "./breadcrumbs/Breadcrumbs";
147 changes: 147 additions & 0 deletions src/course-unit/CourseUnit.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import MockAdapter from 'axios-mock-adapter';
import {
act, render, waitFor, fireEvent,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { getConfig, initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

import {
getCourseUnitApiUrl,
getXBlockBaseApiUrl,
} from './data/api';
import {
fetchCourseUnitQuery,
} from './data/thunk';
import initializeStore from '../store';
import {
courseUnitIndexMock,
} from './__mocks__';
import { executeThunk } from '../utils';
import CourseUnit from './CourseUnit';
import headerNavigationsMessages from './header-navigations/messages';
import headerTitleMessages from './header-title/messages';
import { getUnitPreviewPath, getUnitViewLivePath } from './utils';

let axiosMock;
let store;
const courseId = '123';
const sectionId = '19a30717eff543078a5d94ae9d6c18a5';
const blockId = '567890';
const unitDisplayName = courseUnitIndexMock.metadata.display_name;

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({ blockId }),
}));

const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseUnit courseId={courseId} />
</IntlProvider>
</AppProvider>
);

describe('<CourseUnit />', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});

store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, courseUnitIndexMock);
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
});

it('render CourseUnit component correctly', async () => {
const { getByText, getByRole } = render(<RootWrapper />);
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;

await waitFor(() => {
expect(getByText(unitDisplayName)).toBeInTheDocument();
expect(getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
});
});

it('handles CourseUnit header action buttons', async () => {
const { open } = window;
window.open = jest.fn();
const { getByRole } = render(<RootWrapper />);

await waitFor(() => {
const viewLiveButton = getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage });
userEvent.click(viewLiveButton);
expect(window.open).toHaveBeenCalled();
const VIEW_LIVE_LINK = getConfig().LMS_BASE_URL + getUnitViewLivePath(courseId, blockId);
expect(window.open).toHaveBeenCalledWith(VIEW_LIVE_LINK, '_blank');

const previewButton = getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage });
userEvent.click(previewButton);
expect(window.open).toHaveBeenCalled();
const PREVIEW_LINK = getConfig().PREVIEW_BASE_URL + getUnitPreviewPath(courseId, sectionId, blockId);
expect(window.open).toHaveBeenCalledWith(PREVIEW_LINK, '_blank');
});

window.open = open;
});

it('checks courseUnit title changing when edit query is successfully', async () => {
const {
findByText, queryByRole, getByRole,
} = render(<RootWrapper />);
let editTitleButton = null;
let titleEditField = null;
const newDisplayName = `${unitDisplayName} new`;

axiosMock
.onPost(getXBlockBaseApiUrl(blockId, {
metadata: {
display_name: newDisplayName,
},
}))
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
metadata: {
...courseUnitIndexMock.metadata,
display_name: newDisplayName,
},
});

await waitFor(() => {
editTitleButton = getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
titleEditField = queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
});
expect(titleEditField).not.toBeInTheDocument();
fireEvent.click(editTitleButton);
titleEditField = getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
fireEvent.change(titleEditField, { target: { value: newDisplayName } });
await act(async () => {
fireEvent.blur(titleEditField);
});
expect(titleEditField).toHaveValue(newDisplayName);

titleEditField = queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
expect(titleEditField).not.toBeInTheDocument();
expect(await findByText(newDisplayName)).toBeInTheDocument();
});
});
Loading

0 comments on commit 3aa7257

Please sign in to comment.