Skip to content

Commit 83be311

Browse files
committed
feat: add editable state tests for various sections in Schedule and Details
1 parent fc383af commit 83be311

17 files changed

Lines changed: 345 additions & 6 deletions

File tree

src/schedule-and-details/ScheduleAndDetails.test.jsx

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { executeThunk } from '@src/utils';
1010
import genericMessages from '@src/generic/help-sidebar/messages';
1111
import { DATE_FORMAT } from '@src/constants';
1212
import { getCourseSettingsApiUrl } from '@src/data/api';
13+
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
14+
import { useUserPermissionsWithAuthzCourse } from '@src/authz/hooks';
1315

1416
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
1517
import { courseDetailsMock, courseSettingsMock } from './__mocks__';
@@ -22,6 +24,17 @@ import scheduleMessages from './schedule-section/messages';
2224
import messages from './messages';
2325
import ScheduleAndDetails from '.';
2426

27+
jest.mock('@src/authz/hooks', () => ({
28+
useUserPermissionsWithAuthzCourse: jest.fn().mockReturnValue({
29+
isLoading: false,
30+
permissions: {
31+
canViewScheduleAndDetails: true,
32+
canEditSchedule: true,
33+
canEditDetails: true,
34+
},
35+
}),
36+
}));
37+
2538
let axiosMock;
2639
let store;
2740
const courseId = '123';
@@ -167,3 +180,92 @@ describe('<ScheduleAndDetails />', () => {
167180
expect(getByText(messages.alertFail.defaultMessage)).toBeInTheDocument();
168181
});
169182
});
183+
184+
describe('<ScheduleAndDetails /> permissions', () => {
185+
beforeEach(() => {
186+
jest.restoreAllMocks();
187+
const mocks = initializeMocks();
188+
axiosMock = mocks.axiosMock;
189+
store = mocks.reduxStore;
190+
axiosMock.onGet(getCourseDetailsApiUrl(courseId)).reply(200, courseDetailsMock);
191+
axiosMock.onGet(getCourseSettingsApiUrl(courseId)).reply(200, courseSettingsMock);
192+
axiosMock.onPut(getCourseDetailsApiUrl(courseId)).reply(200);
193+
jest.mocked(useUserPermissionsWithAuthzCourse).mockReturnValue({
194+
isLoading: false,
195+
permissions: {
196+
canViewScheduleAndDetails: true,
197+
canEditSchedule: true,
198+
canEditDetails: true,
199+
},
200+
});
201+
});
202+
203+
it('renders normally when authz flag is disabled (no regression)', async () => {
204+
mockWaffleFlags({ enableAuthzCourseAuthoring: false });
205+
const { getAllByText } = renderComponent();
206+
await waitFor(() => {
207+
expect(getAllByText(messages.headingTitle.defaultMessage).length).toBeGreaterThan(0);
208+
});
209+
});
210+
211+
it('renders normally when user has all permissions', async () => {
212+
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
213+
const { getAllByText } = renderComponent();
214+
await waitFor(() => {
215+
expect(getAllByText(messages.headingTitle.defaultMessage).length).toBeGreaterThan(0);
216+
});
217+
});
218+
219+
it('shows PermissionDeniedAlert when user lacks view permission', async () => {
220+
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
221+
jest.mocked(useUserPermissionsWithAuthzCourse).mockReturnValue({
222+
isLoading: false,
223+
permissions: { canViewScheduleAndDetails: false, canEditSchedule: false, canEditDetails: false },
224+
});
225+
const { getByTestId } = renderComponent();
226+
await waitFor(() => {
227+
expect(getByTestId('permissionDeniedAlert')).toBeInTheDocument();
228+
});
229+
});
230+
231+
it('disables schedule date inputs when user lacks edit_schedule permission', async () => {
232+
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
233+
jest.mocked(useUserPermissionsWithAuthzCourse).mockReturnValue({
234+
isLoading: false,
235+
permissions: { canViewScheduleAndDetails: true, canEditSchedule: false, canEditDetails: true },
236+
});
237+
const { getAllByPlaceholderText } = renderComponent();
238+
await waitFor(() => {
239+
const dateInputs = getAllByPlaceholderText(DATE_FORMAT.toLocaleUpperCase());
240+
dateInputs.forEach((input) => expect(input).toBeDisabled());
241+
});
242+
});
243+
244+
it('disables pacing and details inputs when user lacks edit_details permission', async () => {
245+
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
246+
jest.mocked(useUserPermissionsWithAuthzCourse).mockReturnValue({
247+
isLoading: false,
248+
permissions: { canViewScheduleAndDetails: true, canEditSchedule: true, canEditDetails: false },
249+
});
250+
const { getAllByRole } = renderComponent();
251+
await waitFor(() => {
252+
const radios = getAllByRole('radio');
253+
radios.forEach((radio) => expect(radio).toBeDisabled());
254+
});
255+
});
256+
257+
it('save button cannot be triggered when user has no edit permissions', async () => {
258+
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
259+
jest.mocked(useUserPermissionsWithAuthzCourse).mockReturnValue({
260+
isLoading: false,
261+
permissions: { canViewScheduleAndDetails: true, canEditSchedule: false, canEditDetails: false },
262+
});
263+
const { getAllByPlaceholderText, queryByText } = renderComponent();
264+
// Wait for page to load
265+
const dateInputs = await waitFor(() => getAllByPlaceholderText(DATE_FORMAT.toLocaleUpperCase()));
266+
// All date inputs must be disabled (no edit_schedule permission)
267+
dateInputs.forEach((input) => expect(input).toBeDisabled());
268+
// No changes can be made so the save button never appears
269+
expect(queryByText(messages.buttonSaveText.defaultMessage)).not.toBeInTheDocument();
270+
});
271+
});

