refactor: Migration of course details to React query (#2724)
- Migrates the `courseDetails` part from the Redux Store to React Query. - Creates a new `CourseAuthoringContext` - Update the pages in `<CourseAuthoringRoutes>` to use the newly created context. - Migrates some files to Typescript - Migrates some tests to use `src/testUtils.tsx`
This commit is contained in:
@@ -4,21 +4,16 @@ import {
|
||||
getByRole,
|
||||
getAllByRole,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react';
|
||||
initializeMocks,
|
||||
} from 'CourseAuthoring/testUtils';
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Routes, Route, MemoryRouter } from 'react-router-dom';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider, PageWrap } from '@edx/frontend-platform/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import initializeStore from 'CourseAuthoring/store';
|
||||
import { executeThunk } from 'CourseAuthoring/utils';
|
||||
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
|
||||
|
||||
import { CourseAuthoringProvider } from 'CourseAuthoring/CourseAuthoringContext';
|
||||
import LiveSettings from './Settings';
|
||||
import {
|
||||
generateLiveConfigurationApiResponse,
|
||||
@@ -40,17 +35,20 @@ ReactDOM.createPortal = jest.fn(node => node);
|
||||
|
||||
const renderComponent = () => {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<PagesAndResourcesProvider courseId={courseId}>
|
||||
<MemoryRouter initialEntries={[liveSettingsUrl]}>
|
||||
<Routes>
|
||||
<Route path={liveSettingsUrl} element={<PageWrap><LiveSettings onClose={() => {}} /></PageWrap>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</PagesAndResourcesProvider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<PagesAndResourcesProvider courseId={courseId}>
|
||||
<LiveSettings onClose={() => {}} />
|
||||
</PagesAndResourcesProvider>
|
||||
</CourseAuthoringProvider>,
|
||||
{
|
||||
path: liveSettingsUrl,
|
||||
routerProps: {
|
||||
initialEntries: [liveSettingsUrl],
|
||||
},
|
||||
params: {
|
||||
courseId,
|
||||
},
|
||||
},
|
||||
);
|
||||
container = wrapper.container;
|
||||
};
|
||||
@@ -74,16 +72,9 @@ const mockStore = async ({
|
||||
|
||||
describe('BBB Settings', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore(initialState);
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
const mocks = initializeMocks({ initialState });
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
});
|
||||
|
||||
test('Plan dropdown to be visible and enabled in UI', async () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-m
|
||||
import { useModel } from 'CourseAuthoring/generic/model-store';
|
||||
import Loading from 'CourseAuthoring/generic/Loading';
|
||||
import { RequestStatus } from 'CourseAuthoring/data/constants';
|
||||
import { useCourseAuthoringContext } from 'CourseAuthoring/CourseAuthoringContext';
|
||||
|
||||
import { fetchLiveData, saveLiveConfiguration, saveLiveConfigurationAsDraft } from './data/thunks';
|
||||
import { selectApp } from './data/slice';
|
||||
@@ -25,7 +26,7 @@ const LiveSettings = ({
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const courseId = useSelector(state => state.courseDetail.courseId);
|
||||
const { courseId } = useCourseAuthoringContext();
|
||||
const availableProviders = useSelector((state) => state.live.appIds);
|
||||
const {
|
||||
piiSharingAllowed, selectedAppId, enabled, status,
|
||||
|
||||
@@ -8,20 +8,14 @@ import {
|
||||
queryByText,
|
||||
getByRole,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react';
|
||||
initializeMocks,
|
||||
} from 'CourseAuthoring/testUtils';
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Routes, Route, MemoryRouter } from 'react-router-dom';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider, PageWrap } from '@edx/frontend-platform/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import initializeStore from 'CourseAuthoring/store';
|
||||
import { executeThunk } from 'CourseAuthoring/utils';
|
||||
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
|
||||
|
||||
import { CourseAuthoringProvider } from 'CourseAuthoring/CourseAuthoringContext';
|
||||
import LiveSettings from './Settings';
|
||||
import {
|
||||
generateLiveConfigurationApiResponse,
|
||||
@@ -44,17 +38,20 @@ ReactDOM.createPortal = jest.fn(node => node);
|
||||
|
||||
const renderComponent = () => {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<PagesAndResourcesProvider courseId={courseId}>
|
||||
<MemoryRouter initialEntries={[liveSettingsUrl]}>
|
||||
<Routes>
|
||||
<Route path={liveSettingsUrl} element={<PageWrap><LiveSettings onClose={() => {}} /></PageWrap>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</PagesAndResourcesProvider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
<PagesAndResourcesProvider courseId={courseId}>
|
||||
<CourseAuthoringProvider>
|
||||
<LiveSettings onClose={() => {}} />
|
||||
</CourseAuthoringProvider>
|
||||
</PagesAndResourcesProvider>,
|
||||
{
|
||||
path: liveSettingsUrl,
|
||||
routerProps: {
|
||||
initialEntries: [liveSettingsUrl],
|
||||
},
|
||||
params: {
|
||||
courseId,
|
||||
},
|
||||
},
|
||||
);
|
||||
container = wrapper.container;
|
||||
};
|
||||
@@ -77,16 +74,11 @@ const mockStore = async ({
|
||||
|
||||
describe('LiveSettings', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
},
|
||||
const mocks = initializeMocks({
|
||||
initialState,
|
||||
});
|
||||
store = initializeStore(initialState);
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
});
|
||||
|
||||
test('Live Configuration modal is visible', async () => {
|
||||
|
||||
@@ -3,19 +3,14 @@ import {
|
||||
queryByTestId,
|
||||
getByRole,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react';
|
||||
initializeMocks,
|
||||
} from 'CourseAuthoring/testUtils';
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Routes, Route, MemoryRouter } from 'react-router-dom';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider, PageWrap } from '@edx/frontend-platform/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import initializeStore from 'CourseAuthoring/store';
|
||||
import { executeThunk } from 'CourseAuthoring/utils';
|
||||
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
|
||||
import { CourseAuthoringProvider } from 'CourseAuthoring/CourseAuthoringContext';
|
||||
import LiveSettings from './Settings';
|
||||
import {
|
||||
generateLiveConfigurationApiResponse,
|
||||
@@ -38,17 +33,20 @@ ReactDOM.createPortal = jest.fn(node => node);
|
||||
|
||||
const renderComponent = () => {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<PagesAndResourcesProvider courseId={courseId}>
|
||||
<MemoryRouter initialEntries={[liveSettingsUrl]}>
|
||||
<Routes>
|
||||
<Route path={liveSettingsUrl} element={<PageWrap><LiveSettings onClose={() => {}} /></PageWrap>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</PagesAndResourcesProvider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<PagesAndResourcesProvider courseId={courseId}>
|
||||
<LiveSettings onClose={() => {}} />
|
||||
</PagesAndResourcesProvider>
|
||||
</CourseAuthoringProvider>,
|
||||
{
|
||||
path: liveSettingsUrl,
|
||||
routerProps: {
|
||||
initialEntries: [liveSettingsUrl],
|
||||
},
|
||||
params: {
|
||||
courseId,
|
||||
},
|
||||
},
|
||||
);
|
||||
container = wrapper.container;
|
||||
};
|
||||
@@ -71,16 +69,9 @@ const mockStore = async ({
|
||||
|
||||
describe('Zoom Settings', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore(initialState);
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
const mocks = initializeMocks({ initialState });
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
});
|
||||
|
||||
test('LTI fields are visible when pii sharing is enabled', async () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useModel } from 'CourseAuthoring/generic/model-store';
|
||||
import PermissionDeniedAlert from 'CourseAuthoring/generic/PermissionDeniedAlert';
|
||||
import { useIsMobile } from 'CourseAuthoring/utils';
|
||||
import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
|
||||
import { useCourseAuthoringContext } from 'CourseAuthoring/CourseAuthoringContext';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -66,7 +67,7 @@ const ProctoringSettings = ({ onClose }) => {
|
||||
}
|
||||
|
||||
const { courseId } = useContext(PagesAndResourcesContext);
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const { courseDetails } = useCourseAuthoringContext();
|
||||
const org = courseDetails?.org;
|
||||
const appInfo = useModel('courseApps', 'proctoring');
|
||||
const alertRef = React.createRef();
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
render, screen, cleanup, waitFor, fireEvent, act,
|
||||
} from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
initializeMocks,
|
||||
} from 'CourseAuthoring/testUtils';
|
||||
|
||||
import { initializeMockApp, mergeConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
|
||||
import StudioApiService from 'CourseAuthoring/data/services/StudioApiService';
|
||||
import ExamsApiService from 'CourseAuthoring/data/services/ExamsApiService';
|
||||
import initializeStore from 'CourseAuthoring/store';
|
||||
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
|
||||
import { CourseAuthoringProvider } from 'CourseAuthoring/CourseAuthoringContext';
|
||||
import { getCourseDetailsUrl } from 'CourseAuthoring/data/api';
|
||||
import ProctoredExamSettings from './Settings';
|
||||
|
||||
const courseId = 'course-v1%3AedX%2BDemoX%2BDemo_Course';
|
||||
@@ -20,16 +17,13 @@ const defaultProps = {
|
||||
courseId,
|
||||
onClose: () => {},
|
||||
};
|
||||
let store;
|
||||
|
||||
const intlWrapper = children => (
|
||||
<AppProvider store={store}>
|
||||
const renderComponent = children => (
|
||||
<CourseAuthoringProvider courseId={defaultProps.courseId}>
|
||||
<PagesAndResourcesProvider courseId={defaultProps.courseId}>
|
||||
<IntlProvider locale="en">
|
||||
{children}
|
||||
</IntlProvider>
|
||||
{children}
|
||||
</PagesAndResourcesProvider>
|
||||
</AppProvider>
|
||||
</CourseAuthoringProvider>
|
||||
);
|
||||
let axiosMock;
|
||||
|
||||
@@ -38,38 +32,38 @@ describe('ProctoredExamSettings', () => {
|
||||
mergeConfig({
|
||||
EXAMS_BASE_URL: 'http://exams.testing.co',
|
||||
}, 'CourseAuthoringConfig');
|
||||
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: isAdmin,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore({
|
||||
models: {
|
||||
courseApps: {
|
||||
proctoring: {},
|
||||
},
|
||||
courseDetails: {
|
||||
[courseId]: {
|
||||
start: Date(),
|
||||
const user = {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: isAdmin,
|
||||
roles: [],
|
||||
};
|
||||
const mocks = initializeMocks({
|
||||
user,
|
||||
initialState: {
|
||||
models: {
|
||||
courseApps: {
|
||||
proctoring: {},
|
||||
},
|
||||
},
|
||||
...(org ? { courseDetails: { [courseId]: { org } } } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(
|
||||
`${ExamsApiService.getExamsBaseUrl()}/api/v1/providers${org ? `?org=${org}` : ''}`,
|
||||
).reply(200, [
|
||||
{
|
||||
name: 'test_lti',
|
||||
verbose_name: 'LTI Provider',
|
||||
},
|
||||
]);
|
||||
axiosMock = mocks.axiosMock;
|
||||
axiosMock
|
||||
.onGet(getCourseDetailsUrl(courseId, user.username))
|
||||
.reply(200, {
|
||||
courseId,
|
||||
name: 'Course Test',
|
||||
start: Date(),
|
||||
...(org ? { org } : {}),
|
||||
});
|
||||
axiosMock.onGet(`${ExamsApiService.getExamsBaseUrl()}/api/v1/providers`)
|
||||
.reply(200, [{ name: 'test_lti', verbose_name: 'LTI Provider' }]);
|
||||
if (org) {
|
||||
axiosMock.onGet(`${ExamsApiService.getExamsBaseUrl()}/api/v1/providers?org=${org}`)
|
||||
.reply(200, [{ name: 'test_lti', verbose_name: 'LTI Provider' }]);
|
||||
}
|
||||
axiosMock.onGet(
|
||||
`${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`,
|
||||
).reply(200, {
|
||||
@@ -92,17 +86,13 @@ describe('ProctoredExamSettings', () => {
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
axiosMock.reset();
|
||||
});
|
||||
beforeEach(async () => {
|
||||
setupApp();
|
||||
});
|
||||
|
||||
describe('Field dependencies', () => {
|
||||
beforeEach(async () => {
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
});
|
||||
|
||||
it('Updates Zendesk ticket field if software_secure is provider', async () => {
|
||||
@@ -153,7 +143,7 @@ describe('ProctoredExamSettings', () => {
|
||||
course_start_date: '2070-01-01T00:00:00Z',
|
||||
});
|
||||
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await waitFor(() => {
|
||||
screen.getByText('Proctored exams');
|
||||
});
|
||||
@@ -234,7 +224,7 @@ describe('ProctoredExamSettings', () => {
|
||||
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
|
||||
).reply(200, {});
|
||||
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
});
|
||||
|
||||
proctoringProvidersRequiringEscalationEmail.forEach(provider => {
|
||||
@@ -420,7 +410,7 @@ describe('ProctoredExamSettings', () => {
|
||||
const isAdmin = false;
|
||||
setupApp(isAdmin);
|
||||
mockCourseData(mockGetPastCourseData);
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
const providerOption = screen.getByTestId('software_secure');
|
||||
expect(providerOption.hasAttribute('disabled')).toEqual(true);
|
||||
});
|
||||
@@ -429,7 +419,7 @@ describe('ProctoredExamSettings', () => {
|
||||
const isAdmin = false;
|
||||
setupApp(isAdmin);
|
||||
mockCourseData(mockGetFutureCourseData);
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
const providerOption = screen.getByTestId('software_secure');
|
||||
expect(providerOption.hasAttribute('disabled')).toEqual(false);
|
||||
});
|
||||
@@ -439,7 +429,7 @@ describe('ProctoredExamSettings', () => {
|
||||
const org = 'test-org';
|
||||
setupApp(isAdmin, org);
|
||||
mockCourseData(mockGetFutureCourseData);
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
const providerOption = screen.getByTestId('software_secure');
|
||||
expect(providerOption.hasAttribute('disabled')).toEqual(false);
|
||||
});
|
||||
@@ -448,7 +438,7 @@ describe('ProctoredExamSettings', () => {
|
||||
const isAdmin = true;
|
||||
setupApp(isAdmin);
|
||||
mockCourseData(mockGetPastCourseData);
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
const providerOption = screen.getByTestId('software_secure');
|
||||
expect(providerOption.hasAttribute('disabled')).toEqual(false);
|
||||
});
|
||||
@@ -457,7 +447,7 @@ describe('ProctoredExamSettings', () => {
|
||||
const isAdmin = true;
|
||||
setupApp(isAdmin);
|
||||
mockCourseData(mockGetFutureCourseData);
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
const providerOption = screen.getByTestId('software_secure');
|
||||
expect(providerOption.hasAttribute('disabled')).toEqual(false);
|
||||
});
|
||||
@@ -468,7 +458,7 @@ describe('ProctoredExamSettings', () => {
|
||||
available_proctoring_providers: ['lti_external', 'mockproc'],
|
||||
};
|
||||
mockCourseData(courseData);
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await waitFor(() => {
|
||||
screen.getByDisplayValue('mockproc');
|
||||
});
|
||||
@@ -481,7 +471,7 @@ describe('ProctoredExamSettings', () => {
|
||||
available_proctoring_providers: ['lti_external', 'mockproc'],
|
||||
};
|
||||
mockCourseData(courseData);
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await waitFor(() => {
|
||||
screen.getByDisplayValue('mockproc');
|
||||
});
|
||||
@@ -494,7 +484,7 @@ describe('ProctoredExamSettings', () => {
|
||||
const isAdmin = true;
|
||||
setupApp(isAdmin);
|
||||
mockCourseData(mockGetFutureCourseData);
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await waitFor(() => {
|
||||
screen.getByDisplayValue('mockproc');
|
||||
});
|
||||
@@ -508,12 +498,13 @@ describe('ProctoredExamSettings', () => {
|
||||
EXAMS_BASE_URL: null,
|
||||
}, 'CourseAuthoringConfig');
|
||||
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await waitFor(() => {
|
||||
screen.getByDisplayValue('mockproc');
|
||||
});
|
||||
// only outgoing request should be for studio settings
|
||||
expect(axiosMock.history.get.length).toBe(1);
|
||||
// (1) for studio settings
|
||||
// (2) for course details
|
||||
expect(axiosMock.history.get.length).toBe(2);
|
||||
expect(axiosMock.history.get[0].url.includes('proctored_exam_settings')).toEqual(true);
|
||||
});
|
||||
|
||||
@@ -527,7 +518,7 @@ describe('ProctoredExamSettings', () => {
|
||||
).reply(200, {
|
||||
provider: 'test_lti',
|
||||
});
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await waitFor(() => {
|
||||
screen.getByText('Proctoring provider');
|
||||
});
|
||||
@@ -540,14 +531,14 @@ describe('ProctoredExamSettings', () => {
|
||||
describe('Toggles field visibility based on user permissions', () => {
|
||||
it('Hides opting out and zendesk tickets for non edX staff', async () => {
|
||||
setupApp(false);
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
expect(screen.queryByTestId('allowOptingOutYes')).toBeNull();
|
||||
expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull();
|
||||
});
|
||||
|
||||
it('Shows opting out and zendesk tickets for edX staff', async () => {
|
||||
setupApp(true);
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
expect(screen.queryByTestId('allowOptingOutYes')).not.toBeNull();
|
||||
expect(screen.queryByTestId('createZendeskTicketsYes')).not.toBeNull();
|
||||
});
|
||||
@@ -555,7 +546,7 @@ describe('ProctoredExamSettings', () => {
|
||||
|
||||
describe('Connection states', () => {
|
||||
it('Shows the spinner before the connection is complete', async () => {
|
||||
render(intlWrapper(<ProctoredExamSettings {...defaultProps} />));
|
||||
render(renderComponent(<ProctoredExamSettings {...defaultProps} />));
|
||||
const spinner = await screen.findByRole('status');
|
||||
expect(spinner.textContent).toEqual('Loading...');
|
||||
});
|
||||
@@ -565,7 +556,7 @@ describe('ProctoredExamSettings', () => {
|
||||
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
|
||||
).reply(500);
|
||||
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
const connectionError = screen.getByTestId('connectionErrorAlert');
|
||||
expect(connectionError.textContent).toEqual(
|
||||
expect.stringContaining('We encountered a technical error when loading this page.'),
|
||||
@@ -577,7 +568,7 @@ describe('ProctoredExamSettings', () => {
|
||||
`${ExamsApiService.getExamsBaseUrl()}/api/v1/providers`,
|
||||
).reply(500);
|
||||
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
const connectionError = screen.getByTestId('connectionErrorAlert');
|
||||
expect(connectionError.textContent).toEqual(
|
||||
expect.stringContaining('We encountered a technical error when loading this page.'),
|
||||
@@ -589,7 +580,7 @@ describe('ProctoredExamSettings', () => {
|
||||
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
|
||||
).reply(403);
|
||||
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
const permissionError = screen.getByTestId('permissionDeniedAlert');
|
||||
expect(permissionError.textContent).toEqual(
|
||||
expect.stringContaining('You are not authorized to view this page'),
|
||||
@@ -608,7 +599,7 @@ describe('ProctoredExamSettings', () => {
|
||||
});
|
||||
|
||||
it('Disable button while submitting', async () => {
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
let submitButton = screen.getByTestId('submissionButton');
|
||||
expect(screen.queryByTestId('saveInProgress')).toBeFalsy();
|
||||
fireEvent.click(submitButton);
|
||||
@@ -634,7 +625,7 @@ describe('ProctoredExamSettings', () => {
|
||||
course_start_date: '2070-01-01T00:00:00Z',
|
||||
});
|
||||
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
// Make a change to the provider to test_lti and set the email
|
||||
const selectElement = screen.getByDisplayValue('mockproc');
|
||||
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
|
||||
@@ -670,7 +661,7 @@ describe('ProctoredExamSettings', () => {
|
||||
});
|
||||
|
||||
it('Makes API call successfully without proctoring_escalation_email if not requiring escalation email', async () => {
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
|
||||
// make sure we have not selected a provider requiring escalation email
|
||||
expect(screen.getByDisplayValue('mockproc')).toBeDefined();
|
||||
@@ -697,7 +688,7 @@ describe('ProctoredExamSettings', () => {
|
||||
});
|
||||
|
||||
it('Successfully updates exam configuration and studio provider is set to "lti_external" for lti providers', async () => {
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
// Make a change to the provider to test_lti and set the email
|
||||
const selectElement = screen.getByDisplayValue('mockproc');
|
||||
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
|
||||
@@ -739,7 +730,7 @@ describe('ProctoredExamSettings', () => {
|
||||
});
|
||||
|
||||
it('Sets exam service provider to null if a non-lti provider is selected', async () => {
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
const submitButton = screen.getByTestId('submissionButton');
|
||||
fireEvent.click(submitButton);
|
||||
// update exam service config
|
||||
@@ -784,7 +775,7 @@ describe('ProctoredExamSettings', () => {
|
||||
course_start_date: '2070-01-01T00:00:00Z',
|
||||
});
|
||||
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
const submitButton = screen.getByTestId('submissionButton');
|
||||
fireEvent.click(submitButton);
|
||||
// does not update exam service config
|
||||
@@ -814,7 +805,7 @@ describe('ProctoredExamSettings', () => {
|
||||
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
|
||||
).reply(500);
|
||||
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
const submitButton = screen.getByTestId('submissionButton');
|
||||
fireEvent.click(submitButton);
|
||||
expect(axiosMock.history.post.length).toBe(1);
|
||||
@@ -832,7 +823,7 @@ describe('ProctoredExamSettings', () => {
|
||||
`${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`,
|
||||
).reply(500, 'error');
|
||||
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
const submitButton = screen.getByTestId('submissionButton');
|
||||
fireEvent.click(submitButton);
|
||||
expect(axiosMock.history.post.length).toBe(1);
|
||||
@@ -850,7 +841,7 @@ describe('ProctoredExamSettings', () => {
|
||||
`${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`,
|
||||
).reply(403, 'error');
|
||||
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
const submitButton = screen.getByTestId('submissionButton');
|
||||
fireEvent.click(submitButton);
|
||||
expect(axiosMock.history.post.length).toBe(1);
|
||||
@@ -869,7 +860,7 @@ describe('ProctoredExamSettings', () => {
|
||||
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
|
||||
).reply(500);
|
||||
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
const submitButton = screen.getByTestId('submissionButton');
|
||||
fireEvent.click(submitButton);
|
||||
expect(axiosMock.history.post.length).toBe(1);
|
||||
@@ -902,7 +893,7 @@ describe('ProctoredExamSettings', () => {
|
||||
const isAdmin = false;
|
||||
setupApp(isAdmin);
|
||||
|
||||
await act(async () => render(intlWrapper(<ProctoredExamSettings {...defaultProps} />)));
|
||||
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
|
||||
// Make a change to the proctoring provider
|
||||
const selectElement = screen.getByDisplayValue('mockproc');
|
||||
fireEvent.change(selectElement, { target: { value: 'software_secure' } });
|
||||
|
||||
66
src/CourseAuthoringContext.tsx
Normal file
66
src/CourseAuthoringContext.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { createContext, useContext, useMemo } from 'react';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { CourseDetailsData } from './data/api';
|
||||
import { useCourseDetails } from './data/apiHooks';
|
||||
import { RequestStatusType } from './data/constants';
|
||||
|
||||
export type CourseAuthoringContextData = {
|
||||
/** The ID of the current course */
|
||||
courseId: string;
|
||||
courseDetails?: CourseDetailsData;
|
||||
courseDetailStatus: RequestStatusType;
|
||||
canChangeProviders: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Course Authoring Context.
|
||||
* Always available when we're in the context of a single course.
|
||||
*
|
||||
* Get this using `useCourseAuthoringContext()`
|
||||
*
|
||||
*/
|
||||
const CourseAuthoringContext = createContext<CourseAuthoringContextData | undefined>(undefined);
|
||||
|
||||
type CourseAuthoringProviderProps = {
|
||||
children?: React.ReactNode;
|
||||
courseId: string;
|
||||
};
|
||||
|
||||
export const CourseAuthoringProvider = ({
|
||||
children,
|
||||
courseId,
|
||||
}: CourseAuthoringProviderProps) => {
|
||||
const { data: courseDetails, status: courseDetailStatus } = useCourseDetails(courseId);
|
||||
const canChangeProviders = getAuthenticatedUser().administrator || new Date(courseDetails?.start ?? 0) > new Date();
|
||||
|
||||
const context = useMemo<CourseAuthoringContextData>(() => {
|
||||
const contextValue = {
|
||||
courseId,
|
||||
courseDetails,
|
||||
courseDetailStatus,
|
||||
canChangeProviders,
|
||||
};
|
||||
|
||||
return contextValue;
|
||||
}, [
|
||||
courseId,
|
||||
courseDetails,
|
||||
courseDetailStatus,
|
||||
canChangeProviders,
|
||||
]);
|
||||
|
||||
return (
|
||||
<CourseAuthoringContext.Provider value={context}>
|
||||
{children}
|
||||
</CourseAuthoringContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useCourseAuthoringContext(): CourseAuthoringContextData {
|
||||
const ctx = useContext(CourseAuthoringContext);
|
||||
if (ctx === undefined) {
|
||||
/* istanbul ignore next */
|
||||
throw new Error('useCourseAuthoringContext() was used in a component without a <CourseAuthoringProvider> ancestor.');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import CourseAuthoringPage from './CourseAuthoringPage';
|
||||
import PagesAndResources from './pages-and-resources/PagesAndResources';
|
||||
import { executeThunk } from './utils';
|
||||
import { fetchCourseApps } from './pages-and-resources/data/thunks';
|
||||
import { fetchCourseDetail } from './data/thunks';
|
||||
import { getApiWaffleFlagsUrl } from './data/api';
|
||||
import { initializeMocks, render } from './testUtils';
|
||||
import { CourseAuthoringProvider } from './CourseAuthoringContext';
|
||||
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
let mockPathname = '/evilguy/';
|
||||
@@ -19,6 +19,12 @@ jest.mock('react-router-dom', () => ({
|
||||
let axiosMock;
|
||||
let store;
|
||||
|
||||
const renderComponent = children => render(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
{children}
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
|
||||
beforeEach(async () => {
|
||||
const mocks = initializeMocks();
|
||||
store = mocks.reduxStore;
|
||||
@@ -35,14 +41,13 @@ describe('Editor Pages Load no header', () => {
|
||||
axiosMock.onGet(`${courseAppsApiUrl}/${courseId}`).reply(200, {
|
||||
response: { status: 200 },
|
||||
});
|
||||
await executeThunk(fetchCourseApps(courseId), store.dispatch);
|
||||
};
|
||||
test('renders no loading wheel on editor pages', async () => {
|
||||
mockPathname = '/editor/';
|
||||
await mockStoreSuccess();
|
||||
const wrapper = render(
|
||||
<CourseAuthoringPage courseId={courseId}>
|
||||
<PagesAndResources courseId={courseId} />
|
||||
const wrapper = renderComponent(
|
||||
<CourseAuthoringPage>
|
||||
<PagesAndResources />
|
||||
</CourseAuthoringPage>
|
||||
,
|
||||
);
|
||||
@@ -51,9 +56,9 @@ describe('Editor Pages Load no header', () => {
|
||||
test('renders loading wheel on non editor pages', async () => {
|
||||
mockPathname = '/evilguy/';
|
||||
await mockStoreSuccess();
|
||||
const wrapper = render(
|
||||
<CourseAuthoringPage courseId={courseId}>
|
||||
<PagesAndResources courseId={courseId} />
|
||||
const wrapper = renderComponent(
|
||||
<CourseAuthoringPage>
|
||||
<PagesAndResources />
|
||||
</CourseAuthoringPage>
|
||||
,
|
||||
);
|
||||
@@ -70,7 +75,6 @@ describe('Course authoring page', () => {
|
||||
).reply(404, {
|
||||
response: { status: 404 },
|
||||
});
|
||||
await executeThunk(fetchCourseDetail(courseId), store.dispatch);
|
||||
};
|
||||
const mockStoreError = async () => {
|
||||
axiosMock.onGet(
|
||||
@@ -78,11 +82,10 @@ describe('Course authoring page', () => {
|
||||
).reply(500, {
|
||||
response: { status: 500 },
|
||||
});
|
||||
await executeThunk(fetchCourseDetail(courseId), store.dispatch);
|
||||
};
|
||||
test('renders not found page on non-existent course key', async () => {
|
||||
await mockStoreNotFound();
|
||||
const wrapper = render(<CourseAuthoringPage courseId={courseId} />);
|
||||
const wrapper = renderComponent(<CourseAuthoringPage />);
|
||||
expect(await wrapper.findByTestId('notFoundAlert')).toBeInTheDocument();
|
||||
});
|
||||
test('does not render not found page on other kinds of error', async () => {
|
||||
@@ -92,8 +95,8 @@ describe('Course authoring page', () => {
|
||||
// IN_PROGRESS but also not NOT_FOUND or DENIED- then check that the not
|
||||
// found alert is not present.
|
||||
const contentTestId = 'courseAuthoringPageContent';
|
||||
const wrapper = render(
|
||||
<CourseAuthoringPage courseId={courseId}>
|
||||
const wrapper = renderComponent(
|
||||
<CourseAuthoringPage>
|
||||
<div data-testid={contentTestId} />
|
||||
</CourseAuthoringPage>
|
||||
,
|
||||
@@ -114,7 +117,7 @@ describe('Course authoring page', () => {
|
||||
mockPathname = '/editor/';
|
||||
await mockStoreDenied();
|
||||
|
||||
const wrapper = render(<CourseAuthoringPage courseId={courseId} />);
|
||||
const wrapper = renderComponent(<CourseAuthoringPage />);
|
||||
expect(await wrapper.findByTestId('permissionDeniedAlert')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
@@ -7,34 +6,31 @@ import {
|
||||
} from 'react-router-dom';
|
||||
import { StudioFooterSlot } from '@edx/frontend-component-footer';
|
||||
import Header from './header';
|
||||
import { fetchCourseDetail } from './data/thunks';
|
||||
import { useModel } from './generic/model-store';
|
||||
import NotFoundAlert from './generic/NotFoundAlert';
|
||||
import PermissionDeniedAlert from './generic/PermissionDeniedAlert';
|
||||
import { fetchOnlyStudioHomeData } from './studio-home/data/thunks';
|
||||
import { getCourseAppsApiStatus } from './pages-and-resources/data/selectors';
|
||||
import { RequestStatus } from './data/constants';
|
||||
import Loading from './generic/Loading';
|
||||
import { useCourseAuthoringContext } from './CourseAuthoringContext';
|
||||
|
||||
const CourseAuthoringPage = ({ courseId, children }) => {
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const CourseAuthoringPage = ({ children }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseDetail(courseId));
|
||||
}, [courseId]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchOnlyStudioHomeData());
|
||||
}, []);
|
||||
|
||||
const courseDetail = useModel('courseDetails', courseId);
|
||||
|
||||
const courseNumber = courseDetail ? courseDetail.number : null;
|
||||
const courseOrg = courseDetail ? courseDetail.org : null;
|
||||
const courseTitle = courseDetail ? courseDetail.name : courseId;
|
||||
const { courseId, courseDetails, courseDetailStatus } = useCourseAuthoringContext();
|
||||
const courseNumber = courseDetails?.number;
|
||||
const courseOrg = courseDetails?.org;
|
||||
const courseTitle = courseDetails?.name;
|
||||
const inProgress = courseDetailStatus === RequestStatus.IN_PROGRESS || courseDetailStatus === RequestStatus.PENDING;
|
||||
const courseAppsApiStatus = useSelector(getCourseAppsApiStatus);
|
||||
const courseDetailStatus = useSelector(state => state.courseDetail.status);
|
||||
const inProgress = courseDetailStatus === RequestStatus.IN_PROGRESS;
|
||||
const { pathname } = useLocation();
|
||||
const isEditor = pathname.includes('/editor');
|
||||
|
||||
@@ -70,13 +66,4 @@ const CourseAuthoringPage = ({ courseId, children }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CourseAuthoringPage.propTypes = {
|
||||
children: PropTypes.node,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
CourseAuthoringPage.defaultProps = {
|
||||
children: null,
|
||||
};
|
||||
|
||||
export default CourseAuthoringPage;
|
||||
@@ -1,153 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Navigate, Routes, Route, useParams,
|
||||
} from 'react-router-dom';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { PageWrap } from '@edx/frontend-platform/react';
|
||||
import { Textbooks } from './textbooks';
|
||||
import CourseAuthoringPage from './CourseAuthoringPage';
|
||||
import { PagesAndResources } from './pages-and-resources';
|
||||
import EditorContainer from './editors/EditorContainer';
|
||||
import VideoSelectorContainer from './selectors/VideoSelectorContainer';
|
||||
import CustomPages from './custom-pages';
|
||||
import { FilesPage, VideosPage } from './files-and-videos';
|
||||
import { AdvancedSettings } from './advanced-settings';
|
||||
import { CourseOutline } from './course-outline';
|
||||
import ScheduleAndDetails from './schedule-and-details';
|
||||
import { GradingSettings } from './grading-settings';
|
||||
import CourseTeam from './course-team/CourseTeam';
|
||||
import { CourseUpdates } from './course-updates';
|
||||
import { CourseUnit, SubsectionUnitRedirect } from './course-unit';
|
||||
import { Certificates } from './certificates';
|
||||
import CourseExportPage from './export-page/CourseExportPage';
|
||||
import CourseOptimizerPage from './optimizer-page/CourseOptimizerPage';
|
||||
import CourseImportPage from './import-page/CourseImportPage';
|
||||
import { DECODED_ROUTES } from './constants';
|
||||
import CourseChecklist from './course-checklist';
|
||||
import GroupConfigurations from './group-configurations';
|
||||
import { CourseLibraries } from './course-libraries';
|
||||
import { IframeProvider } from './generic/hooks/context/iFrameContext';
|
||||
|
||||
/**
|
||||
* As of this writing, these routes are mounted at a path prefixed with the following:
|
||||
*
|
||||
* /course/:courseId
|
||||
*
|
||||
* Meaning that their absolute paths look like:
|
||||
*
|
||||
* /course/:courseId/course-pages
|
||||
* /course/:courseId/proctored-exam-settings
|
||||
* /course/:courseId/editor/:blockType/:blockId
|
||||
*
|
||||
* This component and CourseAuthoringPage should maybe be combined once we no longer need to have
|
||||
* CourseAuthoringPage split out for use in LegacyProctoringRoute. Once that route is removed, we
|
||||
* can move the Header/Footer rendering to this component and likely pull the course detail loading
|
||||
* in as well, and it'd feel a bit better-factored and the roles would feel more clear.
|
||||
*/
|
||||
const CourseAuthoringRoutes = () => {
|
||||
const { courseId } = useParams();
|
||||
|
||||
return (
|
||||
<CourseAuthoringPage courseId={courseId}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={<PageWrap><CourseOutline courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="course_info"
|
||||
element={<PageWrap><CourseUpdates courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="libraries"
|
||||
element={<PageWrap><CourseLibraries courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="assets"
|
||||
element={<PageWrap><FilesPage courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="videos"
|
||||
element={getConfig().ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN === 'true' ? <PageWrap><VideosPage courseId={courseId} /></PageWrap> : null}
|
||||
/>
|
||||
<Route
|
||||
path="pages-and-resources/*"
|
||||
element={<PageWrap><PagesAndResources courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="proctored-exam-settings"
|
||||
element={<Navigate replace to={`/course/${courseId}/pages-and-resources`} />}
|
||||
/>
|
||||
<Route
|
||||
path="custom-pages/*"
|
||||
element={<PageWrap><CustomPages courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="/subsection/:subsectionId"
|
||||
element={<PageWrap><SubsectionUnitRedirect courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
{DECODED_ROUTES.COURSE_UNIT.map((path) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
element={<PageWrap><IframeProvider><CourseUnit courseId={courseId} /></IframeProvider></PageWrap>}
|
||||
/>
|
||||
))}
|
||||
<Route
|
||||
path="editor/course-videos/:blockId"
|
||||
element={<PageWrap><VideoSelectorContainer courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="editor/:blockType/:blockId?"
|
||||
element={<PageWrap><EditorContainer learningContextId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="settings/details"
|
||||
element={<PageWrap><ScheduleAndDetails courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="settings/grading"
|
||||
element={<PageWrap><GradingSettings courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="course_team"
|
||||
element={<PageWrap><CourseTeam courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="group_configurations"
|
||||
element={<PageWrap><GroupConfigurations courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="settings/advanced"
|
||||
element={<PageWrap><AdvancedSettings courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="import"
|
||||
element={<PageWrap><CourseImportPage courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="export"
|
||||
element={<PageWrap><CourseExportPage courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="optimizer"
|
||||
element={<PageWrap><CourseOptimizerPage courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="checklists"
|
||||
element={<PageWrap><CourseChecklist courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="certificates"
|
||||
element={getConfig().ENABLE_CERTIFICATE_PAGE === 'true' ? <PageWrap><Certificates courseId={courseId} /></PageWrap> : null}
|
||||
/>
|
||||
<Route
|
||||
path="textbooks"
|
||||
element={<PageWrap><Textbooks courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
</Routes>
|
||||
</CourseAuthoringPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseAuthoringRoutes;
|
||||
@@ -48,7 +48,11 @@ jest.mock('./custom-pages/CustomPages', () => (props) => {
|
||||
|
||||
describe('<CourseAuthoringRoutes>', () => {
|
||||
beforeEach(async () => {
|
||||
const { axiosMock } = initializeMocks();
|
||||
const user = {
|
||||
userId: 1,
|
||||
username: 'username',
|
||||
};
|
||||
const { axiosMock } = initializeMocks({ user });
|
||||
axiosMock
|
||||
.onGet(getApiWaffleFlagsUrl(courseId))
|
||||
.reply(200, {});
|
||||
@@ -61,11 +65,7 @@ describe('<CourseAuthoringRoutes>', () => {
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(pagesAndResourcesMockText)).toBeVisible();
|
||||
expect(mockComponentFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
courseId,
|
||||
}),
|
||||
);
|
||||
expect(mockComponentFn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,11 +93,7 @@ describe('<CourseAuthoringRoutes>', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(videoSelectorContainerMockText)).toBeInTheDocument();
|
||||
expect(screen.queryByText(pagesAndResourcesMockText)).not.toBeInTheDocument();
|
||||
expect(mockComponentFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
courseId,
|
||||
}),
|
||||
);
|
||||
expect(mockComponentFn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
160
src/CourseAuthoringRoutes.tsx
Normal file
160
src/CourseAuthoringRoutes.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import {
|
||||
Navigate, Routes, Route, useParams,
|
||||
} from 'react-router-dom';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { PageWrap } from '@edx/frontend-platform/react';
|
||||
import { Textbooks } from './textbooks';
|
||||
import CourseAuthoringPage from './CourseAuthoringPage';
|
||||
import { PagesAndResources } from './pages-and-resources';
|
||||
import EditorContainer from './editors/EditorContainer';
|
||||
import VideoSelectorContainer from './selectors/VideoSelectorContainer';
|
||||
import CustomPages from './custom-pages';
|
||||
import { FilesPage, VideosPage } from './files-and-videos';
|
||||
import { AdvancedSettings } from './advanced-settings';
|
||||
import { CourseOutline } from './course-outline';
|
||||
import ScheduleAndDetails from './schedule-and-details';
|
||||
import { GradingSettings } from './grading-settings';
|
||||
import CourseTeam from './course-team/CourseTeam';
|
||||
import { CourseUpdates } from './course-updates';
|
||||
import { CourseUnit, SubsectionUnitRedirect } from './course-unit';
|
||||
import { Certificates } from './certificates';
|
||||
import CourseExportPage from './export-page/CourseExportPage';
|
||||
import CourseOptimizerPage from './optimizer-page/CourseOptimizerPage';
|
||||
import CourseImportPage from './import-page/CourseImportPage';
|
||||
import { DECODED_ROUTES } from './constants';
|
||||
import CourseChecklist from './course-checklist';
|
||||
import GroupConfigurations from './group-configurations';
|
||||
import { CourseLibraries } from './course-libraries';
|
||||
import { IframeProvider } from './generic/hooks/context/iFrameContext';
|
||||
import { CourseAuthoringProvider } from './CourseAuthoringContext';
|
||||
|
||||
/**
|
||||
* As of this writing, these routes are mounted at a path prefixed with the following:
|
||||
*
|
||||
* /course/:courseId
|
||||
*
|
||||
* Meaning that their absolute paths look like:
|
||||
*
|
||||
* /course/:courseId/course-pages
|
||||
* /course/:courseId/proctored-exam-settings
|
||||
* /course/:courseId/editor/:blockType/:blockId
|
||||
*
|
||||
* This component and CourseAuthoringPage should maybe be combined once we no longer need to have
|
||||
* CourseAuthoringPage split out for use in LegacyProctoringRoute. Once that route is removed, we
|
||||
* can move the Header/Footer rendering to this component and likely pull the course detail loading
|
||||
* in as well, and it'd feel a bit better-factored and the roles would feel more clear.
|
||||
*/
|
||||
const CourseAuthoringRoutes = () => {
|
||||
const { courseId } = useParams();
|
||||
|
||||
if (courseId === undefined) {
|
||||
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
|
||||
throw new Error('Error: route is missing courseId.');
|
||||
}
|
||||
|
||||
return (
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<CourseAuthoringPage>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={<PageWrap><CourseOutline /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="course_info"
|
||||
element={<PageWrap><CourseUpdates /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="libraries"
|
||||
element={<PageWrap><CourseLibraries /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="assets"
|
||||
element={<PageWrap><FilesPage /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="videos"
|
||||
element={getConfig().ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN === 'true' ? <PageWrap><VideosPage /></PageWrap> : null}
|
||||
/>
|
||||
<Route
|
||||
path="pages-and-resources/*"
|
||||
element={<PageWrap><PagesAndResources /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="proctored-exam-settings"
|
||||
element={<Navigate replace to={`/course/${courseId}/pages-and-resources`} />}
|
||||
/>
|
||||
<Route
|
||||
path="custom-pages/*"
|
||||
element={<PageWrap><CustomPages /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="/subsection/:subsectionId"
|
||||
element={<PageWrap><SubsectionUnitRedirect /></PageWrap>}
|
||||
/>
|
||||
{DECODED_ROUTES.COURSE_UNIT.map((path) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
element={<PageWrap><IframeProvider><CourseUnit /></IframeProvider></PageWrap>}
|
||||
/>
|
||||
))}
|
||||
<Route
|
||||
path="editor/course-videos/:blockId"
|
||||
element={<PageWrap><VideoSelectorContainer /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="editor/:blockType/:blockId?"
|
||||
element={<PageWrap><EditorContainer learningContextId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="settings/details"
|
||||
element={<PageWrap><ScheduleAndDetails /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="settings/grading"
|
||||
element={<PageWrap><GradingSettings /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="course_team"
|
||||
element={<PageWrap><CourseTeam /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="group_configurations"
|
||||
element={<PageWrap><GroupConfigurations /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="settings/advanced"
|
||||
element={<PageWrap><AdvancedSettings /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="import"
|
||||
element={<PageWrap><CourseImportPage /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="export"
|
||||
element={<PageWrap><CourseExportPage /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="optimizer"
|
||||
element={<PageWrap><CourseOptimizerPage /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="checklists"
|
||||
element={<PageWrap><CourseChecklist /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="certificates"
|
||||
element={getConfig().ENABLE_CERTIFICATE_PAGE === 'true' ? <PageWrap><Certificates /></PageWrap> : null}
|
||||
/>
|
||||
<Route
|
||||
path="textbooks"
|
||||
element={<PageWrap><Textbooks /></PageWrap>}
|
||||
/>
|
||||
</Routes>
|
||||
</CourseAuthoringPage>
|
||||
</CourseAuthoringProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseAuthoringRoutes;
|
||||
@@ -1,15 +1,14 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
Container, Button, Layout, StatefulButton, TransitionReplace,
|
||||
} from '@openedx/paragon';
|
||||
import { CheckCircle, Info, Warning } from '@openedx/paragon/icons';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import Placeholder from '../editors/Placeholder';
|
||||
|
||||
import AlertProctoringError from '../generic/AlertProctoringError';
|
||||
import { useModel } from '../generic/model-store';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import { parseArrayOrObjectValues } from '../utils';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
@@ -26,7 +25,7 @@ import messages from './messages';
|
||||
import ModalError from './modal-error/ModalError';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
|
||||
const AdvancedSettings = ({ courseId }) => {
|
||||
const AdvancedSettings = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const [saveSettingsPrompt, showSaveSettingsPrompt] = useState(false);
|
||||
@@ -39,7 +38,7 @@ const AdvancedSettings = ({ courseId }) => {
|
||||
const [isEditableState, setIsEditableState] = useState(false);
|
||||
const [hasInternetConnectionError, setInternetConnectionError] = useState(false);
|
||||
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
||||
|
||||
useEffect(() => {
|
||||
@@ -278,8 +277,4 @@ const AdvancedSettings = ({ courseId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
AdvancedSettings.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default AdvancedSettings;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import {
|
||||
render as baseRender,
|
||||
fireEvent,
|
||||
@@ -26,7 +27,9 @@ jest.mock('react-textarea-autosize', () => jest.fn((props) => (
|
||||
)));
|
||||
|
||||
const render = () => baseRender(
|
||||
<AdvancedSettings courseId={courseId} />,
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<AdvancedSettings />
|
||||
</CourseAuthoringProvider>,
|
||||
{ path: mockPathname },
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-check
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { initializeMocks, render, waitFor } from '../testUtils';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { executeThunk } from '../utils';
|
||||
@@ -14,7 +15,11 @@ let axiosMock;
|
||||
let store;
|
||||
const courseId = 'course-123';
|
||||
|
||||
const renderComponent = (props) => render(<Certificates courseId={courseId} {...props} />);
|
||||
const renderComponent = (props) => render(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<Certificates {...props} />
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
|
||||
describe('Certificates', () => {
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Helmet } from 'react-helmet';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import Placeholder from '../editors/Placeholder';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import Loading from '../generic/Loading';
|
||||
@@ -21,7 +21,8 @@ const MODE_COMPONENTS = {
|
||||
[MODE_STATES.editAll]: CertificateEditForm,
|
||||
};
|
||||
|
||||
const Certificates = ({ courseId }) => {
|
||||
const Certificates = () => {
|
||||
const { courseId } = useCourseAuthoringContext();
|
||||
const {
|
||||
certificates, componentMode, isLoading, loadingStatus, pageHeadTitle, hasCertificateModes,
|
||||
} = useCertificates({ courseId });
|
||||
@@ -50,8 +51,4 @@ const Certificates = ({ courseId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
Certificates.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Certificates;
|
||||
@@ -2,22 +2,17 @@ import {
|
||||
render,
|
||||
waitFor,
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
initializeMocks,
|
||||
} from '@src/testUtils';
|
||||
import '@testing-library/jest-dom';
|
||||
import { getConfig, setConfig, initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import initializeStore from '../store';
|
||||
import { getConfig, setConfig } from '@edx/frontend-platform';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { executeThunk } from '../utils';
|
||||
import { getCourseLaunchApiUrl, getCourseBestPracticesApiUrl } from './data/api';
|
||||
import { fetchCourseLaunchQuery, fetchCourseBestPracticesQuery } from './data/thunks';
|
||||
import {
|
||||
courseId,
|
||||
initialState,
|
||||
generateCourseLaunchData,
|
||||
generateCourseBestPracticesData,
|
||||
} from './factories/mockApiResponses';
|
||||
@@ -29,11 +24,9 @@ let store;
|
||||
|
||||
const renderComponent = () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<CourseChecklist {...{ courseId }} />
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<CourseChecklist />
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -47,16 +40,9 @@ const mockStore = async (status) => {
|
||||
|
||||
describe('CourseChecklistPage', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore(initialState);
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
const mocks = initializeMocks();
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
});
|
||||
describe('renders', () => {
|
||||
describe('if enable_quality prop is true', () => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useEffect } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Container, Stack } from '@openedx/paragon';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import { DeprecatedReduxState } from '@src/store';
|
||||
|
||||
import { useModel } from '../generic/model-store';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import messages from './messages';
|
||||
import AriaLiveRegion from './AriaLiveRegion';
|
||||
@@ -15,12 +15,10 @@ import ChecklistSection from './ChecklistSection';
|
||||
import { fetchCourseLaunchQuery, fetchCourseBestPracticesQuery } from './data/thunks';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
|
||||
const CourseChecklist = ({
|
||||
courseId,
|
||||
}) => {
|
||||
const CourseChecklist = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||
const enableQuality = getConfig().ENABLE_CHECKLIST_QUALITY === 'true';
|
||||
|
||||
useEffect(() => {
|
||||
@@ -32,7 +30,7 @@ const CourseChecklist = ({
|
||||
loadingStatus,
|
||||
launchData,
|
||||
bestPracticeData,
|
||||
} = useSelector(state => state.courseChecklist);
|
||||
} = useSelector((state: DeprecatedReduxState) => state.courseChecklist);
|
||||
|
||||
const { bestPracticeChecklistLoadingStatus, launchChecklistLoadingStatus, launchChecklistStatus } = loadingStatus;
|
||||
|
||||
@@ -94,8 +92,4 @@ const CourseChecklist = ({
|
||||
);
|
||||
};
|
||||
|
||||
CourseChecklist.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CourseChecklist;
|
||||
@@ -9,8 +9,11 @@ import {
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '../testUtils';
|
||||
import { mockContentSearchConfig } from '../search-manager/data/api.mock';
|
||||
} from '@src/testUtils';
|
||||
import { mockContentSearchConfig } from '@src/search-manager/data/api.mock';
|
||||
import { type ToastActionData } from '@src/generic/toast-context';
|
||||
import { libraryBlockChangesUrl } from '@src/course-unit/data/api';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { CourseLibraries } from './CourseLibraries';
|
||||
import {
|
||||
mockGetEntityLinks,
|
||||
@@ -18,8 +21,6 @@ import {
|
||||
mockFetchIndexDocuments,
|
||||
mockUseLibBlockMetadata,
|
||||
} from './data/api.mocks';
|
||||
import { libraryBlockChangesUrl } from '../course-unit/data/api';
|
||||
import { type ToastActionData } from '../generic/toast-context';
|
||||
|
||||
mockContentSearchConfig.applyMock();
|
||||
mockGetEntityLinks.applyMock();
|
||||
@@ -58,7 +59,11 @@ describe('<CourseLibraries />', () => {
|
||||
|
||||
const renderCourseLibrariesPage = async (courseKey?: string) => {
|
||||
const courseId = courseKey || mockGetEntityLinks.courseKey;
|
||||
render(<CourseLibraries courseId={courseId} />);
|
||||
render(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<CourseLibraries />
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
it('shows the spinner before the query is complete', async () => {
|
||||
@@ -176,7 +181,11 @@ describe('<CourseLibraries ReviewTab />', () => {
|
||||
|
||||
const renderCourseLibrariesReviewPage = async (courseKey?: string) => {
|
||||
const courseId = courseKey || mockGetEntityLinks.courseKey;
|
||||
render(<CourseLibraries courseId={courseId} />);
|
||||
render(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<CourseLibraries />
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
it('shows the spinner before the query is complete', async () => {
|
||||
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
|
||||
import sumBy from 'lodash/sumBy';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import { useModel } from '../generic/model-store';
|
||||
import messages from './messages';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import { useEntityLinksSummaryByDownstreamContext } from './data/apiHooks';
|
||||
@@ -34,10 +34,6 @@ import NewsstandIcon from '../generic/NewsstandIcon';
|
||||
import ReviewTabContent from './ReviewTabContent';
|
||||
import { OutOfSyncAlert } from './OutOfSyncAlert';
|
||||
|
||||
interface Props {
|
||||
courseId: string;
|
||||
}
|
||||
|
||||
interface LibraryCardProps {
|
||||
linkSummary: PublishableEntityLinkSummary;
|
||||
}
|
||||
@@ -100,9 +96,9 @@ const LibraryCard = ({ linkSummary }: LibraryCardProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
|
||||
export const CourseLibraries = () => {
|
||||
const intl = useIntl();
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [tabKey, setTabKey] = useState<CourseLibraryTabs>(
|
||||
() => searchParams.get('tab') as CourseLibraryTabs,
|
||||
@@ -189,7 +185,7 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
|
||||
<>
|
||||
<Helmet>
|
||||
<title>
|
||||
{getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle))}
|
||||
{getPageHeadTitle(courseDetails?.name || '', intl.formatMessage(messages.headingTitle))}
|
||||
</title>
|
||||
</Helmet>
|
||||
<Container size="xl" className="px-4 pt-4 mt-3">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getApiBaseUrl, getClipboardUrl } from '@src/generic/data/api';
|
||||
import { postXBlockBaseApiUrl } from '@src/course-unit/data/api';
|
||||
import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants';
|
||||
import { getDownstreamApiUrl } from '@src/generic/unlink-modal/data/api';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import {
|
||||
act, fireEvent, initializeMocks, render, screen, waitFor, within,
|
||||
} from '@src/testUtils';
|
||||
@@ -136,7 +137,9 @@ jest.mock('@src/studio-home/data/selectors', () => ({
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const renderComponent = () => render(
|
||||
<CourseOutline courseId={courseId} />,
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<CourseOutline />
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
|
||||
describe('<CourseOutline />', () => {
|
||||
|
||||
@@ -37,6 +37,7 @@ import { ContentType } from '@src/library-authoring/routes';
|
||||
import { NOTIFICATION_MESSAGES } from '@src/constants';
|
||||
import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants';
|
||||
import { XBlock } from '@src/data/types';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import {
|
||||
getCurrentItem,
|
||||
getProctoredExamsFlag,
|
||||
@@ -63,13 +64,10 @@ import messages from './messages';
|
||||
import { getTagsExportFile } from './data/api';
|
||||
import OutlineAddChildButtons from './OutlineAddChildButtons';
|
||||
|
||||
interface CourseOutlineProps {
|
||||
courseId: string,
|
||||
}
|
||||
|
||||
const CourseOutline = ({ courseId }: CourseOutlineProps) => {
|
||||
const CourseOutline = () => {
|
||||
const intl = useIntl();
|
||||
const location = useLocation();
|
||||
const { courseId } = useCourseAuthoringContext();
|
||||
|
||||
const {
|
||||
courseUsageKey,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button,
|
||||
@@ -7,9 +5,9 @@ import {
|
||||
Layout,
|
||||
} from '@openedx/paragon';
|
||||
import { Add as IconAdd } from '@openedx/paragon/icons';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import { useModel } from '../generic/model-store';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import { USER_ROLES } from '../constants';
|
||||
import messages from './messages';
|
||||
@@ -22,11 +20,10 @@ import { useCourseTeam } from './hooks';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
|
||||
const CourseTeam = ({ courseId }) => {
|
||||
const CourseTeam = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
||||
const { courseId } = useCourseAuthoringContext();
|
||||
|
||||
const {
|
||||
modalType,
|
||||
@@ -57,6 +54,8 @@ const CourseTeam = ({ courseId }) => {
|
||||
handleInternetConnectionFailed,
|
||||
} = useCourseTeam({ intl, courseId });
|
||||
|
||||
document.title = getPageHeadTitle(courseName, intl.formatMessage(messages.headingTitle));
|
||||
|
||||
if (isLoadingDenied) {
|
||||
return (
|
||||
<Container size="xl" className="course-unit px-4 mt-4">
|
||||
@@ -171,8 +170,4 @@ const CourseTeam = ({ courseId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CourseTeam.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CourseTeam;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// @ts-check
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { courseTeamMock, courseTeamWithOneUser, courseTeamWithoutUsers } from './__mocks__';
|
||||
import { getCourseTeamApiUrl, updateCourseTeamUserApiUrl } from './data/api';
|
||||
import CourseTeam from './CourseTeam';
|
||||
@@ -19,7 +20,12 @@ let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
const courseId = '123';
|
||||
|
||||
const render = () => baseRender(<CourseTeam courseId={courseId} />, { path: mockPathname });
|
||||
const render = () => baseRender(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<CourseTeam />
|
||||
</CourseAuthoringProvider>,
|
||||
{ path: mockPathname },
|
||||
);
|
||||
|
||||
describe('<CourseTeam />', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -3,9 +3,9 @@ import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import { USER_ROLES } from '../constants';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { useModel } from '../generic/model-store';
|
||||
import {
|
||||
changeRoleTeamUserQuery,
|
||||
createCourseTeamQuery,
|
||||
@@ -26,7 +26,7 @@ const useCourseTeam = ({ courseId }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { email: currentUserEmail } = getAuthenticatedUser();
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const { courseDetails } = useCourseAuthoringContext();
|
||||
|
||||
const [modalType, setModalType] = useState(MODAL_TYPES.delete);
|
||||
const [isInfoModalOpen, openInfoModal, closeInfoModal] = useToggle(false);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
Warning as WarningIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import { CourseAuthoringUnitSidebarSlot } from '../plugin-slots/CourseAuthoringUnitSidebarSlot';
|
||||
|
||||
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
|
||||
@@ -34,9 +34,10 @@ import MoveModal from './move-modal';
|
||||
import IframePreviewLibraryXBlockChanges from './preview-changes';
|
||||
import CourseUnitHeaderActionsSlot from '../plugin-slots/CourseUnitHeaderActionsSlot';
|
||||
|
||||
const CourseUnit = ({ courseId }) => {
|
||||
const CourseUnit = () => {
|
||||
const { blockId } = useParams();
|
||||
const intl = useIntl();
|
||||
const { courseId } = useCourseAuthoringContext();
|
||||
const {
|
||||
courseUnit,
|
||||
isLoading,
|
||||
@@ -270,8 +271,4 @@ const CourseUnit = ({ courseId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CourseUnit.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CourseUnit;
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import {
|
||||
act, fireEvent, render, waitFor, within, screen,
|
||||
} 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 {
|
||||
camelCaseObject,
|
||||
getConfig,
|
||||
initializeMockApp,
|
||||
setConfig,
|
||||
} from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { cloneDeep, set } from 'lodash';
|
||||
|
||||
import {
|
||||
act, fireEvent, render, waitFor, within, screen, initializeMocks,
|
||||
} from '@src/testUtils';
|
||||
import { IFRAME_FEATURE_POLICY } from '@src/constants';
|
||||
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
|
||||
import pasteComponentMessages from '@src/generic/clipboard/paste-component/messages';
|
||||
import { getClipboardUrl } from '@src/generic/data/api';
|
||||
import { IframeProvider } from '@src/generic/hooks/context/iFrameContext';
|
||||
import { getDownstreamApiUrl } from '@src/generic/unlink-modal/data/api';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
|
||||
import {
|
||||
getCourseSectionVerticalApiUrl,
|
||||
@@ -38,7 +33,6 @@ import {
|
||||
getCourseOutlineInfoQuery,
|
||||
patchUnitItemQuery,
|
||||
} from './data/thunk';
|
||||
import initializeStore from '../store';
|
||||
import {
|
||||
courseCreateXblockMock,
|
||||
courseSectionVerticalMock,
|
||||
@@ -68,7 +62,6 @@ import messages from './messages';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
let queryClient;
|
||||
const courseId = '123';
|
||||
const blockId = '567890';
|
||||
const sequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5';
|
||||
@@ -111,38 +104,20 @@ function simulatePostMessageEvent(type, payload) {
|
||||
}
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<IframeProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CourseUnit courseId={courseId} />
|
||||
</QueryClientProvider>
|
||||
</IframeProvider>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
<IframeProvider>
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<CourseUnit />
|
||||
</CourseAuthoringProvider>
|
||||
</IframeProvider>
|
||||
);
|
||||
|
||||
describe('<CourseUnit />', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
const mocks = initializeMocks();
|
||||
window.scrollTo = jest.fn();
|
||||
global.localStorage.clear();
|
||||
store = initializeStore();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
axiosMock
|
||||
.onGet(getClipboardUrl())
|
||||
.reply(200, clipboardUnit);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import {
|
||||
initializeMocks, waitFor, render, screen,
|
||||
} from '../testUtils';
|
||||
@@ -26,12 +27,17 @@ const expectedCourseItemDataWithoutUnit = [{
|
||||
}];
|
||||
|
||||
const renderSubsectionRedirectPage = () => {
|
||||
render(<SubsectionUnitRedirect courseId={courseId} />, {
|
||||
path,
|
||||
routerProps: {
|
||||
initialEntries: [`/subsection/${subsectionId}`],
|
||||
render(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<SubsectionUnitRedirect />
|
||||
</CourseAuthoringProvider>,
|
||||
{
|
||||
path,
|
||||
routerProps: {
|
||||
initialEntries: [`/subsection/${subsectionId}`],
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { LoadingSpinner } from '@src/generic/Loading';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
|
||||
import { useParams, Navigate } from 'react-router-dom';
|
||||
import { useCourseItemData } from '../course-outline/data/apiHooks';
|
||||
|
||||
const SubsectionUnitRedirect = ({ courseId }: { courseId: string }) => {
|
||||
const SubsectionUnitRedirect = () => {
|
||||
const { courseId } = useCourseAuthoringContext();
|
||||
let { subsectionId } = useParams();
|
||||
// if the call is made via the click on breadcrumbs the re won't be courseId available
|
||||
// in such cases the page should redirect to the 1st unit of he subsection
|
||||
|
||||
@@ -5,8 +5,9 @@ import { Button } from '@openedx/paragon';
|
||||
import { Plus as PlusIcon, ContentPasteGo as ContentPasteGoIcon } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import { changeEditTitleFormOpen, updateQueryPendingStatus } from '../../data/slice';
|
||||
import { getCourseUnitData, getCourseId, getSequenceId } from '../../data/selectors';
|
||||
import { getCourseUnitData, getSequenceId } from '../../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { useIndexOfLastVisibleChild } from '../hooks';
|
||||
import SequenceNavigationDropdown from './SequenceNavigationDropdown';
|
||||
@@ -19,7 +20,7 @@ const SequenceNavigationTabs = ({
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const sequenceId = useSelector(getSequenceId);
|
||||
const courseId = useSelector(getCourseId);
|
||||
const { courseId } = useCourseAuthoringContext();
|
||||
const courseUnit = useSelector(getCourseUnitData);
|
||||
const sequenceChildAddable = courseUnit?.ancestorInfo?.ancestors?.[0]?.actions?.childAddable;
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import { Button } from '@openedx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { DeprecatedReduxState } from '@src/store';
|
||||
import { getCourseId, getSequenceId } from '@src/course-unit/data/selectors';
|
||||
import { getSequenceId } from '@src/course-unit/data/selectors';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
|
||||
import UnitIcon from './UnitIcon';
|
||||
|
||||
interface Props {
|
||||
@@ -20,7 +22,7 @@ const UnitButton: FC<Props> = ({
|
||||
isActive, // passed from parent (SequenceNavigationTabs)
|
||||
showTitle = false,
|
||||
}) => {
|
||||
const courseId = useSelector(getCourseId);
|
||||
const { courseId } = useCourseAuthoringContext();
|
||||
const sequenceId = useSelector(getSequenceId);
|
||||
|
||||
const unit = useSelector((state: DeprecatedReduxState) => state.models.units[unitId]);
|
||||
|
||||
@@ -10,7 +10,6 @@ export const getErrorMessage = (state) => state.courseUnit.errorMessage;
|
||||
export const getSequenceStatus = (state) => state.courseUnit.sequenceStatus;
|
||||
export const getSequenceIds = (state) => state.courseUnit.courseSectionVertical.courseSequenceIds;
|
||||
export const getCourseSectionVertical = (state) => state.courseUnit.courseSectionVertical;
|
||||
export const getCourseId = (state) => state.courseDetail.courseId;
|
||||
export const getSequenceId = (state) => state.courseUnit.sequenceId;
|
||||
export const getCourseVerticalChildren = (state) => state.courseUnit.courseVerticalChildren;
|
||||
export const getCourseOutlineInfo = (state) => state.courseUnit.courseOutlineInfo;
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
import {
|
||||
render, waitFor, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import {
|
||||
getCourseUpdatesApiUrl,
|
||||
getCourseHandoutApiUrl,
|
||||
updateCourseUpdatesApiUrl,
|
||||
} from './data/api';
|
||||
import {
|
||||
createCourseUpdateQuery,
|
||||
deleteCourseUpdateQuery,
|
||||
editCourseHandoutsQuery,
|
||||
editCourseUpdateQuery,
|
||||
} from './data/thunk';
|
||||
import initializeStore from '../store';
|
||||
import { executeThunk } from '../utils';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { courseUpdatesMock, courseHandoutsMock } from './__mocks__';
|
||||
import CourseUpdates from './CourseUpdates';
|
||||
import messages from './messages';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
const courseId = '123';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
pathname: mockPathname,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@tinymce/tinymce-react', () => {
|
||||
const originalModule = jest.requireActual('@tinymce/tinymce-react');
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
Editor: () => 'foo bar',
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../editors/sharedComponents/TinyMceWidget', () => ({
|
||||
__esModule: true, // Required to mock a default export
|
||||
default: () => <div>Widget</div>,
|
||||
prepareEditorRef: jest.fn(() => ({
|
||||
refReady: true,
|
||||
setEditorRef: jest.fn().mockName('prepareEditorRef.setEditorRef'),
|
||||
})),
|
||||
}));
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="es">
|
||||
<CourseUpdates courseId={courseId} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<CourseUpdates />', () => {
|
||||
describe('Successful API responses', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
.onGet(getCourseUpdatesApiUrl(courseId))
|
||||
.reply(200, courseUpdatesMock);
|
||||
axiosMock
|
||||
.onGet(getCourseHandoutApiUrl(courseId))
|
||||
.reply(200, courseHandoutsMock);
|
||||
});
|
||||
|
||||
it('render CourseUpdates component correctly', async () => {
|
||||
const {
|
||||
getByText, getAllByTestId, getByTestId, getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.sectionInfo.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getAllByTestId('course-update')).toHaveLength(3);
|
||||
expect(getByTestId('course-handouts')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should create course update', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
|
||||
const data = {
|
||||
content: '<p>Some text</p>',
|
||||
date: 'August 29, 2023',
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPost(getCourseUpdatesApiUrl(courseId))
|
||||
.reply(200, data);
|
||||
|
||||
await executeThunk(createCourseUpdateQuery(courseId, data), store.dispatch);
|
||||
expect(getByText('Some text')).toBeInTheDocument();
|
||||
expect(getByText(data.date)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should edit course update', async () => {
|
||||
const { getByText, queryByText } = render(<RootWrapper />);
|
||||
|
||||
const data = {
|
||||
id: courseUpdatesMock[0].id,
|
||||
content: '<p>Some text</p>',
|
||||
date: 'August 29, 2023',
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPut(updateCourseUpdatesApiUrl(courseId, courseUpdatesMock[0].id))
|
||||
.reply(200, data);
|
||||
|
||||
await executeThunk(editCourseUpdateQuery(courseId, data), store.dispatch);
|
||||
expect(getByText('Some text')).toBeInTheDocument();
|
||||
expect(getByText(data.date)).toBeInTheDocument();
|
||||
expect(queryByText(courseUpdatesMock[0].date)).not.toBeInTheDocument();
|
||||
expect(queryByText(courseUpdatesMock[0].content)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should delete course update', async () => {
|
||||
const { queryByText } = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onDelete(updateCourseUpdatesApiUrl(courseId, courseUpdatesMock[0].id))
|
||||
.reply(200);
|
||||
|
||||
await executeThunk(deleteCourseUpdateQuery(courseId, courseUpdatesMock[0].id), store.dispatch);
|
||||
expect(queryByText(courseUpdatesMock[0].date)).not.toBeInTheDocument();
|
||||
expect(queryByText(courseUpdatesMock[0].content)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should edit course handouts', async () => {
|
||||
const { getByText, queryByText } = render(<RootWrapper />);
|
||||
|
||||
const data = {
|
||||
...courseHandoutsMock,
|
||||
data: '<p>Some handouts 1</p>',
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPut(getCourseHandoutApiUrl(courseId))
|
||||
.reply(200, data);
|
||||
|
||||
await executeThunk(editCourseHandoutsQuery(courseId, data), store.dispatch);
|
||||
expect(getByText('Some handouts 1')).toBeInTheDocument();
|
||||
expect(queryByText(courseHandoutsMock.data)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Add new update form is visible after clicking "New update" button', async () => {
|
||||
const { getByText, getByRole, getAllByTestId } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const editUpdateButtons = getAllByTestId('course-update-edit-button');
|
||||
const deleteButtons = getAllByTestId('course-update-delete-button');
|
||||
const editHandoutsButtons = getAllByTestId('course-handouts-edit-button');
|
||||
const newUpdateButton = getByRole('button', { name: messages.newUpdateButton.defaultMessage });
|
||||
|
||||
fireEvent.click(newUpdateButton);
|
||||
|
||||
expect(newUpdateButton).toBeDisabled();
|
||||
editUpdateButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
editHandoutsButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
deleteButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
expect(getByText('Add new update')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Edit handouts form is visible after clicking "Edit" button', async () => {
|
||||
const { getByText, getByRole, getAllByTestId } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const editUpdateButtons = getAllByTestId('course-update-edit-button');
|
||||
const deleteButtons = getAllByTestId('course-update-delete-button');
|
||||
const editHandoutsButtons = getAllByTestId('course-handouts-edit-button');
|
||||
const editHandoutsButton = editHandoutsButtons[0];
|
||||
|
||||
fireEvent.click(editHandoutsButton);
|
||||
|
||||
expect(editHandoutsButton).toBeDisabled();
|
||||
expect(getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeDisabled();
|
||||
editUpdateButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
editHandoutsButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
deleteButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
expect(getByText('Edit handouts')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Edit update form is visible after clicking "Edit" button', async () => {
|
||||
const {
|
||||
getByText, getByRole, getAllByTestId, queryByText,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const editUpdateButtons = getAllByTestId('course-update-edit-button');
|
||||
const deleteButtons = getAllByTestId('course-update-delete-button');
|
||||
const editHandoutsButtons = getAllByTestId('course-handouts-edit-button');
|
||||
const editUpdateFirstButton = editUpdateButtons[0];
|
||||
|
||||
fireEvent.click(editUpdateFirstButton);
|
||||
expect(getByText('Edit update')).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeDisabled();
|
||||
editUpdateButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
editHandoutsButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
deleteButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
expect(queryByText(courseUpdatesMock[0].content)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('page load failure API responses', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('Course updates fetch should show updates loading error', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseUpdatesApiUrl(courseId))
|
||||
.reply(404);
|
||||
axiosMock
|
||||
.onGet(getCourseHandoutApiUrl(courseId))
|
||||
.reply(200, courseHandoutsMock);
|
||||
|
||||
const {
|
||||
getByText, queryByTestId, getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const newButton = getByRole('button', { name: messages.newUpdateButton.defaultMessage });
|
||||
expect(getByText(messages.loadingUpdatesErrorTitle.defaultMessage));
|
||||
expect(newButton).toBeDisabled();
|
||||
expect(getByText(messages.noCourseUpdates.defaultMessage)).toBeVisible();
|
||||
expect(queryByTestId('course-update')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('Course handouts fetch should show handouts loading error', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseUpdatesApiUrl(courseId))
|
||||
.reply(200, courseUpdatesMock);
|
||||
axiosMock
|
||||
.onGet(getCourseHandoutApiUrl(courseId))
|
||||
.reply(404);
|
||||
|
||||
const {
|
||||
getByText, getByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText(messages.loadingHandoutsErrorTitle.defaultMessage));
|
||||
expect(getByTestId('course-handouts-edit-button')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseUpdatesApiUrl(courseId))
|
||||
.reply(403, courseUpdatesMock);
|
||||
axiosMock
|
||||
.onGet(getCourseHandoutApiUrl(courseId))
|
||||
.reply(403);
|
||||
|
||||
const { getByTestId } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('connectionErrorAlert')).toBeInTheDocument();
|
||||
const { loadingStatuses } = store.getState().courseUpdates;
|
||||
Object.values(loadingStatuses)
|
||||
.some(status => expect(status).toEqual(RequestStatus.DENIED));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('saving failure API responses', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
.onGet(getCourseUpdatesApiUrl(courseId))
|
||||
.reply(200, courseUpdatesMock);
|
||||
axiosMock
|
||||
.onGet(getCourseHandoutApiUrl(courseId))
|
||||
.reply(200, courseHandoutsMock);
|
||||
});
|
||||
it('creating new update should show saving error alert', async () => {
|
||||
const { getByText, queryByText } = render(<RootWrapper />);
|
||||
|
||||
const data = {
|
||||
content: '<p>Some text</p>',
|
||||
date: 'August 29, 2023',
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPost(getCourseUpdatesApiUrl(courseId), data)
|
||||
.reply(404);
|
||||
|
||||
await executeThunk(createCourseUpdateQuery(courseId, data), store.dispatch);
|
||||
expect(getByText(messages.savingNewUpdateErrorAlertDescription.defaultMessage)).toBeVisible();
|
||||
expect(queryByText('Some text')).toBeNull();
|
||||
expect(queryByText(data.date)).toBeNull();
|
||||
});
|
||||
|
||||
it('editing course update should show saving error alert', async () => {
|
||||
const { getByText, queryByText } = render(<RootWrapper />);
|
||||
|
||||
const data = {
|
||||
id: courseUpdatesMock[0].id,
|
||||
content: '<p>Some text</p>',
|
||||
date: 'August 29, 2023',
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPut(updateCourseUpdatesApiUrl(courseId, courseUpdatesMock[0].id))
|
||||
.reply(404);
|
||||
|
||||
await executeThunk(editCourseUpdateQuery(courseId, data), store.dispatch);
|
||||
expect(queryByText('Some text')).toBeNull();
|
||||
expect(queryByText(data.date)).toBeNull();
|
||||
expect(getByText(courseUpdatesMock[0].date)).toBeVisible();
|
||||
expect(getByText(courseUpdatesMock[0].content)).toBeVisible();
|
||||
expect(getByText(messages.savingUpdatesErrorDescription.defaultMessage)).toBeVisible();
|
||||
});
|
||||
|
||||
it('deleting course update should show delete saving error alert', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onDelete(updateCourseUpdatesApiUrl(courseId, courseUpdatesMock[0].id))
|
||||
.reply(404);
|
||||
|
||||
await executeThunk(deleteCourseUpdateQuery(courseId, courseUpdatesMock[0].id), store.dispatch);
|
||||
expect(getByText(courseUpdatesMock[0].date)).toBeVisible();
|
||||
expect(getByText(courseUpdatesMock[0].content)).toBeVisible();
|
||||
expect(getByText(messages.deletingUpdatesErrorDescription.defaultMessage)).toBeVisible();
|
||||
});
|
||||
|
||||
it('editing course handouts should show saving error alert', async () => {
|
||||
const { getByText, queryByText } = render(<RootWrapper />);
|
||||
|
||||
const data = {
|
||||
...courseHandoutsMock,
|
||||
data: '<p>Some handouts 1</p>',
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPut(getCourseHandoutApiUrl(courseId))
|
||||
.reply(404);
|
||||
|
||||
await executeThunk(editCourseHandoutsQuery(courseId, data), store.dispatch);
|
||||
expect(queryByText('Some handouts 1')).toBeNull();
|
||||
expect(getByText(courseHandoutsMock.data)).toBeVisible();
|
||||
expect(getByText(messages.savingHandoutsErrorDescription.defaultMessage));
|
||||
});
|
||||
});
|
||||
});
|
||||
345
src/course-updates/CourseUpdates.test.tsx
Normal file
345
src/course-updates/CourseUpdates.test.tsx
Normal file
@@ -0,0 +1,345 @@
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { executeThunk } from '@src/utils';
|
||||
import { RequestStatus } from '@src/data/constants';
|
||||
import {
|
||||
initializeMocks, render, waitFor, fireEvent, screen,
|
||||
} from '@src/testUtils';
|
||||
|
||||
import {
|
||||
getCourseUpdatesApiUrl,
|
||||
getCourseHandoutApiUrl,
|
||||
updateCourseUpdatesApiUrl,
|
||||
} from './data/api';
|
||||
import {
|
||||
createCourseUpdateQuery,
|
||||
deleteCourseUpdateQuery,
|
||||
editCourseHandoutsQuery,
|
||||
editCourseUpdateQuery,
|
||||
} from './data/thunk';
|
||||
import { courseUpdatesMock, courseHandoutsMock } from './__mocks__';
|
||||
import CourseUpdates from './CourseUpdates';
|
||||
import messages from './messages';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
const courseId = '123';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
pathname: mockPathname,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@tinymce/tinymce-react', () => {
|
||||
const originalModule = jest.requireActual('@tinymce/tinymce-react');
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
Editor: () => 'foo bar',
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../editors/sharedComponents/TinyMceWidget', () => ({
|
||||
__esModule: true, // Required to mock a default export
|
||||
default: () => <div>Widget</div>,
|
||||
prepareEditorRef: jest.fn(() => ({
|
||||
refReady: true,
|
||||
setEditorRef: jest.fn().mockName('prepareEditorRef.setEditorRef'),
|
||||
})),
|
||||
}));
|
||||
|
||||
const RootWrapper = () => (
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<CourseUpdates />
|
||||
</CourseAuthoringProvider>
|
||||
);
|
||||
|
||||
describe('<CourseUpdates />', () => {
|
||||
describe('Successful API responses', () => {
|
||||
beforeEach(() => {
|
||||
const mocks = initializeMocks();
|
||||
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
axiosMock
|
||||
.onGet(getCourseUpdatesApiUrl(courseId))
|
||||
.reply(200, courseUpdatesMock);
|
||||
axiosMock
|
||||
.onGet(getCourseHandoutApiUrl(courseId))
|
||||
.reply(200, courseHandoutsMock);
|
||||
});
|
||||
|
||||
it('render CourseUpdates component correctly', async () => {
|
||||
render(<RootWrapper />);
|
||||
expect(await screen.findByText(messages.headingTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(messages.sectionInfo.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(screen.getAllByTestId('course-update')).toHaveLength(3);
|
||||
expect(screen.getByTestId('course-handouts')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should create course update', async () => {
|
||||
const data = {
|
||||
content: '<p>Some text</p>',
|
||||
date: 'August 29, 2023',
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPost(getCourseUpdatesApiUrl(courseId))
|
||||
.reply(200, data);
|
||||
|
||||
render(<RootWrapper />);
|
||||
|
||||
await executeThunk(createCourseUpdateQuery(courseId, data), store.dispatch);
|
||||
expect(screen.getByText('Some text')).toBeInTheDocument();
|
||||
expect(screen.getByText(data.date)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should edit course update', async () => {
|
||||
const data = {
|
||||
id: courseUpdatesMock[0].id,
|
||||
content: '<p>Some text</p>',
|
||||
date: 'August 29, 2023',
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPut(updateCourseUpdatesApiUrl(courseId, courseUpdatesMock[0].id))
|
||||
.reply(200, data);
|
||||
|
||||
render(<RootWrapper />);
|
||||
|
||||
await executeThunk(editCourseUpdateQuery(courseId, data), store.dispatch);
|
||||
expect(screen.getByText('Some text')).toBeInTheDocument();
|
||||
expect(screen.getByText(data.date)).toBeInTheDocument();
|
||||
expect(screen.queryByText(courseUpdatesMock[0].date)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(courseUpdatesMock[0].content)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should delete course update', async () => {
|
||||
axiosMock
|
||||
.onDelete(updateCourseUpdatesApiUrl(courseId, courseUpdatesMock[0].id))
|
||||
.reply(200);
|
||||
|
||||
render(<RootWrapper />);
|
||||
|
||||
await executeThunk(deleteCourseUpdateQuery(courseId, courseUpdatesMock[0].id), store.dispatch);
|
||||
expect(screen.queryByText(courseUpdatesMock[0].date)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(courseUpdatesMock[0].content)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should edit course handouts', async () => {
|
||||
const data = {
|
||||
...courseHandoutsMock,
|
||||
data: '<p>Some handouts 1</p>',
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPut(getCourseHandoutApiUrl(courseId))
|
||||
.reply(200, data);
|
||||
|
||||
render(<RootWrapper />);
|
||||
|
||||
await executeThunk(editCourseHandoutsQuery(courseId, data), store.dispatch);
|
||||
expect(screen.getByText('Some handouts 1')).toBeInTheDocument();
|
||||
expect(screen.queryByText(courseHandoutsMock.data)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Add new update form is visible after clicking "New update" button', async () => {
|
||||
render(<RootWrapper />);
|
||||
const editUpdateButtons = await screen.findAllByTestId('course-update-edit-button');
|
||||
const deleteButtons = await screen.findAllByTestId('course-update-delete-button');
|
||||
const editHandoutsButtons = await screen.findAllByTestId('course-handouts-edit-button');
|
||||
const newUpdateButton = await screen.findByRole('button', { name: messages.newUpdateButton.defaultMessage });
|
||||
|
||||
fireEvent.click(newUpdateButton);
|
||||
|
||||
expect(newUpdateButton).toBeDisabled();
|
||||
editUpdateButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
editHandoutsButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
deleteButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
expect(screen.getByText('Add new update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Edit handouts form is visible after clicking "Edit" button', async () => {
|
||||
render(<RootWrapper />);
|
||||
const editUpdateButtons = await screen.findAllByTestId('course-update-edit-button');
|
||||
const deleteButtons = await screen.findAllByTestId('course-update-delete-button');
|
||||
const editHandoutsButtons = await screen.findAllByTestId('course-handouts-edit-button');
|
||||
const editHandoutsButton = editHandoutsButtons[0];
|
||||
|
||||
fireEvent.click(editHandoutsButton);
|
||||
|
||||
expect(editHandoutsButton).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeDisabled();
|
||||
editUpdateButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
editHandoutsButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
deleteButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
expect(screen.getByText('Edit handouts')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Edit update form is visible after clicking "Edit" button', async () => {
|
||||
render(<RootWrapper />);
|
||||
let editUpdateButtons = await screen.findAllByTestId('course-update-edit-button');
|
||||
const editUpdateFirstButton = editUpdateButtons[0];
|
||||
fireEvent.click(editUpdateFirstButton);
|
||||
|
||||
const deleteButtons = await screen.findAllByTestId('course-update-delete-button');
|
||||
const editHandoutsButtons = await screen.findAllByTestId('course-handouts-edit-button');
|
||||
editUpdateButtons = await screen.findAllByTestId('course-update-edit-button');
|
||||
|
||||
expect(screen.getByText('Edit update')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeDisabled();
|
||||
editUpdateButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
editHandoutsButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
deleteButtons.forEach((button) => expect(button).toBeDisabled());
|
||||
expect(screen.queryByText(courseUpdatesMock[0].content)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('page load failure API responses', () => {
|
||||
beforeEach(() => {
|
||||
const mocks = initializeMocks();
|
||||
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
});
|
||||
|
||||
it('Course updates fetch should show updates loading error', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseUpdatesApiUrl(courseId))
|
||||
.reply(404);
|
||||
axiosMock
|
||||
.onGet(getCourseHandoutApiUrl(courseId))
|
||||
.reply(200, courseHandoutsMock);
|
||||
|
||||
render(<RootWrapper />);
|
||||
|
||||
const newButton = await screen.findByRole('button', { name: messages.newUpdateButton.defaultMessage });
|
||||
expect(screen.getByText(messages.loadingUpdatesErrorTitle.defaultMessage));
|
||||
expect(newButton).toBeDisabled();
|
||||
expect(screen.getByText(messages.noCourseUpdates.defaultMessage)).toBeVisible();
|
||||
expect(screen.queryByTestId('course-update')).toBeNull();
|
||||
});
|
||||
|
||||
it('Course handouts fetch should show handouts loading error', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseUpdatesApiUrl(courseId))
|
||||
.reply(200, courseUpdatesMock);
|
||||
axiosMock
|
||||
.onGet(getCourseHandoutApiUrl(courseId))
|
||||
.reply(404);
|
||||
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(messages.loadingHandoutsErrorTitle.defaultMessage));
|
||||
});
|
||||
expect(screen.getByTestId('course-handouts-edit-button')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseUpdatesApiUrl(courseId))
|
||||
.reply(403, courseUpdatesMock);
|
||||
axiosMock
|
||||
.onGet(getCourseHandoutApiUrl(courseId))
|
||||
.reply(403);
|
||||
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const { loadingStatuses } = store.getState().courseUpdates;
|
||||
Object.values(loadingStatuses)
|
||||
.some(status => expect(status).toEqual(RequestStatus.DENIED));
|
||||
});
|
||||
expect(screen.getByTestId('connectionErrorAlert')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('saving failure API responses', () => {
|
||||
beforeEach(() => {
|
||||
const mocks = initializeMocks();
|
||||
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
axiosMock
|
||||
.onGet(getCourseUpdatesApiUrl(courseId))
|
||||
.reply(200, courseUpdatesMock);
|
||||
axiosMock
|
||||
.onGet(getCourseHandoutApiUrl(courseId))
|
||||
.reply(200, courseHandoutsMock);
|
||||
});
|
||||
it('creating new update should show saving error alert', async () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
const data = {
|
||||
content: '<p>Some text</p>',
|
||||
date: 'August 29, 2023',
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPost(getCourseUpdatesApiUrl(courseId), data)
|
||||
.reply(404);
|
||||
|
||||
await executeThunk(createCourseUpdateQuery(courseId, data), store.dispatch);
|
||||
expect(screen.getByText(messages.savingNewUpdateErrorAlertDescription.defaultMessage)).toBeVisible();
|
||||
expect(screen.queryByText('Some text')).toBeNull();
|
||||
expect(screen.queryByText(data.date)).toBeNull();
|
||||
});
|
||||
|
||||
it('editing course update should show saving error alert', async () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
const data = {
|
||||
id: courseUpdatesMock[0].id,
|
||||
content: '<p>Some text</p>',
|
||||
date: 'August 29, 2023',
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPut(updateCourseUpdatesApiUrl(courseId, courseUpdatesMock[0].id))
|
||||
.reply(404);
|
||||
|
||||
await executeThunk(editCourseUpdateQuery(courseId, data), store.dispatch);
|
||||
expect(screen.queryByText('Some text')).toBeNull();
|
||||
expect(screen.queryByText(data.date)).toBeNull();
|
||||
expect(screen.getByText(courseUpdatesMock[0].date)).toBeVisible();
|
||||
expect(screen.getByText(courseUpdatesMock[0].content)).toBeVisible();
|
||||
expect(screen.getByText(messages.savingUpdatesErrorDescription.defaultMessage)).toBeVisible();
|
||||
});
|
||||
|
||||
it('deleting course update should show delete saving error alert', async () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onDelete(updateCourseUpdatesApiUrl(courseId, courseUpdatesMock[0].id))
|
||||
.reply(404);
|
||||
|
||||
await executeThunk(deleteCourseUpdateQuery(courseId, courseUpdatesMock[0].id), store.dispatch);
|
||||
expect(screen.getByText(courseUpdatesMock[0].date)).toBeVisible();
|
||||
expect(screen.getByText(courseUpdatesMock[0].content)).toBeVisible();
|
||||
expect(screen.getByText(messages.deletingUpdatesErrorDescription.defaultMessage)).toBeVisible();
|
||||
});
|
||||
|
||||
it('editing course handouts should show saving error alert', async () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
const data = {
|
||||
...courseHandoutsMock,
|
||||
data: '<p>Some handouts 1</p>',
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPut(getCourseHandoutApiUrl(courseId))
|
||||
.reply(404);
|
||||
|
||||
await executeThunk(editCourseHandoutsQuery(courseId, data), store.dispatch);
|
||||
expect(screen.queryByText('Some handouts 1')).toBeNull();
|
||||
expect(screen.getByText(courseHandoutsMock.data)).toBeVisible();
|
||||
expect(await screen.findByText(messages.savingHandoutsErrorDescription.defaultMessage));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
@@ -11,13 +9,13 @@ import {
|
||||
import { Add as AddIcon, ErrorOutline as ErrorIcon } from '@openedx/paragon/icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useModel } from '../generic/model-store';
|
||||
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
|
||||
import ProcessingNotification from '../generic/processing-notification';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { getProcessingNotification } from '@src/generic/processing-notification/data/selectors';
|
||||
import ProcessingNotification from '@src/generic/processing-notification';
|
||||
import SubHeader from '@src/generic/sub-header/SubHeader';
|
||||
import InternetConnectionAlert from '@src/generic/internet-connection-alert';
|
||||
import ConnectionErrorAlert from '@src/generic/ConnectionErrorAlert';
|
||||
import { RequestStatus } from '@src/data/constants';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import CourseHandouts from './course-handouts/CourseHandouts';
|
||||
import CourseUpdate from './course-update/CourseUpdate';
|
||||
import DeleteModal from './delete-modal/DeleteModal';
|
||||
@@ -34,9 +32,9 @@ import { matchesAnyStatus } from './utils';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
|
||||
const CourseUpdates = ({ courseId }) => {
|
||||
const CourseUpdates = () => {
|
||||
const intl = useIntl();
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||
|
||||
const {
|
||||
requestType,
|
||||
@@ -81,7 +79,7 @@ const CourseUpdates = ({ courseId }) => {
|
||||
<>
|
||||
<Helmet>
|
||||
<title>
|
||||
{getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle))}
|
||||
{getPageHeadTitle(courseDetails?.name || '', intl.formatMessage(messages.headingTitle))}
|
||||
</title>
|
||||
</Helmet>
|
||||
<Container size="xl" className="px-4 pt-4">
|
||||
@@ -163,7 +161,6 @@ const CourseUpdates = ({ courseId }) => {
|
||||
<section className="updates-section">
|
||||
{isMainFormOpen && (
|
||||
<UpdateForm
|
||||
isOpen={isUpdateFormOpen}
|
||||
close={closeUpdateForm}
|
||||
requestType={requestType}
|
||||
onSubmit={handleUpdatesSubmit}
|
||||
@@ -176,7 +173,6 @@ const CourseUpdates = ({ courseId }) => {
|
||||
{courseUpdates.map((courseUpdate, index) => (
|
||||
isInnerFormOpen(courseUpdate.id) ? (
|
||||
<UpdateForm
|
||||
isOpen={isUpdateFormOpen}
|
||||
close={closeUpdateForm}
|
||||
requestType={requestType}
|
||||
isInnerForm
|
||||
@@ -251,8 +247,4 @@ const CourseUpdates = ({ courseId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CourseUpdates.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CourseUpdates;
|
||||
@@ -1,22 +1,18 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
render,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
act,
|
||||
} from '@testing-library/react';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
initializeMocks,
|
||||
} from '@src/testUtils';
|
||||
import moment from 'moment/moment';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { REQUEST_TYPES } from '../constants';
|
||||
import { courseHandoutsMock, courseUpdatesMock } from '../__mocks__';
|
||||
import UpdateForm from './UpdateForm';
|
||||
import messages from './messages';
|
||||
|
||||
let store;
|
||||
const closeMock = jest.fn();
|
||||
const onSubmitMock = jest.fn();
|
||||
const addNewUpdateMock = { id: 0, date: moment().utc().toDate(), content: 'Some content' };
|
||||
@@ -53,31 +49,20 @@ const courseUpdatesInitialValues = (requestType) => {
|
||||
};
|
||||
|
||||
const renderComponent = ({ requestType }) => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<UpdateForm
|
||||
isOpen
|
||||
close={closeMock}
|
||||
requestType={requestType}
|
||||
onSubmit={onSubmitMock}
|
||||
courseUpdatesInitialValues={courseUpdatesInitialValues(requestType)}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</AppProvider>,
|
||||
<CourseAuthoringProvider courseId="1">
|
||||
<UpdateForm
|
||||
isOpen
|
||||
close={closeMock}
|
||||
requestType={requestType}
|
||||
onSubmit={onSubmitMock}
|
||||
courseUpdatesInitialValues={courseUpdatesInitialValues(requestType)}
|
||||
/>,
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
|
||||
describe('<UpdateForm />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
initializeMocks();
|
||||
});
|
||||
it('render Add new update form correctly', async () => {
|
||||
const { getByText, getByDisplayValue, getByRole } = renderComponent({ requestType: REQUEST_TYPES.add_new_update });
|
||||
|
||||
@@ -6,10 +6,11 @@ import {
|
||||
screen,
|
||||
act,
|
||||
render,
|
||||
} from '../testUtils';
|
||||
import { executeThunk } from '../utils';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { getApiWaffleFlagsUrl } from '../data/api';
|
||||
} from '@src/testUtils';
|
||||
import { executeThunk } from '@src/utils';
|
||||
import { RequestStatus } from '@src/data/constants';
|
||||
import { getApiWaffleFlagsUrl } from '@src/data/api';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import CustomPages from './CustomPages';
|
||||
import {
|
||||
generateFetchPageApiResponse,
|
||||
@@ -28,10 +29,15 @@ import messages from './messages';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
// @ts-ignore
|
||||
ReactDOM.createPortal = jest.fn(node => node);
|
||||
|
||||
const renderComponent = () => {
|
||||
render(<CustomPages courseId={courseId} />);
|
||||
render(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<CustomPages />
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
const mockStore = async (status) => {
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useContext, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Routes, Route, useNavigate, Link,
|
||||
} from 'react-router-dom';
|
||||
@@ -20,12 +19,17 @@ import {
|
||||
Container,
|
||||
} from '@openedx/paragon';
|
||||
import { Add, SpinnerSimple } from '@openedx/paragon/icons';
|
||||
import Placeholder from '../editors/Placeholder';
|
||||
import DraggableList, { SortableItem } from '../generic/DraggableList';
|
||||
import ErrorAlert from '../editors/sharedComponents/ErrorAlerts/ErrorAlert';
|
||||
import Placeholder from '@src/editors/Placeholder';
|
||||
import DraggableList, { SortableItem } from '@src/generic/DraggableList';
|
||||
import ErrorAlert from '@src/editors/sharedComponents/ErrorAlerts/ErrorAlert';
|
||||
import { RequestStatus } from '@src/data/constants';
|
||||
import { useModels } from '@src/generic/model-store';
|
||||
import { useWaffleFlags } from '@src/data/apiHooks';
|
||||
import getPageHeadTitle from '@src/generic/utils';
|
||||
import { getPagePath } from '@src/utils';
|
||||
import { DeprecatedReduxState } from '@src/store';
|
||||
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { useModels, useModel } from '../generic/model-store';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import { getLoadingStatus, getSavingStatus } from './data/selectors';
|
||||
import {
|
||||
addSingleCustomPage,
|
||||
@@ -33,29 +37,24 @@ import {
|
||||
updatePageOrder,
|
||||
updateSingleCustomPage,
|
||||
} from './data/thunks';
|
||||
|
||||
import previewLmsStaticPages from './data/images/previewLmsStaticPages.png';
|
||||
import CustomPageCard from './CustomPageCard';
|
||||
import messages from './messages';
|
||||
import CustomPagesProvider from './CustomPagesProvider';
|
||||
import EditModal from './EditModal';
|
||||
import { useWaffleFlags } from '../data/apiHooks';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import { getPagePath } from '../utils';
|
||||
|
||||
const CustomPages = ({
|
||||
courseId,
|
||||
}) => {
|
||||
const CustomPages = () => {
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [orderedPages, setOrderedPages] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState();
|
||||
const [currentPage, setCurrentPage] = useState<any>();
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
|
||||
document.title = getPageHeadTitle(courseDetails?.name || '', intl.formatMessage(messages.heading));
|
||||
|
||||
// @ts-expect-error - frontend-platform doesn't have type information
|
||||
const { config } = useContext(AppContext);
|
||||
const learningCourseURL = `${config.LEARNING_BASE_URL}/course/${courseId}`;
|
||||
|
||||
@@ -63,9 +62,9 @@ const CustomPages = ({
|
||||
dispatch(fetchCustomPages(courseId));
|
||||
}, [courseId]);
|
||||
|
||||
const customPagesIds = useSelector(state => state.customPages.customPagesIds);
|
||||
const addPageStatus = useSelector(state => state.customPages.addingStatus);
|
||||
const deletePageStatus = useSelector(state => state.customPages.deletingStatus);
|
||||
const customPagesIds = useSelector((state: DeprecatedReduxState) => state.customPages.customPagesIds);
|
||||
const addPageStatus = useSelector((state: DeprecatedReduxState) => state.customPages.addingStatus);
|
||||
const deletePageStatus = useSelector((state: DeprecatedReduxState) => state.customPages.deletingStatus);
|
||||
const savingStatus = useSelector(getSavingStatus);
|
||||
const loadingStatus = useSelector(getLoadingStatus);
|
||||
const waffleFlags = useWaffleFlags(courseId);
|
||||
@@ -174,7 +173,7 @@ const CustomPages = ({
|
||||
<FormattedMessage {...messages.note} />
|
||||
</div>
|
||||
<DraggableList itemList={orderedPages} setState={setOrderedPages} updateOrder={handleReorder}>
|
||||
{orderedPages.map((page) => (
|
||||
{orderedPages.map((page: any) => (
|
||||
<SortableItem
|
||||
id={page.id}
|
||||
key={page.id}
|
||||
@@ -243,6 +242,7 @@ const CustomPages = ({
|
||||
onClose={close}
|
||||
size="lg"
|
||||
title={intl.formatMessage(messages.studentViewModalTitle)}
|
||||
isOverflowVisible={false}
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
@@ -275,8 +275,4 @@ const CustomPages = ({
|
||||
);
|
||||
};
|
||||
|
||||
CustomPages.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CustomPages;
|
||||
@@ -2,6 +2,36 @@ import { camelCaseObject, getConfig, snakeCaseObject } from '@edx/frontend-platf
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL as string;
|
||||
export const getCourseDetailsUrl = (courseId: string, username: string) => (
|
||||
`${getConfig().LMS_BASE_URL}/api/courses/v1/courses/${courseId}?username=${username}`
|
||||
);
|
||||
|
||||
export type CourseDetailsData = {
|
||||
blocksUrl: string;
|
||||
courseId: string;
|
||||
effort?: string;
|
||||
end?: string;
|
||||
enrollmentEnd?: string;
|
||||
enrollmentStart?: string;
|
||||
hidden: boolean;
|
||||
id: string;
|
||||
invitationOnly: boolean;
|
||||
isEnrolled: boolean;
|
||||
media: Record<
|
||||
'image' | 'course_image' | 'banner_image' | 'course_video',
|
||||
Record<string, string | null>
|
||||
>;
|
||||
mobileAvailable: boolean;
|
||||
name: string;
|
||||
number: string;
|
||||
org: string;
|
||||
overview: string;
|
||||
pacing: string;
|
||||
shortDescription?: string;
|
||||
start?: string;
|
||||
startDisplay?: string;
|
||||
startType?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the URL to check the migration task status
|
||||
@@ -20,20 +50,15 @@ export const getApiWaffleFlagsUrl = (courseId?: string): string => {
|
||||
return courseId ? `${baseUrl}${apiPath}/${courseId}` : `${baseUrl}${apiPath}`;
|
||||
};
|
||||
|
||||
function normalizeCourseDetail(data) {
|
||||
export async function getCourseDetails(courseId: string, username: string): Promise<CourseDetailsData> {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getCourseDetailsUrl(courseId, username));
|
||||
return {
|
||||
id: data.course_id,
|
||||
...camelCaseObject(data),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCourseDetail(courseId: string, username: string) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getConfig().LMS_BASE_URL}/api/courses/v1/courses/${courseId}?username=${username}`);
|
||||
|
||||
return normalizeCourseDetail(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* The default values of waffle flags, used while we're loading the "real"
|
||||
* values from Studio's REST API, and/or if we fail to load them.
|
||||
@@ -80,7 +105,10 @@ export type WaffleFlagsStatus = { id: string | undefined } & Record<WaffleFlagNa
|
||||
export async function getWaffleFlags(courseId?: string): Promise<WaffleFlagsStatus> {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getApiWaffleFlagsUrl(courseId));
|
||||
return normalizeCourseDetail(data);
|
||||
return {
|
||||
id: data.course_id,
|
||||
...camelCaseObject(data),
|
||||
};
|
||||
}
|
||||
|
||||
export interface MigrateParameters {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
skipToken, useMutation, useQuery, useQueryClient,
|
||||
} from '@tanstack/react-query';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { libraryAuthoringQueryKeys } from '@src/library-authoring/data/apiHooks';
|
||||
import {
|
||||
getWaffleFlags,
|
||||
@@ -8,7 +9,9 @@ import {
|
||||
bulkModulestoreMigrate,
|
||||
getModulestoreMigrationStatus,
|
||||
BulkMigrateRequestData,
|
||||
getCourseDetails,
|
||||
} from './api';
|
||||
import { RequestStatus, RequestStatusType } from './constants';
|
||||
|
||||
export const migrationQueryKeys = {
|
||||
all: ['contentLibrary'],
|
||||
@@ -18,6 +21,14 @@ export const migrationQueryKeys = {
|
||||
migrationTask: (migrationId?: string | null) => [...migrationQueryKeys.all, migrationId],
|
||||
};
|
||||
|
||||
export const courseDetailsKey = {
|
||||
all: ['courseDetails'],
|
||||
/**
|
||||
* Base key for get course details data.
|
||||
*/
|
||||
courseDetails: (courseId: string) => [...courseDetailsKey.all, courseId],
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the waffle flags (which enable/disable specific features). They may
|
||||
* depend on which course we're in.
|
||||
@@ -72,3 +83,38 @@ export const useModulestoreMigrationStatus = (migrationId: string | null, refetc
|
||||
refetchInterval,
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Get details of a course
|
||||
*/
|
||||
export const useCourseDetails = (courseId: string) => {
|
||||
const query = useQuery({
|
||||
queryKey: courseDetailsKey.courseDetails(courseId),
|
||||
queryFn: () => getCourseDetails(courseId, getAuthenticatedUser().username),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Include a status summary field for now, to better match the old redux data
|
||||
* loading status that other components expect. This could be changed/removed in the future.
|
||||
*/
|
||||
let status: RequestStatusType = RequestStatus.PENDING;
|
||||
|
||||
if (query.isLoading) {
|
||||
status = RequestStatus.IN_PROGRESS;
|
||||
} else if (query.isSuccess) {
|
||||
status = RequestStatus.SUCCESSFUL;
|
||||
} else if (query.error) {
|
||||
const errorStatus = (query.error as any)?.response?.status;
|
||||
if (errorStatus === 404) {
|
||||
status = RequestStatus.NOT_FOUND;
|
||||
} else {
|
||||
status = RequestStatus.FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...query,
|
||||
status,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { type RequestStatusType } from './constants';
|
||||
|
||||
export const LOADED = 'LOADED';
|
||||
|
||||
const slice = createSlice({
|
||||
name: 'courseDetail',
|
||||
initialState: {
|
||||
courseId: null as string | null,
|
||||
status: null as RequestStatusType | null,
|
||||
canChangeProviders: null as null | boolean,
|
||||
},
|
||||
reducers: {
|
||||
updateStatus: (state, { payload }) => {
|
||||
state.courseId = payload.courseId;
|
||||
state.status = payload.status;
|
||||
},
|
||||
updateCanChangeProviders: (state, { payload }) => {
|
||||
state.canChangeProviders = payload.canChangeProviders;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
updateStatus,
|
||||
updateCanChangeProviders,
|
||||
} = slice.actions;
|
||||
|
||||
export const {
|
||||
reducer,
|
||||
} = slice;
|
||||
@@ -1,30 +0,0 @@
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { addModel } from '../generic/model-store';
|
||||
import { getCourseDetail } from './api';
|
||||
import {
|
||||
updateStatus,
|
||||
updateCanChangeProviders,
|
||||
} from './slice';
|
||||
import { RequestStatus } from './constants';
|
||||
|
||||
export function fetchCourseDetail(courseId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateStatus({ courseId, status: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
try {
|
||||
const courseDetail = await getCourseDetail(courseId, getAuthenticatedUser().username);
|
||||
dispatch(updateStatus({ courseId, status: RequestStatus.SUCCESSFUL }));
|
||||
|
||||
dispatch(addModel({ modelType: 'courseDetails', model: courseDetail }));
|
||||
dispatch(updateCanChangeProviders({
|
||||
canChangeProviders: getAuthenticatedUser().administrator || new Date(courseDetail.start) > new Date(),
|
||||
}));
|
||||
} catch (error) {
|
||||
if ((error as any).response && (error as any).response.status === 404) {
|
||||
dispatch(updateStatus({ courseId, status: RequestStatus.NOT_FOUND }));
|
||||
} else {
|
||||
dispatch(updateStatus({ courseId, status: RequestStatus.FAILED }));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Cookies from 'universal-cookie';
|
||||
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { getCourseDetailsUrl } from '@src/data/api';
|
||||
import {
|
||||
initializeMocks,
|
||||
fireEvent,
|
||||
@@ -23,12 +25,6 @@ let cookies;
|
||||
const courseId = '123';
|
||||
const courseName = 'About Node JS';
|
||||
|
||||
jest.mock('../generic/model-store', () => ({
|
||||
useModel: jest.fn().mockReturnValue({
|
||||
name: courseName,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('universal-cookie', () => {
|
||||
const mCookie = {
|
||||
get: jest.fn(),
|
||||
@@ -37,16 +33,27 @@ jest.mock('universal-cookie', () => {
|
||||
return jest.fn(() => mCookie);
|
||||
});
|
||||
|
||||
const renderComponent = () => render(<CourseExportPage courseId={courseId} />);
|
||||
const renderComponent = () => render(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<CourseExportPage />
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
|
||||
describe('<CourseExportPage />', () => {
|
||||
beforeEach(() => {
|
||||
const mocks = initializeMocks();
|
||||
const user = {
|
||||
userId: 1,
|
||||
username: 'username',
|
||||
};
|
||||
const mocks = initializeMocks({ user });
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
axiosMock
|
||||
.onGet(postExportCourseApiUrl(courseId))
|
||||
.reply(200, exportPageMock);
|
||||
axiosMock
|
||||
.onGet(getCourseDetailsUrl(courseId, user.username))
|
||||
.reply(200, { courseId, name: courseName });
|
||||
cookies = new Cookies();
|
||||
cookies.get.mockReturnValue(null);
|
||||
});
|
||||
|
||||
@@ -9,11 +9,11 @@ import Cookies from 'universal-cookie';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { useModel } from '../generic/model-store';
|
||||
import messages from './messages';
|
||||
import ExportSidebar from './export-sidebar/ExportSidebar';
|
||||
import {
|
||||
@@ -26,11 +26,11 @@ import ExportModalError from './export-modal-error/ExportModalError';
|
||||
import ExportFooter from './export-footer/ExportFooter';
|
||||
import ExportStepper from './export-stepper/ExportStepper';
|
||||
|
||||
const CourseExportPage = ({ courseId }: { courseId: string }) => {
|
||||
const CourseExportPage = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const exportTriggered = useSelector(getExportTriggered);
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||
const currentStage = useSelector(getCurrentStage);
|
||||
const { msg: errorMessage } = useSelector(getError);
|
||||
const loadingStatus = useSelector(getLoadingStatus);
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Container } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CourseFilesSlot from '../../plugin-slots/CourseFilesSlot';
|
||||
import Placeholder from '../../editors/Placeholder';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import getPageHeadTitle from '../../generic/utils';
|
||||
import EditFileAlertsSlot from '../../plugin-slots/EditFileAlertsSlot';
|
||||
import { Container } from '@openedx/paragon';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import CourseFilesSlot from '@src/plugin-slots/CourseFilesSlot';
|
||||
import Placeholder from '@src/editors/Placeholder';
|
||||
import { RequestStatus } from '@src/data/constants';
|
||||
import getPageHeadTitle from '@src/generic/utils';
|
||||
import EditFileAlertsSlot from '@src/plugin-slots/EditFileAlertsSlot';
|
||||
|
||||
import { EditFileErrors } from '../generic';
|
||||
import { fetchAssets, resetErrors } from './data/thunks';
|
||||
import FilesPageProvider from './FilesPageProvider';
|
||||
import messages from './messages';
|
||||
import './FilesPage.scss';
|
||||
|
||||
const FilesPage = ({
|
||||
courseId,
|
||||
}) => {
|
||||
const FilesPage = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
|
||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||
document.title = getPageHeadTitle(courseDetails?.name || '', intl.formatMessage(messages.heading));
|
||||
const {
|
||||
loadingStatus,
|
||||
addingStatus: addAssetStatus,
|
||||
@@ -68,8 +67,4 @@ const FilesPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
FilesPage.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default FilesPage;
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
|
||||
import {
|
||||
render,
|
||||
fireEvent,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import { executeThunk } from '../../utils';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
initializeMocks,
|
||||
} from '@src/testUtils';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { executeThunk } from '@src/utils';
|
||||
import { RequestStatus } from '@src/data/constants';
|
||||
import FilesPage from './FilesPage';
|
||||
import {
|
||||
generateFetchAssetApiResponse,
|
||||
@@ -51,20 +47,16 @@ jest.mock('file-saver');
|
||||
|
||||
const renderComponent = () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<MemoryRouter initialEntries={[`/course/${courseId}/videos`]}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/course/:courseId/*"
|
||||
element={
|
||||
<FilesPage courseId={courseId} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<FilesPage />,
|
||||
</CourseAuthoringProvider>,
|
||||
{
|
||||
path: '/course/:courseId/*',
|
||||
routerProps: {
|
||||
initialEntries: [`/course/${courseId}/videos`],
|
||||
},
|
||||
params: { courseId },
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -91,24 +83,19 @@ const emptyMockStore = async (status) => {
|
||||
|
||||
describe('FilesAndUploads', () => {
|
||||
describe('empty state', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
beforeEach(() => {
|
||||
const mocks = initializeMocks({
|
||||
initialState: {
|
||||
...initialState,
|
||||
assets: {
|
||||
...initialState.assets,
|
||||
assetIds: [],
|
||||
},
|
||||
models: {},
|
||||
},
|
||||
});
|
||||
store = initializeStore({
|
||||
...initialState,
|
||||
assets: {
|
||||
...initialState.assets,
|
||||
assetIds: [],
|
||||
},
|
||||
models: {},
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
file = new File(['(⌐□_□)'], 'download.png', { type: 'image/png' });
|
||||
});
|
||||
|
||||
@@ -152,17 +139,19 @@ describe('FilesAndUploads', () => {
|
||||
});
|
||||
|
||||
describe('valid assets', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
beforeEach(() => {
|
||||
const mocks = initializeMocks({
|
||||
initialState: {
|
||||
...initialState,
|
||||
assets: {
|
||||
...initialState.assets,
|
||||
assetIds: [],
|
||||
},
|
||||
models: {},
|
||||
},
|
||||
});
|
||||
store = initializeStore(initialState);
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
file = new File(['(⌐□_□)'], 'download.png', { type: 'image/png' });
|
||||
global.localStorage.clear();
|
||||
});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import {
|
||||
render,
|
||||
act,
|
||||
@@ -5,20 +8,13 @@ import {
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
initializeMocks,
|
||||
} from '@src/testUtils';
|
||||
import { getHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { executeThunk } from '@src/utils';
|
||||
import { RequestStatus } from '@src/data/constants';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient, getHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import React from 'react';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import { executeThunk } from '../../utils';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import VideosPage from './VideosPage';
|
||||
import {
|
||||
generateFetchVideosApiResponse,
|
||||
@@ -51,20 +47,16 @@ jest.mock('file-saver');
|
||||
|
||||
const renderComponent = () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<MemoryRouter initialEntries={[`/course/${courseId}/videos`]}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/course/:courseId/*"
|
||||
element={
|
||||
<VideosPage courseId={courseId} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<VideosPage />
|
||||
</CourseAuthoringProvider>,
|
||||
{
|
||||
path: '/course/:courseId/*',
|
||||
routerProps: {
|
||||
initialEntries: [`/course/${courseId}/videos`],
|
||||
},
|
||||
params: { courseId },
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -102,23 +94,19 @@ const emptyMockStore = async (status) => {
|
||||
describe('Videos page', () => {
|
||||
describe('empty state', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
const mocks = initializeMocks({
|
||||
// @ts-ignore
|
||||
initialState: {
|
||||
...initialState,
|
||||
videos: {
|
||||
...initialState.videos,
|
||||
videoIds: [],
|
||||
},
|
||||
models: {},
|
||||
},
|
||||
});
|
||||
store = initializeStore({
|
||||
...initialState,
|
||||
videos: {
|
||||
...initialState.videos,
|
||||
videoIds: [],
|
||||
},
|
||||
models: {},
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
axiosUnauthenticateMock = new MockAdapter(getHttpClient());
|
||||
file = new File(['(⌐□_□)'], 'download.mp4', { type: 'video/mp4' });
|
||||
global.localStorage.clear();
|
||||
@@ -166,16 +154,15 @@ describe('Videos page', () => {
|
||||
|
||||
describe('valid videos', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
const mocks = initializeMocks({
|
||||
// @ts-ignore
|
||||
initialState: {
|
||||
...initialState,
|
||||
},
|
||||
});
|
||||
store = initializeStore({ ...initialState });
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
axiosUnauthenticateMock = new MockAdapter(getHttpClient());
|
||||
file = new File(['(⌐□_□)'], 'download.png', { type: 'image/png' });
|
||||
global.localStorage.clear();
|
||||
@@ -271,6 +258,7 @@ describe('Videos page', () => {
|
||||
axiosMock.onGet(getCourseVideosApiUrl(courseId)).reply(200, generateAddVideoApiResponse());
|
||||
|
||||
const uploadSpy = jest.spyOn(api, 'uploadVideo');
|
||||
// @ts-ignore
|
||||
const setFailedSpy = jest.spyOn(api, 'sendVideoUploadStatus').mockImplementation(() => {});
|
||||
uploadSpy.mockResolvedValue(new Promise(() => {}));
|
||||
|
||||
@@ -301,6 +289,7 @@ describe('Videos page', () => {
|
||||
axiosMock.onGet(getCourseVideosApiUrl(courseId)).reply(200, generateAddVideoApiResponse());
|
||||
|
||||
const uploadSpy = jest.spyOn(api, 'uploadVideo');
|
||||
// @ts-ignore
|
||||
const setFailedSpy = jest.spyOn(api, 'sendVideoUploadStatus').mockImplementation(() => {});
|
||||
uploadSpy.mockResolvedValue(new Promise(() => {}));
|
||||
|
||||
@@ -356,13 +345,14 @@ describe('Videos page', () => {
|
||||
fireEvent.click(actionsButton);
|
||||
|
||||
const deleteButton = screen.getByText(messages.deleteTitle.defaultMessage).closest('a');
|
||||
expect(deleteButton).not.toBeNull();
|
||||
expect(deleteButton).not.toHaveClass('disabled');
|
||||
|
||||
axiosMock.onDelete(`${getCourseVideosApiUrl(courseId)}/mOckID1`).reply(204);
|
||||
|
||||
fireEvent.click(deleteButton);
|
||||
fireEvent.click(deleteButton!);
|
||||
expect(screen.getByText('Delete mOckID1.mp4')).toBeVisible();
|
||||
fireEvent.click(deleteButton);
|
||||
fireEvent.click(deleteButton!);
|
||||
|
||||
// Wait for the delete confirmation button to appear
|
||||
const confirmDeleteButton = await screen.findByRole('button', {
|
||||
@@ -391,10 +381,11 @@ describe('Videos page', () => {
|
||||
axiosMock.onPut(`${getVideosUrl(courseId)}/download`).reply(200, null);
|
||||
|
||||
const downloadButton = screen.getByText(messages.downloadTitle.defaultMessage).closest('a');
|
||||
expect(downloadButton).not.toBeNull();
|
||||
expect(downloadButton).not.toHaveClass('disabled');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(downloadButton);
|
||||
fireEvent.click(downloadButton!);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -594,7 +585,7 @@ describe('Videos page', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Delete mOckID1.mp4')).toBeNull();
|
||||
});
|
||||
await executeThunk(deleteVideoFile(courseId, 'mOckID1', 5), store.dispatch);
|
||||
await executeThunk(deleteVideoFile(courseId, 'mOckID1'), store.dispatch);
|
||||
const deleteStatus = store.getState().videos.deletingStatus;
|
||||
expect(deleteStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||
|
||||
@@ -733,10 +724,12 @@ describe('Videos page', () => {
|
||||
|
||||
fireEvent.click(actionsButton);
|
||||
const downloadButton = screen.getByText(messages.downloadTitle.defaultMessage).closest('a');
|
||||
expect(downloadButton).not.toBeNull();
|
||||
expect(downloadButton).not.toHaveClass('disabled');
|
||||
|
||||
axiosMock.onPut(`${getVideosUrl(courseId)}/download`).reply(404);
|
||||
fireEvent.click(downloadButton);
|
||||
fireEvent.click(downloadButton!);
|
||||
// @ts-ignore
|
||||
await executeThunk(fetchVideoDownload([{ original: { displayName: 'mOckID1', id: '2', downloadLink: 'test' } }]), store.dispatch);
|
||||
|
||||
const updateStatus = store.getState().videos.updatingStatus;
|
||||
@@ -1,34 +1,33 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Container } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CourseVideosSlot from '../../plugin-slots/CourseVideosSlot';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
import Placeholder from '../../editors/Placeholder';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import getPageHeadTitle from '../../generic/utils';
|
||||
import EditVideoAlertsSlot from '../../plugin-slots/EditVideoAlertsSlot';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Container } from '@openedx/paragon';
|
||||
import CourseVideosSlot from '@src/plugin-slots/CourseVideosSlot';
|
||||
import { RequestStatus } from '@src/data/constants';
|
||||
import Placeholder from '@src/editors/Placeholder';
|
||||
import getPageHeadTitle from '@src/generic/utils';
|
||||
import EditVideoAlertsSlot from '@src/plugin-slots/EditVideoAlertsSlot';
|
||||
|
||||
import { DeprecatedReduxState } from '@src/store';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import { EditFileErrors } from '../generic';
|
||||
import { fetchVideos, resetErrors } from './data/thunks';
|
||||
import messages from './messages';
|
||||
import VideosPageProvider from './VideosPageProvider';
|
||||
|
||||
const VideosPage = ({
|
||||
courseId,
|
||||
}) => {
|
||||
const VideosPage = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||
const {
|
||||
loadingStatus,
|
||||
addingStatus: addVideoStatus,
|
||||
deletingStatus: deleteVideoStatus,
|
||||
updatingStatus: updateVideoStatus,
|
||||
errors: errorMessages,
|
||||
} = useSelector((state) => state.videos);
|
||||
} = useSelector((state: DeprecatedReduxState) => state.videos);
|
||||
|
||||
const handleErrorReset = (error) => dispatch(resetErrors(error));
|
||||
|
||||
@@ -47,7 +46,7 @@ const VideosPage = ({
|
||||
return (
|
||||
<VideosPageProvider courseId={courseId}>
|
||||
<Helmet>
|
||||
<title>{getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading))}</title>
|
||||
<title>{getPageHeadTitle(courseDetails?.name || '', intl.formatMessage(messages.heading))}</title>
|
||||
</Helmet>
|
||||
<Container size="xl" className="p-4 pt-4.5">
|
||||
<EditFileErrors
|
||||
@@ -66,8 +65,4 @@ const VideosPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
VideosPage.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default VideosPage;
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import TinyMceWidget, { prepareEditorRef } from '../editors/sharedComponents/TinyMceWidget';
|
||||
|
||||
import { DEFAULT_EMPTY_WYSIWYG_VALUE } from '../constants';
|
||||
@@ -14,7 +13,7 @@ export const WysiwygEditor = ({
|
||||
initialValue, editorType, onChange, minHeight,
|
||||
}) => {
|
||||
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
|
||||
const { courseId } = useSelector((state) => state.courseDetail);
|
||||
const { courseId } = useCourseAuthoringContext();
|
||||
const isEquivalentCodeExtraSpaces = (first, second) => {
|
||||
// Utils allows to compare code extra spaces
|
||||
const removeWhitespace = (str) => str.replace(/\s/g, '');
|
||||
|
||||
@@ -8,14 +8,13 @@ import {
|
||||
useGradingSettings,
|
||||
useGradingSettingUpdater,
|
||||
} from 'CourseAuthoring/grading-settings/data/apiHooks';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import { STATEFUL_BUTTON_STATES } from '../constants';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
|
||||
import { useModel } from '../generic/model-store';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
import SectionSubHeader from '../generic/section-sub-header';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
@@ -28,8 +27,9 @@ import GradingSidebar from './grading-sidebar';
|
||||
import { useConvertGradeCutoffs, useUpdateGradingData } from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
const GradingSettings = ({ courseId }) => {
|
||||
const GradingSettings = () => {
|
||||
const intl = useIntl();
|
||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||
const {
|
||||
data: gradingSettings,
|
||||
isLoading: isGradingSettingsLoading,
|
||||
@@ -56,7 +56,7 @@ const GradingSettings = ({ courseId }) => {
|
||||
const [showOverrideInternetConnectionAlert, setOverrideInternetConnectionAlert] = useState(false);
|
||||
const [eligibleGrade, setEligibleGrade] = useState(null);
|
||||
|
||||
const courseName = useModel('courseDetails', courseId)?.name;
|
||||
const courseName = courseDetails?.name || '';
|
||||
|
||||
const {
|
||||
graders,
|
||||
@@ -279,8 +279,4 @@ const GradingSettings = ({ courseId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
GradingSettings.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default GradingSettings;
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import {
|
||||
act, fireEvent, render, screen,
|
||||
} from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
act, fireEvent, render, screen, initializeMocks,
|
||||
} from '@src/testUtils';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
|
||||
import initializeStore from '../store';
|
||||
import gradingSettings from './__mocks__/gradingSettings';
|
||||
import { getCourseSettingsApiUrl, getGradingSettingsApiUrl } from './data/api';
|
||||
import * as apiHooks from './data/apiHooks';
|
||||
@@ -17,33 +11,21 @@ import messages from './messages';
|
||||
|
||||
const courseId = '123';
|
||||
let axiosMock;
|
||||
let store;
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<GradingSettings courseId={courseId} />
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<GradingSettings />
|
||||
</CourseAuthoringProvider>
|
||||
);
|
||||
|
||||
describe('<GradingSettings />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3, username: 'abc123', administrator: true, roles: [],
|
||||
},
|
||||
});
|
||||
const mocks = initializeMocks();
|
||||
|
||||
// jsdom doesn't implement scrollTo; mock to avoid noisy console errors.
|
||||
Object.defineProperty(window, 'scrollTo', { value: jest.fn(), writable: true });
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock = mocks.axiosMock;
|
||||
axiosMock
|
||||
.onGet(getGradingSettingsApiUrl(courseId))
|
||||
.reply(200, gradingSettings);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import {
|
||||
initializeMocks,
|
||||
render,
|
||||
@@ -20,7 +21,11 @@ const courseId = 'course-v1:org+101+101';
|
||||
const enrollmentTrackGroups = groupConfigurationResponseMock.allGroupConfigurations[0];
|
||||
const contentGroups = groupConfigurationResponseMock.allGroupConfigurations[1];
|
||||
|
||||
const renderComponent = () => render(<GroupConfigurations courseId={courseId} />);
|
||||
const renderComponent = () => render(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<GroupConfigurations />
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
|
||||
describe('<GroupConfigurations />', () => {
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Container, Layout, Stack, Row,
|
||||
} from '@openedx/paragon';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
|
||||
import { LoadingSpinner } from '../generic/Loading';
|
||||
import { useModel } from '../generic/model-store';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import ProcessingNotification from '../generic/processing-notification';
|
||||
@@ -18,9 +17,9 @@ import GroupConfigurationSidebar from './group-configuration-sidebar';
|
||||
import { useGroupConfigurations } from './hooks';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
|
||||
const GroupConfigurations = ({ courseId }) => {
|
||||
const GroupConfigurations = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||
const {
|
||||
isLoading,
|
||||
savingStatus,
|
||||
@@ -128,8 +127,4 @@ const GroupConfigurations = ({ courseId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
GroupConfigurations.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default GroupConfigurations;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Cookies from 'universal-cookie';
|
||||
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { getCourseDetailsUrl } from '@src/data/api';
|
||||
import { initializeMocks, render, waitFor } from '../testUtils';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import messages from './messages';
|
||||
@@ -15,12 +17,6 @@ let cookies;
|
||||
const courseId = '123';
|
||||
const courseName = 'About Node JS';
|
||||
|
||||
jest.mock('../generic/model-store', () => ({
|
||||
useModel: jest.fn().mockReturnValue({
|
||||
name: courseName,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('universal-cookie', () => {
|
||||
const Cookie = {
|
||||
get: jest.fn(),
|
||||
@@ -29,16 +25,27 @@ jest.mock('universal-cookie', () => {
|
||||
return jest.fn(() => Cookie);
|
||||
});
|
||||
|
||||
const renderComponent = () => render(<CourseImportPage courseId={courseId} />);
|
||||
const renderComponent = () => render(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<CourseImportPage />
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
|
||||
describe('<CourseImportPage />', () => {
|
||||
beforeEach(() => {
|
||||
const mocks = initializeMocks();
|
||||
const user = {
|
||||
userId: 1,
|
||||
username: 'username',
|
||||
};
|
||||
const mocks = initializeMocks({ user });
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
axiosMock
|
||||
.onGet(getImportStatusApiUrl(courseId, 'testFileName.test'))
|
||||
.reply(200, { importStatus: 1, message: '' });
|
||||
axiosMock
|
||||
.onGet(getCourseDetailsUrl(courseId, user.username))
|
||||
.reply(200, { courseId, name: courseName });
|
||||
cookies = new Cookies();
|
||||
cookies.get.mockReturnValue(null);
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable max-len */
|
||||
import React, { useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
@@ -8,11 +8,12 @@ import {
|
||||
import Cookies from 'universal-cookie';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { useModel } from '../generic/model-store';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
import SubHeader from '@src/generic/sub-header/SubHeader';
|
||||
import InternetConnectionAlert from '@src/generic/internet-connection-alert';
|
||||
import { RequestStatus } from '@src/data/constants';
|
||||
import ConnectionErrorAlert from '@src/generic/ConnectionErrorAlert';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
|
||||
import {
|
||||
updateFileName, updateImportTriggered, updateSavingStatus, updateSuccessDate,
|
||||
} from './data/slice';
|
||||
@@ -23,11 +24,11 @@ import ImportSidebar from './import-sidebar/ImportSidebar';
|
||||
import FileSection from './file-section/FileSection';
|
||||
import messages from './messages';
|
||||
|
||||
const CourseImportPage = ({ courseId }: { courseId: string }) => {
|
||||
const CourseImportPage = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const cookies = new Cookies();
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||
const importTriggered = useSelector(getImportTriggered);
|
||||
const savingStatus = useSelector(getSavingStatus);
|
||||
const loadingStatus = useSelector(getLoadingStatus);
|
||||
|
||||
@@ -2,14 +2,9 @@
|
||||
/* eslint-disable react/jsx-filename-extension */
|
||||
import {
|
||||
fireEvent, render, waitFor, screen,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import initializeStore from '../store';
|
||||
initializeMocks,
|
||||
} from '@src/testUtils';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import messages from './messages';
|
||||
import generalMessages from '../messages';
|
||||
import scanResultsMessages from './scan-results/messages';
|
||||
@@ -24,36 +19,21 @@ import {
|
||||
import * as thunks from './data/thunks';
|
||||
import { useWaffleFlags } from '../data/apiHooks';
|
||||
|
||||
let store;
|
||||
let axiosMock;
|
||||
const courseId = '123';
|
||||
const courseName = 'About Node JS';
|
||||
|
||||
jest.mock('../generic/model-store', () => ({
|
||||
useModel: jest.fn().mockReturnValue({
|
||||
name: courseName,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the waffle flags hook
|
||||
jest.mock('../data/apiHooks', () => ({
|
||||
...jest.requireActual('../data/apiHooks'),
|
||||
useWaffleFlags: jest.fn(() => ({
|
||||
enableCourseOptimizerCheckPrevRunLinks: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../generic/model-store', () => ({
|
||||
useModel: jest.fn().mockReturnValue({
|
||||
name: 'About Node JS',
|
||||
}),
|
||||
}));
|
||||
|
||||
const OptimizerPage = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<CourseOptimizerPage courseId={courseId} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<CourseOptimizerPage />
|
||||
</CourseAuthoringProvider>
|
||||
);
|
||||
|
||||
const setupOptimizerPage = async (apiResponse = mockApiResponse) => {
|
||||
@@ -132,16 +112,8 @@ describe('CourseOptimizerPage', () => {
|
||||
beforeEach(() => {
|
||||
jest.useRealTimers();
|
||||
jest.clearAllMocks();
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
const mocks = initializeMocks();
|
||||
axiosMock = mocks.axiosMock;
|
||||
axiosMock
|
||||
.onPost(postLinkCheckCourseApiUrl(courseId))
|
||||
.reply(200, { LinkCheckStatus: 'In-Progress' });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import {
|
||||
useEffect, useState, useRef, FC, MutableRefObject,
|
||||
useEffect, useState, useRef, MutableRefObject,
|
||||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { SpinnerSimple } from '@openedx/paragon/icons';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import CourseStepper from '../generic/course-stepper';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
@@ -22,7 +23,6 @@ import {
|
||||
getLastScannedAt, getRerunLinkUpdateInProgress, getRerunLinkUpdateResult,
|
||||
} from './data/selectors';
|
||||
import { startLinkCheck, fetchLinkCheckStatus, fetchRerunLinkUpdateStatus } from './data/thunks';
|
||||
import { useModel } from '../generic/model-store';
|
||||
import ScanResults from './scan-results';
|
||||
|
||||
const pollLinkCheckStatus = (dispatch: any, courseId: string, delay: number): number => {
|
||||
@@ -74,7 +74,7 @@ export function pollLinkCheckDuringScan(
|
||||
}
|
||||
}
|
||||
|
||||
const CourseOptimizerPage: FC<{ courseId: string }> = ({ courseId }) => {
|
||||
const CourseOptimizerPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const linkCheckInProgress = useSelector(getLinkCheckInProgress);
|
||||
const rerunLinkUpdateInProgress = useSelector(getRerunLinkUpdateInProgress);
|
||||
@@ -88,7 +88,7 @@ const CourseOptimizerPage: FC<{ courseId: string }> = ({ courseId }) => {
|
||||
const isLoadingDenied = (RequestFailureStatuses as string[]).includes(loadingStatus);
|
||||
const interval = useRef<number | undefined>(undefined);
|
||||
const rerunUpdateInterval = useRef<number | undefined>(undefined);
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||
const linkCheckPresent = currentStage != null ? currentStage >= 0 : !!currentStage;
|
||||
const [showStepper, setShowStepper] = useState(false);
|
||||
const [scanResultsError, setScanResultsError] = useState<string | null>(null);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// @ts-check
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
screen, waitFor, initializeMocks, render,
|
||||
} from '@src/testUtils';
|
||||
|
||||
import { getConfig, setConfig } from '@edx/frontend-platform';
|
||||
import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { PagesAndResources } from '.';
|
||||
import { initializeMocks, render } from '../testUtils';
|
||||
|
||||
const mockPlugin = (identifier) => ({
|
||||
plugins: [
|
||||
@@ -22,6 +23,12 @@ const mockPlugin = (identifier) => ({
|
||||
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
|
||||
const renderComponent = () => render(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<PagesAndResources />
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
|
||||
describe('PagesAndResources', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -45,7 +52,7 @@ describe('PagesAndResources', () => {
|
||||
};
|
||||
|
||||
initializeMocks({ initialState });
|
||||
render(<PagesAndResources courseId={courseId} />);
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => expect(screen.queryByRole('heading', { name: 'Content permissions' })).not.toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.queryByTestId('additional_course_plugin')).toBeInTheDocument());
|
||||
@@ -75,7 +82,7 @@ describe('PagesAndResources', () => {
|
||||
};
|
||||
|
||||
initializeMocks({ initialState });
|
||||
render(<PagesAndResources courseId={courseId} />);
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('heading', { name: 'Content permissions' })).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.getByText('Learning Assistant')).toBeInTheDocument());
|
||||
@@ -108,7 +115,7 @@ describe('PagesAndResources', () => {
|
||||
};
|
||||
|
||||
initializeMocks({ initialState });
|
||||
render(<PagesAndResources courseId={courseId} />);
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('heading', { name: 'Content permissions' })).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.getByText('Xpert unit summaries')).toBeInTheDocument());
|
||||
@@ -1,42 +1,43 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { PageWrap, AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { PageWrap, AppContext } from '@edx/frontend-platform/react';
|
||||
import { Button, Hyperlink } from '@openedx/paragon';
|
||||
import { useModels } from '@src/generic/model-store';
|
||||
import { RequestStatus } from '@src/data/constants';
|
||||
import PermissionDeniedAlert from '@src/generic/PermissionDeniedAlert';
|
||||
import getPageHeadTitle from '@src/generic/utils';
|
||||
import { AdditionalCoursePluginSlot } from '@src/plugin-slots/AdditionalCoursePluginSlot';
|
||||
import { AdditionalCourseContentPluginSlot } from '@src/plugin-slots/AdditionalCourseContentPluginSlot';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import { DeprecatedReduxState } from '@src/store';
|
||||
|
||||
import messages from './messages';
|
||||
import DiscussionsSettings from './discussions';
|
||||
|
||||
import PageGrid from './pages/PageGrid';
|
||||
import { fetchCourseApps } from './data/thunks';
|
||||
import { useModels, useModel } from '../generic/model-store';
|
||||
import { getCourseAppsApiStatus, getLoadingStatus } from './data/selectors';
|
||||
import PagesAndResourcesProvider from './PagesAndResourcesProvider';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import SettingsComponent from './SettingsComponent';
|
||||
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import { AdditionalCoursePluginSlot } from '../plugin-slots/AdditionalCoursePluginSlot';
|
||||
import { AdditionalCourseContentPluginSlot } from '../plugin-slots/AdditionalCourseContentPluginSlot';
|
||||
|
||||
const PagesAndResources = ({ courseId }) => {
|
||||
const PagesAndResources = () => {
|
||||
const intl = useIntl();
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
|
||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||
document.title = getPageHeadTitle(courseDetails?.name || '', intl.formatMessage(messages.heading));
|
||||
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseApps(courseId));
|
||||
}, [courseId]);
|
||||
|
||||
const courseAppIds = useSelector(state => state.pagesAndResources.courseAppIds);
|
||||
const courseAppIds = useSelector((state: DeprecatedReduxState) => state.pagesAndResources.courseAppIds);
|
||||
const loadingStatus = useSelector(getLoadingStatus);
|
||||
const courseAppsApiStatus = useSelector(getCourseAppsApiStatus);
|
||||
|
||||
// @ts-ignore
|
||||
const { config } = useContext(AppContext);
|
||||
const learningCourseURL = `${config.LEARNING_BASE_URL}/course/${courseId}`;
|
||||
const redirectUrl = `/course/${courseId}/pages-and-resources`;
|
||||
@@ -47,7 +48,7 @@ const PagesAndResources = ({ courseId }) => {
|
||||
|
||||
// We want the Xpert learning assistant and unit summaries to appear in the "Content Permissions" section instead,
|
||||
// so we remove them from pages and add them to contentPermissionsPages.
|
||||
const contentPermissionsPages = [];
|
||||
const contentPermissionsPages: any[] = [];
|
||||
|
||||
['xpert_unit_summary', 'learning_assistant'].forEach(separateAppId => {
|
||||
const index = pages.findIndex(app => app.id === separateAppId);
|
||||
@@ -86,9 +87,9 @@ const PagesAndResources = ({ courseId }) => {
|
||||
</div>
|
||||
|
||||
<Routes>
|
||||
<Route path="discussion/configure/:appId" element={<PageWrap><DiscussionsSettings courseId={courseId} /></PageWrap>} />
|
||||
<Route path="discussion" element={<PageWrap><DiscussionsSettings courseId={courseId} /></PageWrap>} />
|
||||
<Route path="discussion/settings" element={<PageWrap><DiscussionsSettings courseId={courseId} /></PageWrap>} />
|
||||
<Route path="discussion/configure/:appId" element={<PageWrap><DiscussionsSettings /></PageWrap>} />
|
||||
<Route path="discussion" element={<PageWrap><DiscussionsSettings /></PageWrap>} />
|
||||
<Route path="discussion/settings" element={<PageWrap><DiscussionsSettings /></PageWrap>} />
|
||||
<Route path=":appId/settings" element={<PageWrap><SettingsComponent url={redirectUrl} /></PageWrap>} />
|
||||
</Routes>
|
||||
|
||||
@@ -109,8 +110,4 @@ const PagesAndResources = ({ courseId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
PagesAndResources.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default PagesAndResources;
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, {
|
||||
useCallback, useContext, useEffect, useState,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -9,7 +8,10 @@ import {
|
||||
Alert, Button, FullscreenModal, Stepper,
|
||||
} from '@openedx/paragon';
|
||||
|
||||
import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import ConnectionErrorAlert from '@src/generic/ConnectionErrorAlert';
|
||||
import PermissionDeniedAlert from '@src/generic/PermissionDeniedAlert';
|
||||
import Loading from '@src/generic/Loading';
|
||||
|
||||
import messages from './messages';
|
||||
import DiscussionsProvider from './DiscussionsProvider';
|
||||
@@ -17,22 +19,18 @@ import { fetchProviders } from './data/thunks';
|
||||
import AppList from './app-list';
|
||||
import AppConfigForm from './app-config-form';
|
||||
import { DENIED, FAILED } from './data/slice';
|
||||
import ConnectionErrorAlert from '../../generic/ConnectionErrorAlert';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import PermissionDeniedAlert from '../../generic/PermissionDeniedAlert';
|
||||
import Loading from '../../generic/Loading';
|
||||
import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
|
||||
|
||||
const SELECTION_STEP = 'selection';
|
||||
const SETTINGS_STEP = 'settings';
|
||||
|
||||
const DiscussionsSettings = ({ courseId }) => {
|
||||
const DiscussionsSettings = () => {
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { path: pagesAndResourcesPath } = useContext(PagesAndResourcesContext);
|
||||
const { status, hasValidationError } = useSelector(state => state.discussions);
|
||||
const { canChangeProviders } = useSelector(state => state.courseDetail);
|
||||
const courseDetail = useModel('courseDetails', courseId);
|
||||
const { courseId, courseDetails, canChangeProviders } = useCourseAuthoringContext();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchProviders(courseId));
|
||||
@@ -56,7 +54,7 @@ const DiscussionsSettings = ({ courseId }) => {
|
||||
navigate(discussionsPath);
|
||||
}, [discussionsPath]);
|
||||
|
||||
if (!courseDetail) {
|
||||
if (!courseDetails) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
@@ -147,8 +145,4 @@ const DiscussionsSettings = ({ courseId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
DiscussionsSettings.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default DiscussionsSettings;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ReactDOM from 'react-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import {
|
||||
getConfig, initializeMockApp, setConfig,
|
||||
} from '@edx/frontend-platform';
|
||||
@@ -10,16 +11,15 @@ import {
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import React from 'react';
|
||||
import {
|
||||
Routes,
|
||||
Route,
|
||||
MemoryRouter,
|
||||
useLocation,
|
||||
} from 'react-router-dom';
|
||||
import { fetchCourseDetail } from '../../data/thunks';
|
||||
import initializeStore from '../../store';
|
||||
import { executeThunk } from '../../utils';
|
||||
import initializeStore from '@src/store';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { getCourseDetailsUrl } from '@src/data/api';
|
||||
import PagesAndResourcesProvider from '../PagesAndResourcesProvider';
|
||||
import ltiMessages from './app-config-form/apps/lti/messages';
|
||||
import appMessages from './app-config-form/messages';
|
||||
@@ -38,6 +38,13 @@ const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
let axiosMock;
|
||||
let store;
|
||||
let container;
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = jest.fn(node => node);
|
||||
@@ -50,21 +57,25 @@ const LocationDisplay = () => {
|
||||
function renderComponent(route) {
|
||||
const wrapper = render(
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<PagesAndResourcesProvider courseId={courseId}>
|
||||
<MemoryRouter initialEntries={[`${route}`]}>
|
||||
<Routes>
|
||||
<Route
|
||||
path={`/course/${courseId}/pages-and-resources/discussion/configure/:appId`}
|
||||
element={<PageWrap><DiscussionsSettings courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path={`/course/${courseId}/pages-and-resources/discussion`}
|
||||
element={<PageWrap><DiscussionsSettings courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
</Routes>
|
||||
<LocationDisplay />
|
||||
</MemoryRouter>
|
||||
</PagesAndResourcesProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<PagesAndResourcesProvider courseId={courseId}>
|
||||
<MemoryRouter initialEntries={[`${route}`]}>
|
||||
<Routes>
|
||||
<Route
|
||||
path={`/course/${courseId}/pages-and-resources/discussion/configure/:appId`}
|
||||
element={<PageWrap><DiscussionsSettings /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path={`/course/${courseId}/pages-and-resources/discussion`}
|
||||
element={<PageWrap><DiscussionsSettings /></PageWrap>}
|
||||
/>
|
||||
</Routes>
|
||||
<LocationDisplay />
|
||||
</MemoryRouter>
|
||||
</PagesAndResourcesProvider>
|
||||
</CourseAuthoringProvider>
|
||||
</QueryClientProvider>
|
||||
</AppProvider>,
|
||||
);
|
||||
container = wrapper.container;
|
||||
@@ -74,25 +85,21 @@ describe('DiscussionsSettings', () => {
|
||||
let user;
|
||||
beforeEach(() => {
|
||||
user = userEvent.setup();
|
||||
const username = 'abc123';
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
username,
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore({
|
||||
models: {
|
||||
courseDetails: {
|
||||
[courseId]: {
|
||||
start: Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
.onGet(getCourseDetailsUrl(courseId, username))
|
||||
.reply(200, { courseId, name: 'Course Test' });
|
||||
});
|
||||
|
||||
describe('with successful network connections', () => {
|
||||
@@ -217,7 +224,6 @@ describe('DiscussionsSettings', () => {
|
||||
|
||||
test('requires confirmation if changing provider', async () => {
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/courses/v1/courses/${courseId}?username=abc123`).reply(200, courseDetailResponse);
|
||||
await executeThunk(fetchCourseDetail(courseId), store.dispatch);
|
||||
|
||||
renderComponent(`/course/${courseId}/pages-and-resources/discussion`);
|
||||
// This is an important line that ensures the spinner has been removed - and thus our main
|
||||
@@ -238,7 +244,6 @@ describe('DiscussionsSettings', () => {
|
||||
|
||||
test('can cancel confirmation', async () => {
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/courses/v1/courses/${courseId}?username=abc123`).reply(200, courseDetailResponse);
|
||||
await executeThunk(fetchCourseDetail(courseId), store.dispatch);
|
||||
|
||||
renderComponent(`/course/${courseId}/pages-and-resources/discussion`);
|
||||
// This is an important line that ensures the spinner has been removed - and thus our main
|
||||
@@ -445,28 +450,26 @@ describe.each([
|
||||
const enablePIISharing = false;
|
||||
|
||||
beforeEach(() => {
|
||||
const username = 'abc123';
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
username,
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore({
|
||||
models: {
|
||||
courseDetails: {
|
||||
[courseId]: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
|
||||
axiosMock.onGet(getDiscussionsProvidersUrl(courseId))
|
||||
.reply(200, generateProvidersApiResponse(false));
|
||||
axiosMock.onGet(getDiscussionsSettingsUrl(courseId))
|
||||
.reply(200, generatePiazzaApiResponse(piiSharingAllowed));
|
||||
axiosMock
|
||||
.onGet(getCourseDetailsUrl(courseId, username))
|
||||
.reply(200, { courseId, name: 'Course Test' });
|
||||
});
|
||||
|
||||
test(`${piiSharingAllowed ? 'shows PII share username/email field when piiSharingAllowed is true'
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import Responsive from 'react-responsive';
|
||||
import {
|
||||
Card, CheckboxControl, breakpoints,
|
||||
} from '@openedx/paragon';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import messages from './messages';
|
||||
import appMessages from '../app-config-form/messages';
|
||||
import FeaturesList from './FeaturesList';
|
||||
@@ -15,7 +14,7 @@ const AppCard = ({
|
||||
app, onClick, selected, features,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { canChangeProviders } = useSelector(state => state.courseDetail);
|
||||
const { canChangeProviders } = useCourseAuthoringContext();
|
||||
const supportText = app.hasFullSupport
|
||||
? intl.formatMessage(messages.appFullSupport)
|
||||
: intl.formatMessage(messages.appBasicSupport);
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import React from 'react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { render, queryByLabelText, queryByTestId } from '@testing-library/react';
|
||||
import {
|
||||
render, queryByLabelText, queryByTestId, initializeMocks,
|
||||
} from '@src/testUtils';
|
||||
import { executeThunk } from '@src/utils';
|
||||
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import AppCard from './AppCard';
|
||||
import messages from './messages';
|
||||
import appMessages from '../app-config-form/messages';
|
||||
import initializeStore from '../../../store';
|
||||
import { executeThunk } from '../../../utils';
|
||||
import { getDiscussionsProvidersUrl } from '../data/api';
|
||||
import { fetchProviders } from '../data/thunks';
|
||||
import { legacyApiResponse } from '../factories/mockApiResponses';
|
||||
@@ -29,17 +25,9 @@ describe('AppCard', () => {
|
||||
let container;
|
||||
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
const mocks = initializeMocks();
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
});
|
||||
|
||||
const mockStore = async (mockResponse) => {
|
||||
@@ -49,16 +37,14 @@ describe('AppCard', () => {
|
||||
|
||||
const createComponent = (data) => {
|
||||
const wrapper = render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<AppCard
|
||||
app={data}
|
||||
onClick={() => jest.fn()}
|
||||
selected={selected}
|
||||
features={[]}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</AppProvider>,
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<AppCard
|
||||
app={data}
|
||||
onClick={() => jest.fn()}
|
||||
selected={selected}
|
||||
features={[]}
|
||||
/>
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
container = wrapper.container;
|
||||
return container;
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
/* eslint-disable react/jsx-no-constructed-context-values */
|
||||
import React from 'react';
|
||||
import {
|
||||
render, screen, within, queryAllByRole, waitFor, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
initializeMocks,
|
||||
} from '@src/testUtils';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { breakpoints } from '@openedx/paragon';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { Context as ResponsiveContext } from 'react-responsive';
|
||||
|
||||
import initializeStore from '../../../store';
|
||||
import { executeThunk } from '../../../utils';
|
||||
import { executeThunk } from '@src/utils';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { getDiscussionsProvidersUrl, getDiscussionsSettingsUrl } from '../data/api';
|
||||
import { fetchDiscussionSettings, fetchProviders } from '../data/thunks';
|
||||
import {
|
||||
@@ -40,13 +35,11 @@ const mockStore = async (mockResponse, provider) => {
|
||||
|
||||
function renderComponent(screenWidth = breakpoints.extraLarge.minWidth) {
|
||||
const wrapper = render(
|
||||
<AppProvider store={store}>
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<ResponsiveContext.Provider value={{ width: screenWidth }}>
|
||||
<IntlProvider locale="en">
|
||||
<AppList />
|
||||
</IntlProvider>
|
||||
<AppList />
|
||||
</ResponsiveContext.Provider>
|
||||
</AppProvider>,
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
container = wrapper.container;
|
||||
}
|
||||
@@ -54,17 +47,10 @@ function renderComponent(screenWidth = breakpoints.extraLarge.minWidth) {
|
||||
describe('AppList', () => {
|
||||
describe('AppList for Admin role', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
const mocks = initializeMocks();
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
await mockStore(piazzaApiResponse);
|
||||
});
|
||||
|
||||
@@ -154,8 +140,8 @@ describe('AppList', () => {
|
||||
|
||||
describe('AppList for Non Admin role', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
const mocks = initializeMocks({
|
||||
user: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
@@ -163,8 +149,8 @@ describe('AppList', () => {
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
await mockStore(legacyApiResponse, 'legacy');
|
||||
});
|
||||
|
||||
|
||||
@@ -3,13 +3,12 @@ import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { DivisionSchemes } from '../../../data/constants';
|
||||
import { LOADED } from '../../../data/slice';
|
||||
import initializeStore from '../../../store';
|
||||
import { executeThunk } from '../../../utils';
|
||||
import { generateProvidersApiResponse, legacyApiResponse, piazzaApiResponse } from '../factories/mockApiResponses';
|
||||
import { getDiscussionsProvidersUrl, getDiscussionsSettingsUrl } from './api';
|
||||
import {
|
||||
DENIED, FAILED, SAVED, selectApp, updateValidationStatus,
|
||||
DENIED, FAILED, SAVED, LOADED, selectApp, updateValidationStatus,
|
||||
} from './slice';
|
||||
import { fetchDiscussionSettings, fetchProviders, saveProviderConfig } from './thunks';
|
||||
|
||||
|
||||
@@ -5,17 +5,19 @@ import {
|
||||
render,
|
||||
waitFor,
|
||||
fireEvent,
|
||||
} from '../testUtils';
|
||||
import { executeThunk } from '../utils';
|
||||
} from '@src/testUtils';
|
||||
import { executeThunk } from '@src/utils';
|
||||
import genericMessages from '@src/generic/help-sidebar/messages';
|
||||
import { DATE_FORMAT } from '@src/constants';
|
||||
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { courseDetailsMock, courseSettingsMock } from './__mocks__';
|
||||
import { getCourseDetailsApiUrl, getCourseSettingsApiUrl } from './data/api';
|
||||
import { updateCourseDetailsQuery } from './data/thunks';
|
||||
import { DATE_FORMAT } from '../constants';
|
||||
import creditMessages from './credit-section/messages';
|
||||
import pacingMessages from './pacing-section/messages';
|
||||
import basicMessages from './basic-section/messages';
|
||||
import scheduleMessages from './schedule-section/messages';
|
||||
import genericMessages from '../generic/help-sidebar/messages';
|
||||
import messages from './messages';
|
||||
import ScheduleAndDetails from '.';
|
||||
|
||||
@@ -47,6 +49,12 @@ jest.mock('react-textarea-autosize', () => jest.fn((props) => (
|
||||
<textarea {...props} onFocus={() => {}} onBlur={() => {}} />
|
||||
)));
|
||||
|
||||
const renderComponent = () => render(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<ScheduleAndDetails />
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
|
||||
describe('<ScheduleAndDetails />', () => {
|
||||
beforeEach(() => {
|
||||
const mocks = initializeMocks();
|
||||
@@ -64,7 +72,7 @@ describe('<ScheduleAndDetails />', () => {
|
||||
});
|
||||
|
||||
it('should render without errors', async () => {
|
||||
const { getByText, getByRole, getAllByText } = render(<ScheduleAndDetails courseId={courseId} />);
|
||||
const { getByText, getByRole, getAllByText } = renderComponent();
|
||||
await waitFor(() => {
|
||||
const scheduleAndDetailElements = getAllByText(messages.headingTitle.defaultMessage);
|
||||
const scheduleAndDetailTitle = scheduleAndDetailElements[0];
|
||||
@@ -99,7 +107,7 @@ describe('<ScheduleAndDetails />', () => {
|
||||
.onGet(getCourseSettingsApiUrl(courseId))
|
||||
.reply(200, updatedResponse);
|
||||
|
||||
const { queryAllByText } = render(<ScheduleAndDetails courseId={courseId} />);
|
||||
const { queryAllByText } = renderComponent();
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
queryAllByText(creditMessages.creditTitle.defaultMessage).length,
|
||||
@@ -108,9 +116,7 @@ describe('<ScheduleAndDetails />', () => {
|
||||
});
|
||||
|
||||
it('should show save alert onChange ', async () => {
|
||||
const { getAllByPlaceholderText, getByText } = render(
|
||||
<ScheduleAndDetails courseId={courseId} />,
|
||||
);
|
||||
const { getAllByPlaceholderText, getByText } = renderComponent();
|
||||
let inputs;
|
||||
await waitFor(() => {
|
||||
inputs = getAllByPlaceholderText(DATE_FORMAT.toLocaleUpperCase());
|
||||
@@ -124,7 +130,7 @@ describe('<ScheduleAndDetails />', () => {
|
||||
});
|
||||
|
||||
it('should display a success message when course details saves', async () => {
|
||||
const { getByText } = render(<ScheduleAndDetails courseId={courseId} />);
|
||||
const { getByText } = renderComponent();
|
||||
await executeThunk(updateCourseDetailsQuery(courseId, 'DaTa'), store.dispatch);
|
||||
expect(getByText(messages.alertSuccess.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
@@ -133,7 +139,7 @@ describe('<ScheduleAndDetails />', () => {
|
||||
axiosMock
|
||||
.onGet(getCourseDetailsApiUrl(courseId))
|
||||
.reply(404, 'error');
|
||||
const { getByText } = render(<ScheduleAndDetails courseId={courseId} />);
|
||||
const { getByText } = renderComponent();
|
||||
await waitFor(() => {
|
||||
expect(getByText(messages.alertLoadFail.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
@@ -143,7 +149,7 @@ describe('<ScheduleAndDetails />', () => {
|
||||
axiosMock
|
||||
.onGet(getCourseSettingsApiUrl(courseId))
|
||||
.reply(404, 'error');
|
||||
const { getByText } = render(<ScheduleAndDetails courseId={courseId} />);
|
||||
const { getByText } = renderComponent();
|
||||
await waitFor(() => {
|
||||
expect(getByText(messages.alertLoadFail.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
@@ -153,7 +159,7 @@ describe('<ScheduleAndDetails />', () => {
|
||||
axiosMock
|
||||
.onPut(getCourseDetailsApiUrl(courseId))
|
||||
.reply(404, 'error');
|
||||
const { getByText } = render(<ScheduleAndDetails courseId={courseId} />);
|
||||
const { getByText } = renderComponent();
|
||||
await act(async () => {
|
||||
await executeThunk(updateCourseDetailsQuery(courseId, 'DaTa'), store.dispatch);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
Container, Button, Layout, StatefulButton,
|
||||
@@ -11,14 +9,15 @@ import {
|
||||
} from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import Placeholder from '../editors/Placeholder';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { useModel } from '../generic/model-store';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import { STATEFUL_BUTTON_STATES } from '../constants';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import { useScrollToHashElement } from '../hooks';
|
||||
import Placeholder from '@src/editors/Placeholder';
|
||||
import { RequestStatus } from '@src/data/constants';
|
||||
import AlertMessage from '@src/generic/alert-message';
|
||||
import InternetConnectionAlert from '@src/generic/internet-connection-alert';
|
||||
import { STATEFUL_BUTTON_STATES } from '@src/constants';
|
||||
import getPageHeadTitle from '@src/generic/utils';
|
||||
import { useScrollToHashElement } from '@src/hooks';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
|
||||
import {
|
||||
fetchCourseSettingsQuery,
|
||||
fetchCourseDetailsQuery,
|
||||
@@ -44,7 +43,7 @@ import ScheduleSidebar from './schedule-sidebar';
|
||||
import messages from './messages';
|
||||
import { useLoadValuesPrompt, useSaveValuesPrompt } from './hooks';
|
||||
|
||||
const ScheduleAndDetails = ({ courseId }) => {
|
||||
const ScheduleAndDetails = () => {
|
||||
const intl = useIntl();
|
||||
const courseSettings = useSelector(getCourseSettings);
|
||||
const courseDetails = useSelector(getCourseDetails);
|
||||
@@ -53,8 +52,8 @@ const ScheduleAndDetails = ({ courseId }) => {
|
||||
const isLoading = loadingDetailsStatus === RequestStatus.IN_PROGRESS
|
||||
|| loadingSettingsStatus === RequestStatus.IN_PROGRESS;
|
||||
|
||||
const course = useModel('courseDetails', courseId);
|
||||
document.title = getPageHeadTitle(course?.name, intl.formatMessage(messages.headingTitle));
|
||||
const { courseId, courseDetails: course } = useCourseAuthoringContext();
|
||||
document.title = getPageHeadTitle(course?.name || '', intl.formatMessage(messages.headingTitle));
|
||||
|
||||
const {
|
||||
platformName,
|
||||
@@ -395,8 +394,4 @@ const ScheduleAndDetails = ({ courseId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
ScheduleAndDetails.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ScheduleAndDetails;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import React from 'react';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { initializeMocks, render } from '@src/testUtils';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { courseSettingsMock, courseDetailsMock } from '../__mocks__';
|
||||
import messages from './messages';
|
||||
import IntroducingSection from '.';
|
||||
@@ -29,14 +25,11 @@ jest.mock('../../editors/sharedComponents/TinyMceWidget', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
let store;
|
||||
const onChangeMock = jest.fn();
|
||||
const RootWrapper = (props) => (
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<IntroducingSection {...props} />
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
<CourseAuthoringProvider courseId="1">
|
||||
<IntroducingSection {...props} />
|
||||
</CourseAuthoringProvider>
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -68,16 +61,7 @@ const props = {
|
||||
|
||||
describe('<IntroducingSection />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
initializeMocks();
|
||||
});
|
||||
|
||||
it('renders successfully', () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { render, initializeMocks } from '../testUtils';
|
||||
import VideoSelectorContainer from './VideoSelectorContainer';
|
||||
|
||||
@@ -8,7 +9,11 @@ describe('VideoSelectorContainer', () => {
|
||||
});
|
||||
|
||||
it('renders the wrapper div with correct class', () => {
|
||||
const { container } = render(<VideoSelectorContainer courseId="course-v1:edX+Test+2024" />);
|
||||
const { container } = render(
|
||||
<CourseAuthoringProvider courseId="course-v1:edX+Test+2024">
|
||||
<VideoSelectorContainer />
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
expect(container.querySelector('.selector-page')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import VideoSelectorPage from '../editors/VideoSelectorPage';
|
||||
|
||||
const VideoSelectorContainer = ({
|
||||
courseId,
|
||||
}) => {
|
||||
const VideoSelectorContainer = () => {
|
||||
const { blockId } = useParams();
|
||||
const { courseId } = useCourseAuthoringContext();
|
||||
return (
|
||||
<div className="selector-page">
|
||||
<VideoSelectorPage
|
||||
@@ -20,8 +18,4 @@ const VideoSelectorContainer = ({
|
||||
);
|
||||
};
|
||||
|
||||
VideoSelectorContainer.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default VideoSelectorContainer;
|
||||
@@ -5,7 +5,6 @@ import { configureStore, Reducer } from '@reduxjs/toolkit';
|
||||
import { reducer as liveReducer } from '@openedx-plugins/course-app-live/data/slice';
|
||||
|
||||
import { reducer as modelsReducer } from './generic/model-store';
|
||||
import { reducer as courseDetailReducer } from './data/slice';
|
||||
import { reducer as discussionsReducer } from './pages-and-resources/discussions/data/slice';
|
||||
import { reducer as pagesAndResourcesReducer } from './pages-and-resources/data/slice';
|
||||
import { reducer as customPagesReducer } from './custom-pages/data/slice';
|
||||
@@ -36,7 +35,6 @@ type InferState<ReducerType> = ReducerType extends Reducer<infer T> ? T : never;
|
||||
* TODO: refactor each part to use React Context and React Query instead.
|
||||
*/
|
||||
export interface DeprecatedReduxState {
|
||||
courseDetail: InferState<typeof courseDetailReducer>;
|
||||
customPages: Record<string, any>;
|
||||
discussions: Record<string, any>;
|
||||
assets: Record<string, any>;
|
||||
@@ -66,7 +64,6 @@ export interface DeprecatedReduxState {
|
||||
export default function initializeStore(preloadedState: Partial<DeprecatedReduxState> | undefined = undefined) {
|
||||
return configureStore<DeprecatedReduxState>({
|
||||
reducer: {
|
||||
courseDetail: courseDetailReducer,
|
||||
customPages: customPagesReducer,
|
||||
discussions: discussionsReducer,
|
||||
assets: filesReducer,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-check
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { initializeMocks, render, waitFor } from '../testUtils';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { executeThunk } from '../utils';
|
||||
@@ -15,7 +16,11 @@ let store;
|
||||
const courseId = 'course-v1:org+101+101';
|
||||
const emptyTextbooksMock = { textbooks: [] };
|
||||
|
||||
const renderComponent = () => render(<Textbooks courseId={courseId} />);
|
||||
const renderComponent = () => render(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<Textbooks />
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
|
||||
describe('<Textbooks />', () => {
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Breadcrumb,
|
||||
@@ -11,11 +10,11 @@ import { Add as AddIcon } from '@openedx/paragon/icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
|
||||
import { useWaffleFlags } from '../data/apiHooks';
|
||||
import { SavingErrorAlert } from '../generic/saving-error-alert';
|
||||
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
|
||||
import { useModel } from '../generic/model-store';
|
||||
import { LoadingSpinner } from '../generic/Loading';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
@@ -28,12 +27,11 @@ import { useTextbooks } from './hooks';
|
||||
import { getTextbookFormInitialValues } from './utils';
|
||||
import messages from './messages';
|
||||
|
||||
const Textbooks = ({ courseId }) => {
|
||||
const Textbooks = () => {
|
||||
const intl = useIntl();
|
||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||
const waffleFlags = useWaffleFlags(courseId);
|
||||
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
|
||||
const {
|
||||
textbooks,
|
||||
isLoading,
|
||||
@@ -129,7 +127,6 @@ const Textbooks = ({ courseId }) => {
|
||||
initialFormValues={getTextbookFormInitialValues()}
|
||||
onSubmit={handleTextbookFormSubmit}
|
||||
onSavingStatus={handleSavingStatusDispatch}
|
||||
courseId={courseId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -156,8 +153,4 @@ const Textbooks = ({ courseId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
Textbooks.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Textbooks;
|
||||
|
||||
@@ -66,7 +66,6 @@ const TextbookCard = ({
|
||||
initialFormValues={getTextbookFormInitialValues(true, { tab_title: tabTitle, chapters, id })}
|
||||
onSubmit={onEditSubmit}
|
||||
onSavingStatus={handleSavingStatusDispatch}
|
||||
courseId={courseId}
|
||||
/>
|
||||
) : (
|
||||
(
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import React from 'react';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { render, waitFor, within } from '@testing-library/react';
|
||||
import {
|
||||
render, waitFor, within, initializeMocks,
|
||||
} from '@src/testUtils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { getEditTextbooksApiUrl } from '../data/api';
|
||||
import { deleteTextbookQuery, editTextbookQuery } from '../data/thunk';
|
||||
import { textbooksMock } from '../__mocks__';
|
||||
import initializeStore from '../../store';
|
||||
import { executeThunk } from '../../utils';
|
||||
import TextbookCard from './TextbooksCard';
|
||||
import messages from '../textbook-form/messages';
|
||||
@@ -26,34 +22,25 @@ const onDeleteSubmitMock = jest.fn();
|
||||
const handleSavingStatusDispatchMock = jest.fn();
|
||||
|
||||
const renderComponent = () => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<TextbookCard
|
||||
textbook={textbook}
|
||||
courseId={courseId}
|
||||
onEditSubmit={onEditSubmitMock}
|
||||
onDeleteSubmit={onDeleteSubmitMock}
|
||||
handleSavingStatusDispatch={handleSavingStatusDispatchMock}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</AppProvider>,
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<TextbookCard
|
||||
textbook={textbook}
|
||||
courseId={courseId}
|
||||
onEditSubmit={onEditSubmitMock}
|
||||
onDeleteSubmit={onDeleteSubmitMock}
|
||||
handleSavingStatusDispatch={handleSavingStatusDispatchMock}
|
||||
/>,
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
|
||||
describe('<TextbookCard />', () => {
|
||||
let user;
|
||||
beforeEach(async () => {
|
||||
user = userEvent.setup();
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
const mocks = initializeMocks();
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
});
|
||||
|
||||
it('render TextbookCard component correctly', async () => {
|
||||
|
||||
@@ -17,11 +17,11 @@ import {
|
||||
useToggle,
|
||||
} from '@openedx/paragon';
|
||||
|
||||
import FormikControl from '../../generic/FormikControl';
|
||||
import PromptIfDirty from '../../generic/prompt-if-dirty/PromptIfDirty';
|
||||
import ModalDropzone from '../../generic/modal-dropzone/ModalDropzone';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import { UPLOAD_FILE_MAX_SIZE } from '../../constants';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import FormikControl from '@src/generic/FormikControl';
|
||||
import PromptIfDirty from '@src/generic/prompt-if-dirty/PromptIfDirty';
|
||||
import ModalDropzone from '@src/generic/modal-dropzone/ModalDropzone';
|
||||
import { UPLOAD_FILE_MAX_SIZE } from '@src/constants';
|
||||
import textbookFormValidationSchema from './validations';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -30,11 +30,10 @@ const TextbookForm = ({
|
||||
initialFormValues,
|
||||
onSubmit,
|
||||
onSavingStatus,
|
||||
courseId,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const courseDetail = useModel('courseDetails', courseId);
|
||||
const { courseDetail } = useCourseAuthoringContext();
|
||||
const courseTitle = courseDetail ? courseDetail?.name : '';
|
||||
|
||||
const [currentTextbookIndex, setCurrentTextbookIndex] = useState(0);
|
||||
@@ -195,7 +194,6 @@ TextbookForm.propTypes = {
|
||||
initialFormValues: PropTypes.shape({}).isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onSavingStatus: PropTypes.func.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default TextbookForm;
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import {
|
||||
initializeMocks,
|
||||
render, waitFor, within,
|
||||
} from '@testing-library/react';
|
||||
} from '@src/testUtils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import { executeThunk } from '../../utils';
|
||||
import { executeThunk } from '@src/utils';
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { getTextbookFormInitialValues } from '../utils';
|
||||
import { getUpdateTextbooksApiUrl } from '../data/api';
|
||||
import { createTextbookQuery } from '../data/thunk';
|
||||
@@ -26,32 +22,22 @@ const onSubmitMock = jest.fn();
|
||||
const onSavingStatus = jest.fn();
|
||||
|
||||
const renderComponent = () => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<TextbookForm
|
||||
closeTextbookForm={closeTextbookFormMock}
|
||||
initialFormValues={initialFormValuesMock}
|
||||
onSubmit={onSubmitMock}
|
||||
onSavingStatus={onSavingStatus}
|
||||
courseId={courseId}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</AppProvider>,
|
||||
<CourseAuthoringProvider>
|
||||
<TextbookForm
|
||||
closeTextbookForm={closeTextbookFormMock}
|
||||
initialFormValues={initialFormValuesMock}
|
||||
onSubmit={onSubmitMock}
|
||||
onSavingStatus={onSavingStatus}
|
||||
/>
|
||||
</CourseAuthoringProvider>,
|
||||
);
|
||||
|
||||
describe('<TextbookForm />', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
const mocks = initializeMocks();
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
});
|
||||
|
||||
it('renders TextbooksForm component correctly', async () => {
|
||||
|
||||
Reference in New Issue
Block a user