fix: disable special exams config if feature flag is disabled (#2325)
* fix: disable special exams config if feature flag is disabled * test: add testcases * fix: convert AdvancedTab to typescript
This commit is contained in:
@@ -36,7 +36,11 @@ 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 { getCurrentItem, getProctoredExamsFlag } from './data/selectors';
|
||||
import {
|
||||
getCurrentItem,
|
||||
getProctoredExamsFlag,
|
||||
getTimedExamsFlag,
|
||||
} from './data/selectors';
|
||||
import { COURSE_BLOCK_NAMES } from './constants';
|
||||
import StatusBar from './status-bar/StatusBar';
|
||||
import EnableHighlightsModal from './enable-highlights-modal/EnableHighlightsModal';
|
||||
@@ -167,6 +171,7 @@ const CourseOutline = ({ courseId }: CourseOutlineProps) => {
|
||||
const deleteCategory = COURSE_BLOCK_NAMES[currentItemData.category]?.name.toLowerCase();
|
||||
|
||||
const enableProctoredExams = useSelector(getProctoredExamsFlag);
|
||||
const enableTimedExams = useSelector(getTimedExamsFlag);
|
||||
|
||||
/**
|
||||
* Move section to new index
|
||||
@@ -505,6 +510,7 @@ const CourseOutline = ({ courseId }: CourseOutlineProps) => {
|
||||
onConfigureSubmit={handleConfigureItemSubmit}
|
||||
currentItemData={currentItemData}
|
||||
enableProctoredExams={enableProctoredExams}
|
||||
enableTimedExams={enableTimedExams}
|
||||
isSelfPaced={statusBarData.isSelfPaced}
|
||||
/>
|
||||
<DeleteModal
|
||||
|
||||
41
src/course-outline/data/selectors.test.js
Normal file
41
src/course-outline/data/selectors.test.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { getTimedExamsFlag, getProctoredExamsFlag } from './selectors';
|
||||
|
||||
const mockState = {
|
||||
courseOutline: {
|
||||
enableTimedExams: true,
|
||||
enableProctoredExams: false,
|
||||
},
|
||||
};
|
||||
|
||||
describe('course-outline selectors', () => {
|
||||
describe('getTimedExamsFlag', () => {
|
||||
it('returns enableTimedExams value from state', () => {
|
||||
expect(getTimedExamsFlag(mockState)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when enableTimedExams is false', () => {
|
||||
const stateWithDisabledExams = {
|
||||
courseOutline: {
|
||||
...mockState.courseOutline,
|
||||
enableTimedExams: false,
|
||||
},
|
||||
};
|
||||
expect(getTimedExamsFlag(stateWithDisabledExams)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns undefined when enableTimedExams is not set', () => {
|
||||
const stateWithoutProperty = {
|
||||
courseOutline: {
|
||||
enableProctoredExams: false,
|
||||
},
|
||||
};
|
||||
expect(getTimedExamsFlag(stateWithoutProperty)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProctoredExamsFlag', () => {
|
||||
it('returns enableProctoredExams value from state', () => {
|
||||
expect(getProctoredExamsFlag(mockState)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ export const getCurrentSubsection = (state) => state.courseOutline.currentSubsec
|
||||
export const getCourseActions = (state) => state.courseOutline.actions;
|
||||
export const getCustomRelativeDatesActiveFlag = (state) => state.courseOutline.isCustomRelativeDatesActive;
|
||||
export const getProctoredExamsFlag = (state) => state.courseOutline.enableProctoredExams;
|
||||
export const getTimedExamsFlag = (state) => state.courseOutline.enableTimedExams;
|
||||
export const getPasteFileNotices = (state) => state.courseOutline.pasteFileNotices;
|
||||
export const getErrors = (state) => state.courseOutline.errors;
|
||||
export const getCreatedOn = (state) => state.courseOutline.createdOn;
|
||||
|
||||
79
src/course-outline/data/slice.test.js
Normal file
79
src/course-outline/data/slice.test.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { reducer, fetchOutlineIndexSuccess } from './slice';
|
||||
|
||||
describe('course-outline slice', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = configureStore({
|
||||
reducer: {
|
||||
courseOutline: reducer,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchOutlineIndexSuccess action', () => {
|
||||
it('sets enableTimedExams from payload', () => {
|
||||
const mockPayload = {
|
||||
courseStructure: {
|
||||
enableProctoredExams: true,
|
||||
enableTimedExams: false,
|
||||
childInfo: {
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
isCustomRelativeDatesActive: false,
|
||||
createdOn: null,
|
||||
};
|
||||
|
||||
store.dispatch(fetchOutlineIndexSuccess(mockPayload));
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.courseOutline.enableTimedExams).toBe(false);
|
||||
expect(state.courseOutline.enableProctoredExams).toBe(true);
|
||||
});
|
||||
|
||||
it('sets enableTimedExams to true when provided', () => {
|
||||
const mockPayload = {
|
||||
courseStructure: {
|
||||
enableProctoredExams: false,
|
||||
enableTimedExams: true,
|
||||
childInfo: {
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
isCustomRelativeDatesActive: false,
|
||||
createdOn: null,
|
||||
};
|
||||
|
||||
store.dispatch(fetchOutlineIndexSuccess(mockPayload));
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.courseOutline.enableTimedExams).toBe(true);
|
||||
});
|
||||
|
||||
it('handles missing enableTimedExams field gracefully', () => {
|
||||
const mockPayload = {
|
||||
courseStructure: {
|
||||
enableProctoredExams: true,
|
||||
childInfo: {
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
isCustomRelativeDatesActive: false,
|
||||
createdOn: null,
|
||||
};
|
||||
|
||||
store.dispatch(fetchOutlineIndexSuccess(mockPayload));
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.courseOutline.enableTimedExams).toBeUndefined();
|
||||
expect(state.courseOutline.enableProctoredExams).toBe(true);
|
||||
});
|
||||
|
||||
it('initializes with enableTimedExams false by default', () => {
|
||||
const state = store.getState();
|
||||
expect(state.courseOutline.enableTimedExams).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -47,6 +47,7 @@ const initialState = {
|
||||
allowMoveDown: false,
|
||||
},
|
||||
enableProctoredExams: false,
|
||||
enableTimedExams: false,
|
||||
pasteFileNotices: {},
|
||||
createdOn: null,
|
||||
} satisfies CourseOutlineState as unknown as CourseOutlineState;
|
||||
@@ -60,6 +61,7 @@ const slice = createSlice({
|
||||
state.sectionsList = payload.courseStructure?.childInfo?.children || [];
|
||||
state.isCustomRelativeDatesActive = payload.isCustomRelativeDatesActive;
|
||||
state.enableProctoredExams = payload.courseStructure?.enableProctoredExams;
|
||||
state.enableTimedExams = payload.courseStructure?.enableTimedExams;
|
||||
state.createdOn = payload.createdOn;
|
||||
},
|
||||
updateOutlineIndexLoadingStatus: (state: CourseOutlineState, { payload }) => {
|
||||
|
||||
@@ -59,6 +59,7 @@ export interface CourseOutlineState {
|
||||
currentItem: XBlock | {};
|
||||
actions: XBlockActions;
|
||||
enableProctoredExams: boolean;
|
||||
enableTimedExams: boolean;
|
||||
pasteFileNotices: object;
|
||||
createdOn: null | Date;
|
||||
}
|
||||
|
||||
615
src/generic/configure-modal/AdvancedTab.test.jsx
Normal file
615
src/generic/configure-modal/AdvancedTab.test.jsx
Normal file
@@ -0,0 +1,615 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { Formik } from 'formik';
|
||||
|
||||
import AdvancedTab from './AdvancedTab';
|
||||
import messages from './messages';
|
||||
|
||||
const defaultProps = {
|
||||
values: {
|
||||
isTimeLimited: false,
|
||||
defaultTimeLimitMinutes: 30,
|
||||
isPrereq: false,
|
||||
prereqUsageKey: '',
|
||||
prereqMinScore: 100,
|
||||
prereqMinCompletion: 100,
|
||||
isProctoredExam: false,
|
||||
isPracticeExam: false,
|
||||
isOnboardingExam: false,
|
||||
examReviewRules: '',
|
||||
},
|
||||
setFieldValue: jest.fn(),
|
||||
prereqs: [],
|
||||
releasedToStudents: false,
|
||||
wasExamEverLinkedWithExternal: false,
|
||||
enableProctoredExams: false,
|
||||
enableTimedExams: true,
|
||||
supportsOnboarding: false,
|
||||
wasProctoredExam: false,
|
||||
showReviewRules: false,
|
||||
onlineProctoringRules: '',
|
||||
};
|
||||
|
||||
const renderComponent = (props = {}) => render(
|
||||
<IntlProvider locale="en">
|
||||
<Formik initialValues={defaultProps.values} onSubmit={() => {}}>
|
||||
<AdvancedTab {...defaultProps} {...props} />
|
||||
</Formik>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
describe('<AdvancedTab /> with enableTimedExams prop', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when enableTimedExams is true', () => {
|
||||
it('renders special exam options enabled', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByText('Set as a special exam')).toBeInTheDocument();
|
||||
const headingContainer = screen.getByText(
|
||||
'Set as a special exam',
|
||||
).parentElement;
|
||||
expect(headingContainer.querySelector('svg')).not.toBeInTheDocument();
|
||||
|
||||
const noneRadio = screen.getByLabelText('None');
|
||||
const timedRadio = screen.getByLabelText('Timed');
|
||||
|
||||
expect(noneRadio).toBeInTheDocument();
|
||||
expect(noneRadio).not.toBeDisabled();
|
||||
expect(timedRadio).toBeInTheDocument();
|
||||
expect(timedRadio).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('allows user to interact with timed exam option', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent();
|
||||
|
||||
const timedRadio = screen.getByLabelText('Timed');
|
||||
|
||||
expect(timedRadio).not.toBeDisabled();
|
||||
await user.click(timedRadio);
|
||||
|
||||
expect(timedRadio).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when enableTimedExams is false', () => {
|
||||
const disabledProps = { enableTimedExams: false };
|
||||
|
||||
it('renders special exam options disabled', () => {
|
||||
renderComponent(disabledProps);
|
||||
|
||||
const noneRadio = screen.getByLabelText('None');
|
||||
const timedRadio = screen.getByLabelText('Timed');
|
||||
|
||||
expect(noneRadio).toBeDisabled();
|
||||
expect(timedRadio).toBeDisabled();
|
||||
});
|
||||
|
||||
it('shows tooltip icon next to heading', () => {
|
||||
renderComponent(disabledProps);
|
||||
|
||||
const questionIcon = screen
|
||||
.getByText('Set as a special exam')
|
||||
.parentElement.querySelector('svg');
|
||||
expect(questionIcon).toBeInTheDocument();
|
||||
expect(questionIcon).toHaveClass('ml-2', 'text-gray-500');
|
||||
});
|
||||
|
||||
it('shows tooltip message when hovering over icon', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent(disabledProps);
|
||||
|
||||
const questionIcon = screen
|
||||
.getByText('Set as a special exam')
|
||||
.parentElement.querySelector('svg');
|
||||
await user.hover(questionIcon);
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
messages.timedExamsDisabledTooltip.defaultMessage,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('prevents user from interacting with disabled options', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent(disabledProps);
|
||||
|
||||
const noneRadio = screen.getByLabelText('None');
|
||||
const timedRadio = screen.getByLabelText('Timed');
|
||||
|
||||
expect(noneRadio).toBeDisabled();
|
||||
expect(timedRadio).toBeDisabled();
|
||||
|
||||
await user.click(noneRadio);
|
||||
await user.click(timedRadio);
|
||||
|
||||
expect(noneRadio).toBeDisabled();
|
||||
expect(timedRadio).toBeDisabled();
|
||||
});
|
||||
|
||||
it('does not show tooltip icon when enableTimedExams is true', () => {
|
||||
renderComponent({ enableTimedExams: true });
|
||||
|
||||
const headingContainer = screen.getByText(
|
||||
'Set as a special exam',
|
||||
).parentElement;
|
||||
expect(headingContainer.querySelector('svg')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows tooltip icon with proper classes', () => {
|
||||
renderComponent(disabledProps);
|
||||
|
||||
const questionIcon = screen
|
||||
.getByText('Set as a special exam')
|
||||
.parentElement.querySelector('svg');
|
||||
expect(questionIcon).toHaveClass('ml-2', 'text-gray-500');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with proctored exams enabled', () => {
|
||||
it('shows proctored exam options when both flags are enabled', () => {
|
||||
renderComponent({
|
||||
enableProctoredExams: true,
|
||||
enableTimedExams: true,
|
||||
});
|
||||
|
||||
expect(screen.getByLabelText('None')).not.toBeDisabled();
|
||||
expect(screen.getByLabelText('Timed')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('shows proctored options but disables timed options when enableTimedExams is false', () => {
|
||||
renderComponent({
|
||||
enableProctoredExams: true,
|
||||
enableTimedExams: false,
|
||||
});
|
||||
|
||||
expect(screen.getByLabelText('None')).toBeDisabled();
|
||||
expect(screen.getByLabelText('Timed')).toBeDisabled();
|
||||
|
||||
const questionIcon = screen
|
||||
.getByText('Set as a special exam')
|
||||
.parentElement.querySelector('svg');
|
||||
expect(questionIcon).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('default props and prop types', () => {
|
||||
it('uses default value for enableTimedExams when not provided', () => {
|
||||
const propsWithoutTimedExams = { ...defaultProps };
|
||||
delete propsWithoutTimedExams.enableTimedExams;
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<Formik initialValues={defaultProps.values} onSubmit={() => {}}>
|
||||
<AdvancedTab {...propsWithoutTimedExams} />
|
||||
</Formik>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText('None')).not.toBeDisabled();
|
||||
expect(screen.getByLabelText('Timed')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('handles enableTimedExams prop correctly', () => {
|
||||
renderComponent({ enableTimedExams: false });
|
||||
|
||||
expect(screen.getByLabelText('None')).toBeDisabled();
|
||||
expect(screen.getByLabelText('Timed')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('exam type selection behavior', () => {
|
||||
const mockSetFieldValue = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockSetFieldValue.mockClear();
|
||||
});
|
||||
|
||||
it('handles timed exam selection', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent({ setFieldValue: mockSetFieldValue });
|
||||
|
||||
const timedRadio = screen.getByLabelText('Timed');
|
||||
await user.click(timedRadio);
|
||||
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isTimeLimited', true);
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isOnboardingExam', false);
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isPracticeExam', false);
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isProctoredExam', false);
|
||||
});
|
||||
|
||||
it('handles none exam selection', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent({
|
||||
setFieldValue: mockSetFieldValue,
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isTimeLimited: true,
|
||||
},
|
||||
});
|
||||
|
||||
const noneRadio = screen.getByLabelText('None');
|
||||
await user.click(noneRadio);
|
||||
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isTimeLimited', false);
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isOnboardingExam', false);
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isPracticeExam', false);
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isProctoredExam', false);
|
||||
});
|
||||
|
||||
it('handles proctored exam selection', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent({
|
||||
setFieldValue: mockSetFieldValue,
|
||||
enableProctoredExams: true,
|
||||
});
|
||||
|
||||
const proctoredRadio = screen.queryByLabelText('Proctored');
|
||||
|
||||
if (proctoredRadio) {
|
||||
await user.click(proctoredRadio);
|
||||
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isProctoredExam', true);
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isTimeLimited', true);
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith(
|
||||
'isOnboardingExam',
|
||||
false,
|
||||
);
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isPracticeExam', false);
|
||||
}
|
||||
});
|
||||
|
||||
it('handles practice exam selection', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent({
|
||||
setFieldValue: mockSetFieldValue,
|
||||
enableProctoredExams: true,
|
||||
supportsOnboarding: false, // Explicitly set to false to show practice exam
|
||||
});
|
||||
|
||||
const practiceRadio = screen.getByLabelText('Practice proctored');
|
||||
expect(practiceRadio).toBeInTheDocument();
|
||||
|
||||
await user.click(practiceRadio);
|
||||
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isPracticeExam', true);
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isProctoredExam', true);
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isTimeLimited', true);
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isOnboardingExam', false);
|
||||
});
|
||||
|
||||
it('handles onboarding exam selection', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent({
|
||||
setFieldValue: mockSetFieldValue,
|
||||
enableProctoredExams: true,
|
||||
supportsOnboarding: true,
|
||||
});
|
||||
|
||||
const onboardingRadio = screen.queryByLabelText('Onboarding');
|
||||
|
||||
if (onboardingRadio) {
|
||||
await user.click(onboardingRadio);
|
||||
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith(
|
||||
'isOnboardingExam',
|
||||
true,
|
||||
);
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isProctoredExam', true);
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isTimeLimited', true);
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith('isPracticeExam', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('exam type value calculation', () => {
|
||||
it('shows onboarding exam when both isTimeLimited and isProctoredExam and isOnboardingExam are true', () => {
|
||||
renderComponent({
|
||||
enableProctoredExams: true,
|
||||
supportsOnboarding: true,
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isTimeLimited: true,
|
||||
isProctoredExam: true,
|
||||
isOnboardingExam: true,
|
||||
},
|
||||
});
|
||||
|
||||
const onboardingRadio = screen.queryByLabelText('Onboarding');
|
||||
if (onboardingRadio) {
|
||||
expect(onboardingRadio).toBeChecked();
|
||||
}
|
||||
});
|
||||
|
||||
it('shows practice exam when both isTimeLimited and isProctoredExam and isPracticeExam are true', () => {
|
||||
renderComponent({
|
||||
enableProctoredExams: true,
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isTimeLimited: true,
|
||||
isProctoredExam: true,
|
||||
isPracticeExam: true,
|
||||
},
|
||||
});
|
||||
|
||||
const practiceRadio = screen.queryByLabelText('Practice proctored');
|
||||
if (practiceRadio) {
|
||||
expect(practiceRadio).toBeChecked();
|
||||
}
|
||||
});
|
||||
|
||||
it('shows proctored exam when both isTimeLimited and isProctoredExam are true', () => {
|
||||
renderComponent({
|
||||
enableProctoredExams: true,
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isTimeLimited: true,
|
||||
isProctoredExam: true,
|
||||
},
|
||||
});
|
||||
|
||||
const proctoredRadio = screen.queryByLabelText('Proctored');
|
||||
if (proctoredRadio) {
|
||||
expect(proctoredRadio).toBeChecked();
|
||||
}
|
||||
});
|
||||
|
||||
it('shows timed exam when only isTimeLimited is true', () => {
|
||||
renderComponent({
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isTimeLimited: true,
|
||||
},
|
||||
});
|
||||
|
||||
const timedRadio = screen.getByLabelText('Timed');
|
||||
expect(timedRadio).toBeChecked();
|
||||
});
|
||||
|
||||
it('shows none when no exam flags are set', () => {
|
||||
renderComponent();
|
||||
|
||||
const noneRadio = screen.getByLabelText('None');
|
||||
expect(noneRadio).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
describe('time limit functionality', () => {
|
||||
const mockSetFieldValue = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockSetFieldValue.mockClear();
|
||||
});
|
||||
|
||||
it('displays time limit input when timed exam is selected', () => {
|
||||
renderComponent({
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isTimeLimited: true,
|
||||
},
|
||||
});
|
||||
|
||||
const timeInput = screen.queryByDisplayValue('00:30');
|
||||
expect(timeInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles time limit changes', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent({
|
||||
setFieldValue: mockSetFieldValue,
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isTimeLimited: true,
|
||||
},
|
||||
});
|
||||
|
||||
const timeInput = screen.queryByDisplayValue('00:30');
|
||||
if (timeInput) {
|
||||
await user.clear(timeInput);
|
||||
await user.type(timeInput, '01:30');
|
||||
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith(
|
||||
'defaultTimeLimitMinutes',
|
||||
90,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('formats time correctly for different minute values', () => {
|
||||
renderComponent({
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isTimeLimited: true,
|
||||
defaultTimeLimitMinutes: 0,
|
||||
},
|
||||
});
|
||||
expect(screen.queryByDisplayValue('00:00')).toBeInTheDocument();
|
||||
|
||||
renderComponent({
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isTimeLimited: true,
|
||||
defaultTimeLimitMinutes: 90,
|
||||
},
|
||||
});
|
||||
expect(screen.queryByDisplayValue('01:30')).toBeInTheDocument();
|
||||
|
||||
renderComponent({
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isTimeLimited: true,
|
||||
defaultTimeLimitMinutes: 605,
|
||||
},
|
||||
});
|
||||
expect(screen.queryByDisplayValue('10:05')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles NaN values in time formatting', () => {
|
||||
renderComponent({
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isTimeLimited: true,
|
||||
defaultTimeLimitMinutes: NaN,
|
||||
},
|
||||
});
|
||||
expect(screen.queryByDisplayValue('00:00')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles undefined values in time formatting', () => {
|
||||
renderComponent({
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isTimeLimited: true,
|
||||
defaultTimeLimitMinutes: undefined,
|
||||
},
|
||||
});
|
||||
expect(screen.queryByDisplayValue('00:00')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('alerts and warnings', () => {
|
||||
it('shows warning when exam is locked and was not proctored', () => {
|
||||
renderComponent({
|
||||
releasedToStudents: true,
|
||||
wasExamEverLinkedWithExternal: true,
|
||||
wasProctoredExam: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
messages.proctoredExamLockedAndisNotProctoredExamAlert.defaultMessage,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows warning when exam is locked and was proctored', () => {
|
||||
renderComponent({
|
||||
releasedToStudents: true,
|
||||
wasExamEverLinkedWithExternal: true,
|
||||
wasProctoredExam: true,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
messages.proctoredExamLockedAndisProctoredExamAlert.defaultMessage,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show warnings when exam is not locked', () => {
|
||||
renderComponent({
|
||||
releasedToStudents: false,
|
||||
wasExamEverLinkedWithExternal: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByText(
|
||||
messages.proctoredExamLockedAndisNotProctoredExamAlert.defaultMessage,
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(
|
||||
messages.proctoredExamLockedAndisProctoredExamAlert.defaultMessage,
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('review rules section', () => {
|
||||
it('shows review rules when showReviewRules is true and conditions are met', () => {
|
||||
renderComponent({
|
||||
showReviewRules: true,
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isProctoredExam: true,
|
||||
isPracticeExam: false,
|
||||
isOnboardingExam: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(messages.reviewRulesLabel.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show review rules when showReviewRules is false', () => {
|
||||
renderComponent({
|
||||
showReviewRules: false,
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isProctoredExam: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByText(messages.reviewRulesLabel.defaultMessage),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows review rules with online proctoring link when available', () => {
|
||||
renderComponent({
|
||||
showReviewRules: true,
|
||||
onlineProctoringRules: 'https://example.com/rules',
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isProctoredExam: true,
|
||||
isPracticeExam: false,
|
||||
isOnboardingExam: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(messages.reviewRulesLabel.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
const link = screen.queryByText('example.com/rules');
|
||||
if (link) {
|
||||
expect(link).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it('handles review rules text area changes', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockSetFieldValue = jest.fn();
|
||||
|
||||
renderComponent({
|
||||
setFieldValue: mockSetFieldValue,
|
||||
showReviewRules: true,
|
||||
values: {
|
||||
...defaultProps.values,
|
||||
isProctoredExam: true,
|
||||
isPracticeExam: false,
|
||||
isOnboardingExam: false,
|
||||
},
|
||||
});
|
||||
|
||||
const textArea = screen.getByRole('textbox');
|
||||
await user.type(textArea, 'New review rules');
|
||||
|
||||
expect(mockSetFieldValue).toHaveBeenCalledWith(
|
||||
'examReviewRules',
|
||||
expect.any(String),
|
||||
);
|
||||
const examRulesCalls = mockSetFieldValue.mock.calls.filter(
|
||||
(call) => call[0] === 'examReviewRules',
|
||||
);
|
||||
expect(examRulesCalls.length).toBeGreaterThan(0);
|
||||
const lastExamRulesCall = examRulesCalls[examRulesCalls.length - 1];
|
||||
expect(lastExamRulesCall[1]).toContain('s');
|
||||
});
|
||||
});
|
||||
|
||||
describe('prerequisites section', () => {
|
||||
it('renders prerequisite settings', () => {
|
||||
renderComponent({
|
||||
prereqs: [{ id: 'prereq1', name: 'Prerequisite 1' }],
|
||||
});
|
||||
|
||||
expect(screen.getByText('Set as a special exam')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,26 +1,62 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import { Alert, Form, Hyperlink } from '@openedx/paragon';
|
||||
import {
|
||||
Warning as WarningIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
Alert,
|
||||
Form,
|
||||
Hyperlink,
|
||||
OverlayTrigger,
|
||||
Tooltip,
|
||||
} from '@openedx/paragon';
|
||||
import { Warning as WarningIcon, Question } from '@openedx/paragon/icons';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
import PrereqSettings from './PrereqSettings';
|
||||
|
||||
const AdvancedTab = ({
|
||||
interface ValuesProps {
|
||||
isTimeLimited: boolean;
|
||||
defaultTimeLimitMinutes?: number;
|
||||
isPrereq?: boolean;
|
||||
prereqUsageKey?: string;
|
||||
prereqMinScore?: number;
|
||||
prereqMinCompletion?: number;
|
||||
isProctoredExam?: boolean;
|
||||
isPracticeExam?: boolean;
|
||||
isOnboardingExam?: boolean;
|
||||
examReviewRules?: string;
|
||||
}
|
||||
|
||||
interface PrereqItem {
|
||||
blockUsageKey: string;
|
||||
blockDisplayName: string;
|
||||
}
|
||||
|
||||
interface AdvancedTabProps {
|
||||
values: ValuesProps;
|
||||
setFieldValue: (field: string, value: any) => void;
|
||||
releasedToStudents: boolean;
|
||||
prereqs?: PrereqItem[];
|
||||
wasExamEverLinkedWithExternal?: boolean;
|
||||
enableProctoredExams?: boolean;
|
||||
enableTimedExams?: boolean;
|
||||
supportsOnboarding?: boolean;
|
||||
wasProctoredExam?: boolean;
|
||||
showReviewRules?: boolean;
|
||||
onlineProctoringRules?: string;
|
||||
}
|
||||
|
||||
const AdvancedTab: React.FC<AdvancedTabProps> = ({
|
||||
values,
|
||||
setFieldValue,
|
||||
prereqs,
|
||||
releasedToStudents,
|
||||
wasExamEverLinkedWithExternal,
|
||||
enableProctoredExams,
|
||||
supportsOnboarding,
|
||||
wasProctoredExam,
|
||||
showReviewRules,
|
||||
onlineProctoringRules,
|
||||
prereqs = [],
|
||||
wasExamEverLinkedWithExternal = false,
|
||||
enableProctoredExams = false,
|
||||
enableTimedExams = true,
|
||||
supportsOnboarding = false,
|
||||
wasProctoredExam = false,
|
||||
showReviewRules = false,
|
||||
onlineProctoringRules = '',
|
||||
}) => {
|
||||
const {
|
||||
isTimeLimited,
|
||||
@@ -46,7 +82,11 @@ const AdvancedTab = ({
|
||||
examTypeValue = 'timed';
|
||||
}
|
||||
|
||||
const formatHour = (hour) => {
|
||||
const formatHour = (hour: number | undefined): string => {
|
||||
if (hour === undefined) {
|
||||
return '00:00';
|
||||
}
|
||||
|
||||
const hh = Math.floor(hour / 60);
|
||||
const mm = hour % 60;
|
||||
let hhs = `${hh}`;
|
||||
@@ -66,10 +106,12 @@ const AdvancedTab = ({
|
||||
return `${hhs}:${mms}`;
|
||||
};
|
||||
|
||||
const [timeLimit, setTimeLimit] = useState(formatHour(defaultTimeLimitMinutes));
|
||||
const [timeLimit, setTimeLimit] = useState(
|
||||
formatHour(defaultTimeLimitMinutes),
|
||||
);
|
||||
const showReviewRulesDiv = showReviewRules && isProctoredExam && !isPracticeExam && !isOnboardingExam;
|
||||
|
||||
const handleChange = (e) => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.value === 'timed') {
|
||||
setFieldValue('isTimeLimited', true);
|
||||
setFieldValue('isOnboardingExam', false);
|
||||
@@ -98,8 +140,10 @@ const AdvancedTab = ({
|
||||
}
|
||||
};
|
||||
|
||||
const setCurrentTimeLimit = (event) => {
|
||||
const { validity: { valid } } = event.target;
|
||||
const setCurrentTimeLimit = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const {
|
||||
validity: { valid },
|
||||
} = event.target;
|
||||
let { value } = event.target;
|
||||
value = value.trim();
|
||||
if (value && valid) {
|
||||
@@ -115,12 +159,16 @@ const AdvancedTab = ({
|
||||
<>
|
||||
{proctoredExamLockedIn && !wasProctoredExam && (
|
||||
<Alert variant="warning" icon={WarningIcon}>
|
||||
<FormattedMessage {...messages.proctoredExamLockedAndisNotProctoredExamAlert} />
|
||||
<FormattedMessage
|
||||
{...messages.proctoredExamLockedAndisNotProctoredExamAlert}
|
||||
/>
|
||||
</Alert>
|
||||
)}
|
||||
{proctoredExamLockedIn && wasProctoredExam && (
|
||||
<Alert variant="warning" icon={WarningIcon}>
|
||||
<FormattedMessage {...messages.proctoredExamLockedAndisProctoredExamAlert} />
|
||||
<FormattedMessage
|
||||
{...messages.proctoredExamLockedAndisProctoredExamAlert}
|
||||
/>
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
@@ -129,7 +177,26 @@ const AdvancedTab = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.setSpecialExam} /></h5>
|
||||
<div className="d-flex align-items-center mt-4">
|
||||
<h5 className="text-gray-700 mb-0">
|
||||
<FormattedMessage {...messages.setSpecialExam} />
|
||||
</h5>
|
||||
{!enableTimedExams && (
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
overlay={(
|
||||
<Tooltip id={messages.timedExamsDisabledTooltip.id}>
|
||||
<FormattedMessage {...messages.timedExamsDisabledTooltip} />
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Question
|
||||
className="ml-2 text-gray-500"
|
||||
style={{ cursor: 'help' }}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</div>
|
||||
<hr />
|
||||
<Form.RadioSet
|
||||
name="specialExam"
|
||||
@@ -137,11 +204,12 @@ const AdvancedTab = ({
|
||||
value={examTypeValue}
|
||||
>
|
||||
{renderAlerts()}
|
||||
<Form.Radio value="none">
|
||||
<Form.Radio value="none" disabled={!enableTimedExams}>
|
||||
<FormattedMessage {...messages.none} />
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
value="timed"
|
||||
disabled={!enableTimedExams}
|
||||
description={<FormattedMessage {...messages.timedDescription} />}
|
||||
controlClassName="mw-1-25rem"
|
||||
>
|
||||
@@ -151,14 +219,18 @@ const AdvancedTab = ({
|
||||
<>
|
||||
<Form.Radio
|
||||
value="proctoredExam"
|
||||
description={<FormattedMessage {...messages.proctoredExamDescription} />}
|
||||
description={
|
||||
<FormattedMessage {...messages.proctoredExamDescription} />
|
||||
}
|
||||
controlClassName="mw-1-25rem"
|
||||
>
|
||||
<FormattedMessage {...messages.proctoredExam} />
|
||||
</Form.Radio>
|
||||
{supportsOnboarding ? (
|
||||
<Form.Radio
|
||||
description={<FormattedMessage {...messages.onboardingExamDescription} />}
|
||||
description={
|
||||
<FormattedMessage {...messages.onboardingExamDescription} />
|
||||
}
|
||||
value="onboardingExam"
|
||||
controlClassName="mw-1-25rem"
|
||||
>
|
||||
@@ -168,7 +240,9 @@ const AdvancedTab = ({
|
||||
<Form.Radio
|
||||
value="practiceExam"
|
||||
controlClassName="mw-1-25rem"
|
||||
description={<FormattedMessage {...messages.practiceExamDescription} />}
|
||||
description={
|
||||
<FormattedMessage {...messages.practiceExamDescription} />
|
||||
}
|
||||
>
|
||||
<FormattedMessage {...messages.practiceExam} />
|
||||
</Form.Radio>
|
||||
@@ -176,7 +250,7 @@ const AdvancedTab = ({
|
||||
</>
|
||||
)}
|
||||
</Form.RadioSet>
|
||||
{ isTimeLimited && (
|
||||
{isTimeLimited && (
|
||||
<div className="mt-3" data-testid="advanced-tab-hours-picker-wrapper">
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
@@ -189,10 +263,12 @@ const AdvancedTab = ({
|
||||
pattern="^[0-9][0-9]:[0-5][0-9]$"
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Text><FormattedMessage {...messages.timeLimitDescription} /></Form.Text>
|
||||
<Form.Text>
|
||||
<FormattedMessage {...messages.timeLimitDescription} />
|
||||
</Form.Text>
|
||||
</div>
|
||||
)}
|
||||
{ showReviewRulesDiv && (
|
||||
{showReviewRulesDiv && (
|
||||
<div className="mt-3">
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
@@ -206,7 +282,7 @@ const AdvancedTab = ({
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Text>
|
||||
{ onlineProctoringRules ? (
|
||||
{onlineProctoringRules ? (
|
||||
<FormattedMessage
|
||||
{...messages.reviewRulesDescriptionWithLink}
|
||||
values={{
|
||||
@@ -238,41 +314,4 @@ const AdvancedTab = ({
|
||||
);
|
||||
};
|
||||
|
||||
AdvancedTab.defaultProps = {
|
||||
prereqs: [],
|
||||
wasExamEverLinkedWithExternal: false,
|
||||
enableProctoredExams: false,
|
||||
supportsOnboarding: false,
|
||||
wasProctoredExam: false,
|
||||
showReviewRules: false,
|
||||
onlineProctoringRules: '',
|
||||
};
|
||||
|
||||
AdvancedTab.propTypes = {
|
||||
values: PropTypes.shape({
|
||||
isTimeLimited: PropTypes.bool.isRequired,
|
||||
defaultTimeLimitMinutes: PropTypes.number,
|
||||
isPrereq: PropTypes.bool,
|
||||
prereqUsageKey: PropTypes.string,
|
||||
prereqMinScore: PropTypes.number,
|
||||
prereqMinCompletion: PropTypes.number,
|
||||
isProctoredExam: PropTypes.bool,
|
||||
isPracticeExam: PropTypes.bool,
|
||||
isOnboardingExam: PropTypes.bool,
|
||||
examReviewRules: PropTypes.string,
|
||||
}).isRequired,
|
||||
setFieldValue: PropTypes.func.isRequired,
|
||||
prereqs: PropTypes.arrayOf(PropTypes.shape({
|
||||
blockUsageKey: PropTypes.string.isRequired,
|
||||
blockDisplayName: PropTypes.string.isRequired,
|
||||
})),
|
||||
releasedToStudents: PropTypes.bool.isRequired,
|
||||
wasExamEverLinkedWithExternal: PropTypes.bool,
|
||||
enableProctoredExams: PropTypes.bool,
|
||||
supportsOnboarding: PropTypes.bool,
|
||||
wasProctoredExam: PropTypes.bool,
|
||||
showReviewRules: PropTypes.bool,
|
||||
onlineProctoringRules: PropTypes.string,
|
||||
};
|
||||
|
||||
export default AdvancedTab;
|
||||
@@ -27,6 +27,7 @@ const ConfigureModal = ({
|
||||
onConfigureSubmit,
|
||||
currentItemData,
|
||||
enableProctoredExams = false,
|
||||
enableTimedExams = false,
|
||||
isXBlockComponent = false,
|
||||
isSelfPaced,
|
||||
}) => {
|
||||
@@ -172,7 +173,7 @@ const ConfigureModal = ({
|
||||
case COURSE_BLOCK_NAMES.libraryContent.id:
|
||||
case COURSE_BLOCK_NAMES.splitTest.id:
|
||||
case COURSE_BLOCK_NAMES.component.id:
|
||||
// groupAccess should be {partitionId: [group1, group2]} or {} if selectedPartitionIndex === -1
|
||||
// groupAccess should be {partitionId: [group1, group2]} or {} if selectedPartitionIndex === -1
|
||||
if (data.selectedPartitionIndex >= 0) {
|
||||
const partitionId = userPartitionInfo.selectablePartitions[data.selectedPartitionIndex].id;
|
||||
groupAccess[partitionId] = data.selectedGroups.map(g => parseInt(g, 10));
|
||||
@@ -239,6 +240,7 @@ const ConfigureModal = ({
|
||||
releasedToStudents={releasedToStudents}
|
||||
wasExamEverLinkedWithExternal={wasExamEverLinkedWithExternal}
|
||||
enableProctoredExams={enableProctoredExams}
|
||||
enableTimedExams={enableTimedExams}
|
||||
supportsOnboarding={supportsOnboarding}
|
||||
showReviewRules={showReviewRules}
|
||||
wasProctoredExam={isProctoredExam}
|
||||
@@ -325,6 +327,7 @@ ConfigureModal.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onConfigureSubmit: PropTypes.func.isRequired,
|
||||
enableProctoredExams: PropTypes.bool,
|
||||
enableTimedExams: PropTypes.bool,
|
||||
currentItemData: PropTypes.shape({
|
||||
displayName: PropTypes.string,
|
||||
start: PropTypes.string,
|
||||
|
||||
@@ -301,3 +301,131 @@ describe('<ConfigureModal /> for XBlock', () => {
|
||||
expect(queryByText(messages.discussionEnabledDescription.defaultMessage)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('<ConfigureModal /> with enableTimedExams prop', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
const renderWithTimedExamsProps = (enableTimedExams = true) => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<ConfigureModal
|
||||
isOpen
|
||||
onClose={onCloseMock}
|
||||
onConfigureSubmit={onConfigureSubmitMock}
|
||||
currentItemData={currentSubsectionMock}
|
||||
enableTimedExams={enableTimedExams}
|
||||
isSelfPaced={false}
|
||||
/>
|
||||
</IntlProvider>
|
||||
,
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
it('passes enableTimedExams=true to AdvancedTab', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { getByRole, getByText } = renderWithTimedExamsProps(true);
|
||||
|
||||
const advancedTab = getByRole('tab', {
|
||||
name: messages.advancedTabTitle.defaultMessage,
|
||||
});
|
||||
await user.click(advancedTab);
|
||||
|
||||
expect(
|
||||
getByText(messages.setSpecialExam.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const noneRadio = getByRole('radio', {
|
||||
name: messages.none.defaultMessage,
|
||||
});
|
||||
const timedRadio = getByRole('radio', {
|
||||
name: messages.timed.defaultMessage,
|
||||
});
|
||||
|
||||
expect(noneRadio).not.toBeDisabled();
|
||||
expect(timedRadio).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('passes enableTimedExams=false to AdvancedTab and shows disabled state', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { getByRole, getByText } = renderWithTimedExamsProps(false);
|
||||
|
||||
const advancedTab = getByRole('tab', {
|
||||
name: messages.advancedTabTitle.defaultMessage,
|
||||
});
|
||||
await user.click(advancedTab);
|
||||
|
||||
expect(
|
||||
getByText(messages.setSpecialExam.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const noneRadio = getByRole('radio', {
|
||||
name: messages.none.defaultMessage,
|
||||
});
|
||||
const timedRadio = getByRole('radio', {
|
||||
name: messages.timed.defaultMessage,
|
||||
});
|
||||
|
||||
expect(noneRadio).toBeDisabled();
|
||||
expect(timedRadio).toBeDisabled();
|
||||
});
|
||||
|
||||
it('shows tooltip when enableTimedExams is false', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { getByRole } = renderWithTimedExamsProps(false);
|
||||
|
||||
const advancedTab = getByRole('tab', {
|
||||
name: messages.advancedTabTitle.defaultMessage,
|
||||
});
|
||||
await user.click(advancedTab);
|
||||
|
||||
const buttons = getByRole('tab', {
|
||||
name: messages.advancedTabTitle.defaultMessage,
|
||||
}).parentElement.querySelectorAll('button');
|
||||
expect(buttons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('defaults enableTimedExams to false when not provided', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const { getByRole } = render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<ConfigureModal
|
||||
isOpen
|
||||
onClose={onCloseMock}
|
||||
onConfigureSubmit={onConfigureSubmitMock}
|
||||
currentItemData={currentSubsectionMock}
|
||||
isSelfPaced={false}
|
||||
/>
|
||||
</IntlProvider>
|
||||
,
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
const advancedTab = getByRole('tab', {
|
||||
name: messages.advancedTabTitle.defaultMessage,
|
||||
});
|
||||
await user.click(advancedTab);
|
||||
|
||||
const noneRadio = getByRole('radio', {
|
||||
name: messages.none.defaultMessage,
|
||||
});
|
||||
const timedRadio = getByRole('radio', {
|
||||
name: messages.timed.defaultMessage,
|
||||
});
|
||||
|
||||
expect(noneRadio).toBeDisabled();
|
||||
expect(timedRadio).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -231,6 +231,10 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.practice-exam-description',
|
||||
defaultMessage: 'Use a practice proctored exam to introduce learners to the proctoring tools and processes. Results of a practice exam do not affect a learner\'s grade.',
|
||||
},
|
||||
timedExamsDisabledTooltip: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.timed-exams-disabled-tooltip',
|
||||
defaultMessage: 'Timed exams are not enabled for this Open edX instance',
|
||||
},
|
||||
advancedTabTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.title',
|
||||
defaultMessage: 'Advanced',
|
||||
|
||||
Reference in New Issue
Block a user