src/schedule-and-details/details-section/DetailsSection.test.jsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,26 @@ describe('<DetailsSection />', () => {
5757
getByRole('button', { name: messages.dropdownEmpty.defaultMessage }),
5858
).toBeInTheDocument();
5959
});
60+
61+
it('disables the language dropdown toggle when isEditable is false', () => {
62+
const { getByRole } = render(<RootWrapper {...props} isEditable={false} />);
63+
const toggle = getByRole('button', { name: courseSettingsMock.languageOptions[1][1] });
64+
expect(toggle).toBeDisabled();
65+
});
66+
67+
it('does not call onChange when dropdown item clicked while isEditable is false', () => {
68+
onChangeMock.mockClear();
69+
const { getByRole } = render(<RootWrapper {...props} isEditable={false} />);
70+
// Toggle is disabled, so clicking it does not open the dropdown
71+
const toggle = getByRole('button', { name: courseSettingsMock.languageOptions[1][1] });
72+
expect(toggle).toBeDisabled();
73+
fireEvent.click(toggle);
74+
expect(onChangeMock).not.toHaveBeenCalled();
75+
});
76+
77+
it('enables the language dropdown when isEditable is true', () => {
78+
const { getByRole } = render(<RootWrapper {...props} isEditable />);
79+
const toggle = getByRole('button', { name: courseSettingsMock.languageOptions[1][1] });
80+
expect(toggle).not.toBeDisabled();
81+
});
6082
});

src/schedule-and-details/instructors-section/InstructorsSection.test.jsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,14 @@ describe('<InstructorsSection />', () => {
106106
}],
107107
}, 'instructorInfo');
108108
});
109+
110+
it('disables add button when isEditable is false', () => {
111+
render(<RootWrapper {...props} isEditable={false} />);
112+
expect(screen.getByRole('button', { name: messages.instructorAdd.defaultMessage })).toBeDisabled();
113+
});
114+
115+
it('enables add button when isEditable is true', () => {
116+
render(<RootWrapper {...props} isEditable />);
117+
expect(screen.getByRole('button', { name: messages.instructorAdd.defaultMessage })).not.toBeDisabled();
118+
});
109119
});

