Skip to content

Commit ea4a75f

Browse files
committed
test: improve coverage
1 parent 90e4785 commit ea4a75f

3 files changed

Lines changed: 222 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { initializeMocks, render, screen } from '@src/testUtils';
2+
import userEvent from '@testing-library/user-event';
3+
4+
import { useCourseItemData } from '@src/course-outline/data/apiHooks';
5+
import { ReleaseSection } from './ReleaseSection';
6+
7+
// Make useStateWithCallback synchronous so callbacks call onChange immediately
8+
jest.mock('@src/hooks', () => ({
9+
useStateWithCallback: (defaultValue: any, cb?: any) => {
10+
const { useState } = jest.requireActual('react');
11+
const [state, setState] = useState(defaultValue);
12+
const wrappedSetState = (val: any) => {
13+
const newVal = typeof val === 'function' ? val(state) : val;
14+
setState(newVal);
15+
if (cb) { cb(newVal); }
16+
};
17+
return [state, wrappedSetState];
18+
},
19+
}));
20+
21+
// Mock DatepickerControl so we can trigger onChange easily
22+
jest.mock('@src/generic/datepicker-control', () => ({
23+
DATEPICKER_TYPES: { date: 'date', time: 'time' },
24+
DatepickerControl: ({ onChange, type }: any) => (
25+
<button type="button" onClick={() => onChange(type === 'date' ? '2025-12-31' : '12:00')}>
26+
{type}
27+
</button>
28+
),
29+
}));
30+
31+
jest.mock('@src/course-outline/data/apiHooks', () => ({
32+
useCourseItemData: jest.fn(),
33+
}));
34+
35+
const mockUseCourseItemData = useCourseItemData as jest.Mock;
36+
37+
describe('ReleaseSection', () => {
38+
beforeEach(() => {
39+
initializeMocks();
40+
mockUseCourseItemData.mockReturnValue({ data: { start: null } });
41+
});
42+
43+
it('renders date and time pickers', () => {
44+
render(<ReleaseSection itemId="i" onChange={jest.fn()} />);
45+
expect(screen.getByRole('button', { name: 'date' })).toBeInTheDocument();
46+
expect(screen.getByRole('button', { name: 'time' })).toBeInTheDocument();
47+
});
48+
49+
it('calls onChange when pickers change', async () => {
50+
const user = userEvent.setup();
51+
const onChange = jest.fn();
52+
render(<ReleaseSection itemId="i" onChange={onChange} />);
53+
54+
await user.click(screen.getByRole('button', { name: 'date' }));
55+
expect(onChange).toHaveBeenCalledWith('2025-12-31');
56+
57+
await user.click(screen.getByRole('button', { name: 'time' }));
58+
expect(onChange).toHaveBeenCalledWith('12:00');
59+
});
60+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {
2+
initializeMocks, render, screen, waitFor,
3+
} from '@src/testUtils';
4+
import userEvent from '@testing-library/user-event';
5+
import { useCourseItemData } from '@src/course-outline/data/apiHooks';
6+
import { VisibilityTypes } from '@src/data/constants';
7+
import { VisibilitySection } from './VisibilitySection';
8+
9+
jest.mock('@src/course-outline/data/apiHooks', () => ({
10+
...jest.requireActual('@src/course-outline/data/apiHooks'),
11+
useCourseItemData: jest.fn(),
12+
}));
13+
14+
const mockUseCourseItemData = useCourseItemData as jest.Mock;
15+
16+
const defaultProps = {
17+
itemId: 'block-v1:course+type@sequential+block@test',
18+
isSubsection: true,
19+
onChange: jest.fn(),
20+
};
21+
22+
describe('VisibilitySection component', () => {
23+
beforeEach(() => {
24+
initializeMocks();
25+
mockUseCourseItemData.mockReturnValue({ data: undefined });
26+
});
27+
28+
it('renders title and buttons', async () => {
29+
render(<VisibilitySection {...defaultProps} />);
30+
expect(await screen.findByText('Visibility')).toBeInTheDocument();
31+
expect(await screen.findByRole('button', { name: 'Student Visible' })).toBeInTheDocument();
32+
expect(await screen.findByRole('button', { name: 'Staff Only' })).toBeInTheDocument();
33+
});
34+
35+
it('clicking staff only calls onChange with staff and hideAfterDue false', async () => {
36+
const user = userEvent.setup();
37+
const onChange = jest.fn();
38+
render(<VisibilitySection {...defaultProps} onChange={onChange} />);
39+
40+
await user.click(await screen.findByRole('button', { name: 'Staff Only' }));
41+
await waitFor(async () => {
42+
expect(onChange).toHaveBeenCalledWith({ isVisibleToStaffOnly: true, hideAfterDue: false });
43+
});
44+
});
45+
46+
it('clicking student visible calls onChange with isVisibleToStaffOnly false', async () => {
47+
const user = userEvent.setup();
48+
const onChange = jest.fn();
49+
mockUseCourseItemData.mockReturnValue({ data: { visibilityState: VisibilityTypes.STAFF_ONLY } });
50+
render(<VisibilitySection {...defaultProps} onChange={onChange} />);
51+
52+
await user.click(await screen.findByRole('button', { name: 'Student Visible' }));
53+
await waitFor(async () => {
54+
expect(onChange).toHaveBeenCalledWith({ isVisibleToStaffOnly: false });
55+
});
56+
});
57+
58+
it('shows checkbox when subsection and not staff only, and toggling it calls onChange', async () => {
59+
const user = userEvent.setup();
60+
const onChange = jest.fn();
61+
// initial data not staff only
62+
mockUseCourseItemData.mockReturnValue({ data: { visibilityState: undefined, hideAfterDue: false } });
63+
render(<VisibilitySection {...defaultProps} onChange={onChange} />);
64+
65+
const checkbox = await screen.findByRole('checkbox');
66+
await user.click(checkbox);
67+
await waitFor(async () => {
68+
expect(onChange).toHaveBeenCalledWith({ hideAfterDue: true, isVisibleToStaffOnly: false });
69+
});
70+
});
71+
72+
it('hides checkbox when staff visible', async () => {
73+
const onChange = jest.fn();
74+
// when item is staff only, checkbox should not be present
75+
mockUseCourseItemData.mockReturnValue({ data: { visibilityState: VisibilityTypes.STAFF_ONLY } });
76+
render(<VisibilitySection {...defaultProps} onChange={onChange} />);
77+
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
78+
});
79+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { IframeProvider } from '@src/generic/hooks/context/iFrameContext';
2+
import { initializeMocks, render, screen } from '@src/testUtils';
3+
import { useParams } from 'react-router-dom';
4+
import { UnitInfoSidebar } from './UnitInfoSidebar';
5+
6+
jest.mock('react-router-dom', () => ({
7+
...jest.requireActual('react-router-dom'),
8+
useParams: jest.fn(),
9+
}));
10+
11+
jest.mock('@src/course-unit/data/selectors', () => ({
12+
getCourseUnitData: jest.fn(),
13+
getCourseVerticalChildren: jest.fn(),
14+
}));
15+
16+
jest.mock('./PublishControls', () => ({ __esModule: true, default: () => <div>PublishControls</div> }));
17+
jest.mock('@src/generic/block-type-utils', () => ({
18+
...jest.requireActual('@src/generic/block-type-utils'),
19+
ComponentCountSnippet: ({ componentData }: any) => <div>ComponentCount: {JSON.stringify(componentData)}</div>,
20+
getItemIcon: () => () => null,
21+
}));
22+
jest.mock('@src/content-tags-drawer', () => ({ ContentTagsSnippet: ({ contentId }: any) => <div>ContentTags:{contentId}</div> }));
23+
jest.mock('@src/course-unit/unit-sidebar/unit-info/GenericUnitInfoSettings', () => ({
24+
__esModule: true,
25+
GenericUnitInfoSettings: () => <div>GenericUnitInfoSettings</div>,
26+
}));
27+
28+
jest.mock('../UnitSidebarContext', () => ({ useUnitSidebarContext: jest.fn() }));
29+
30+
const mockUseParams = useParams as jest.MockedFunction<typeof useParams>;
31+
const selectors = jest.requireMock('@src/course-unit/data/selectors') as any;
32+
const unitSidebarContext = jest.requireMock('../UnitSidebarContext') as any;
33+
34+
const renderComponent = () => {
35+
render(
36+
<IframeProvider>
37+
<UnitInfoSidebar />
38+
</IframeProvider>,
39+
);
40+
};
41+
42+
describe('UnitInfoSidebar', () => {
43+
beforeEach(() => {
44+
initializeMocks();
45+
mockUseParams.mockReturnValue({ blockId: 'block-1' } as any);
46+
47+
selectors.getCourseUnitData.mockReturnValue({
48+
displayName: 'Unit title',
49+
id: 'block-1',
50+
visibilityState: undefined,
51+
discussionEnabled: false,
52+
userPartitionInfo: null,
53+
});
54+
55+
selectors.getCourseVerticalChildren.mockReturnValue({
56+
children: [
57+
{ blockType: 'html' }, { blockType: 'problem' }, { blockType: 'html' },
58+
],
59+
});
60+
});
61+
62+
it('renders title and details components and sets default tab', () => {
63+
const setCurrentTabKey = jest.fn();
64+
unitSidebarContext.useUnitSidebarContext.mockReturnValue({ currentTabKey: 'details', setCurrentTabKey });
65+
66+
renderComponent();
67+
68+
expect(screen.getByText('Unit title')).toBeInTheDocument();
69+
expect(screen.getByText(/ComponentCount/)).toBeInTheDocument();
70+
expect(screen.getByText('ContentTags:block-1')).toBeInTheDocument();
71+
// effect should set default tab to details
72+
expect(setCurrentTabKey).toHaveBeenCalledWith('details');
73+
});
74+
75+
it('renders settings tab content when active', () => {
76+
const setCurrentTabKey = jest.fn();
77+
unitSidebarContext.useUnitSidebarContext.mockReturnValue({ currentTabKey: 'settings', setCurrentTabKey });
78+
79+
renderComponent();
80+
81+
expect(screen.getByText('GenericUnitInfoSettings')).toBeInTheDocument();
82+
});
83+
});

0 commit comments

Comments
 (0)