src/schedule-and-details/instructors-section/instructor-container/InstructorContainer.test.jsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,18 @@ describe('<InstructorContainer />', () => {
122122
fireEvent.click(deleteBtn);
123123
expect(onDeleteMock).toHaveBeenCalledWith(props.idx);
124124
});
125+
126+
it('disables all inputs and delete button when isEditable is false', () => {
127+
const { getAllByRole, getByRole } = render(<RootWrapper {...props} isEditable={false} />);
128+
const textboxes = getAllByRole('textbox');
129+
textboxes.forEach((input) => expect(input).toBeDisabled());
130+
expect(getByRole('button', { name: messages.instructorDelete.defaultMessage })).toBeDisabled();
131+
});
132+
133+
it('enables all inputs and delete button when isEditable is true', () => {
134+
const { getAllByRole, getByRole } = render(<RootWrapper {...props} isEditable />);
135+
const textboxes = getAllByRole('textbox');
136+
textboxes.forEach((input) => expect(input).not.toBeDisabled());
137+
expect(getByRole('button', { name: messages.instructorDelete.defaultMessage })).not.toBeDisabled();
138+
});
125139
});

src/schedule-and-details/introducing-section/IntroducingSection.test.jsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,14 @@ describe('<IntroducingSection />', () => {
8383
expect(queryAllByText(messages.courseOverviewLabel.defaultMessage).length).toBe(0);
8484
expect(queryAllByText(messages.courseAboutSidebarLabel.defaultMessage).length).toBe(0);
8585
});
86+
87+
it('disables the short description textarea when isEditable is false', () => {
88+
const { getByLabelText } = render(<RootWrapper {...props} isEditable={false} />);
89+
expect(getByLabelText(messages.courseShortDescriptionLabel.defaultMessage)).toBeDisabled();
90+
});
91+
92+
it('enables the short description textarea when isEditable is true', () => {
93+
const { getByLabelText } = render(<RootWrapper {...props} isEditable />);
94+
expect(getByLabelText(messages.courseShortDescriptionLabel.defaultMessage)).not.toBeDisabled();
95+
});
8696
});

src/schedule-and-details/introducing-section/extended-course-details/ExtendedCourseDetails.test.jsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,16 @@ describe('<ExtendedCourseDetails />', () => {
6464
});
6565
expect(onChangeMock).toHaveBeenCalledWith('abc', 'title');
6666
});
67+
68+
it('disables all inputs when isEditable is false', () => {
69+
const { getAllByRole } = render(<RootWrapper {...props} isEditable={false} />);
70+
const inputs = getAllByRole('textbox');
71+
inputs.forEach((input) => expect(input).toBeDisabled());
72+
});
73+
74+
it('enables all inputs when isEditable is true', () => {
75+
const { getAllByRole } = render(<RootWrapper {...props} isEditable />);
76+
const inputs = getAllByRole('textbox');
77+
inputs.forEach((input) => expect(input).not.toBeDisabled());
78+
});
6779
});

src/schedule-and-details/introducing-section/introduction-video/IntroductionVideo.test.jsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,18 @@ describe('<IntroductionVideo />', () => {
6262
fireEvent.click(button);
6363
expect(onChangeMock).toHaveBeenCalledWith('', 'introVideo');
6464
});
65+
66+
it('disables input and delete button when isEditable is false', () => {
67+
const initialProps = { ...props, introVideo: 'BvgNgTPTkSo', isEditable: false };
68+
const { getByPlaceholderText, getByRole } = render(<RootWrapper {...initialProps} />);
69+
expect(getByPlaceholderText(messages.courseIntroductionVideoPlaceholder.defaultMessage)).toBeDisabled();
70+
expect(getByRole('button', { name: messages.courseIntroductionVideoDelete.defaultMessage })).toBeDisabled();
71+
});
72+
73+
it('enables input and delete button when isEditable is true', () => {
74+
const initialProps = { ...props, introVideo: 'BvgNgTPTkSo', isEditable: true };
75+
const { getByPlaceholderText, getByRole } = render(<RootWrapper {...initialProps} />);
76+
expect(getByPlaceholderText(messages.courseIntroductionVideoPlaceholder.defaultMessage)).not.toBeDisabled();
77+
expect(getByRole('button', { name: messages.courseIntroductionVideoDelete.defaultMessage })).not.toBeDisabled();
78+
});
6579
});

src/schedule-and-details/learning-outcomes-section/InstructorsSection.test.jsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,18 @@ describe('<LearningOutcomesSection />', () => {
6666

6767
expect(onChangeMock).toHaveBeenCalledWith(['abc'], 'learningInfo');
6868
});
69+
70+
it('disables input, delete button and add button when isEditable is false', () => {
71+
const { getByPlaceholderText, getByRole } = render(<RootWrapper {...props} isEditable={false} />);
72+
expect(getByPlaceholderText(messages.outcomesInputPlaceholder.defaultMessage)).toBeDisabled();
73+
expect(getByRole('button', { name: messages.outcomesDelete.defaultMessage })).toBeDisabled();
74+
expect(getByRole('button', { name: messages.outcomesAdd.defaultMessage })).toBeDisabled();
75+
});
76+
77+
it('enables input, delete button and add button when isEditable is true', () => {
78+
const { getByPlaceholderText, getByRole } = render(<RootWrapper {...props} isEditable />);
79+
expect(getByPlaceholderText(messages.outcomesInputPlaceholder.defaultMessage)).not.toBeDisabled();
80+
expect(getByRole('button', { name: messages.outcomesDelete.defaultMessage })).not.toBeDisabled();
81+
expect(getByRole('button', { name: messages.outcomesAdd.defaultMessage })).not.toBeDisabled();
82+
});
6983
});

src/schedule-and-details/license-section/LicenseSection.test.jsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,16 @@ describe('<LicenseSection />', () => {
2525
expect(getByText(messages.licenseTitle.defaultMessage)).toBeInTheDocument();
2626
expect(getByText(messages.licenseDescription.defaultMessage)).toBeInTheDocument();
2727
});
28+
29+
it('disables license type buttons when isEditable is false', () => {
30+
const { getAllByRole } = render(<RootWrapper {...props} isEditable={false} />);
31+
const buttons = getAllByRole('button');
32+
buttons.forEach((button) => expect(button).toBeDisabled());
33+
});
34+
35+
it('enables license type buttons when isEditable is true', () => {
36+
const { getAllByRole } = render(<RootWrapper {...props} isEditable />);
37+
const buttons = getAllByRole('button');
38+
buttons.forEach((button) => expect(button).not.toBeDisabled());
39+
});
2840
});

src/schedule-and-details/license-section/license-commons-options/LicenseCommonsOptions.test.jsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,29 @@ describe('<LicenseCommonsOptions />', () => {
4747
expect(props.onToggleCheckbox).not.toHaveBeenCalled();
4848
fireEvent.click(checkboxList[1]);
4949
expect(props.onToggleCheckbox).toHaveBeenCalledWith(LICENSE_COMMONS_OPTIONS.nonCommercial);
50-
// Note: there is no point in asserting that the checkbox is now checked,
51-
// because it is a controlled component that never changes unless the props change.
52-
// This test should really be implemented in a higher level component/page.
53-
// await waitFor(() => {
54-
// expect(checkboxList[1].checked).toBeFalsy();
55-
// });
50+
});
51+
52+
it('disables all non-fixed checkboxes when isEditable is false', () => {
53+
const { getAllByRole } = render(<RootWrapper {...props} isEditable={false} />);
54+
const checkboxList = getAllByRole('checkbox');
55+
// All checkboxes (including attribution which is always disabled) should be disabled
56+
checkboxList.forEach((checkbox) => expect(checkbox).toBeDisabled());
57+
});
58+
59+
it('does not call onToggleCheckbox when clicked while isEditable is false', () => {
60+
onToggleCheckboxMock.mockClear();
61+
const { getAllByRole } = render(<RootWrapper {...props} isEditable={false} />);
62+
const checkboxList = getAllByRole('checkbox');
63+
fireEvent.click(checkboxList[1]);
64+
expect(onToggleCheckboxMock).not.toHaveBeenCalled();
65+
});
66+
67+
it('non-fixed checkboxes are enabled when isEditable is true', () => {
68+
const { getAllByRole } = render(<RootWrapper {...props} isEditable />);
69+
const checkboxList = getAllByRole('checkbox');
70+
// checkboxList[0] is attribution (always disabled), rest should be enabled
71+
expect(checkboxList[1]).not.toBeDisabled();
72+
expect(checkboxList[2]).not.toBeDisabled();
73+
expect(checkboxList[3]).not.toBeDisabled();
5674
});
5775
});

0 commit comments

Comments
 (0)