1- import { initializeMocks , render , screen } from '@src/testUtils' ;
1+ import { fireEvent , initializeMocks , render , screen } from '@src/testUtils' ;
22import { SelectionState } from '@src/data/types' ;
33import { OutlineSidebarProvider } from '@src/course-outline/outline-sidebar/OutlineSidebarContext' ;
44import { getXBlockApiUrl } from '@src/course-outline/data/api' ;
55import userEvent from '@testing-library/user-event' ;
6+ import { getDownstreamApiUrl } from '@src/generic/unlink-modal/data/api' ;
67import { InfoSidebar } from './InfoSidebar' ;
78
89let selectedContainerState : SelectionState | undefined ;
@@ -11,6 +12,8 @@ jest.mock('@src/course-outline/outline-sidebar/OutlineSidebarContext', () => ({
1112 useOutlineSidebarContext : ( ) => ( {
1213 ...jest . requireActual ( '@src/course-outline/outline-sidebar/OutlineSidebarContext' ) . useOutlineSidebarContext ( ) ,
1314 selectedContainerState,
15+ clearSelection : jest . fn ( ) ,
16+ setSelectedContainerState : jest . fn ( ) ,
1417 } ) ,
1518} ) ) ;
1619
@@ -23,12 +26,33 @@ jest.mock('@src/course-outline/data/apiHooks', () => ({
2326} ) ) ;
2427
2528const openPublishModal = jest . fn ( ) ;
29+ const openDeleteModal = jest . fn ( ) ;
30+ const openUnlinkModal = jest . fn ( ) ;
31+ const handleDuplicateSectionSubmit = jest . fn ( ) ;
32+ const handleDuplicateUnitSubmit = jest . fn ( ) ;
33+ const handleDuplicateSubsectionSubmit = jest . fn ( ) ;
34+ const mockedNavigate = jest . fn ( ) ;
35+
36+ jest . mock ( 'react-router-dom' , ( ) => ( {
37+ ...jest . requireActual ( 'react-router-dom' ) ,
38+ useNavigate : ( ) => mockedNavigate ,
39+ } ) ) ;
40+
2641jest . mock ( '@src/CourseAuthoringContext' , ( ) => ( {
2742 useCourseAuthoringContext : ( ) => ( {
2843 courseId : 5 ,
2944 setCurrentSelection : jest . fn ( ) ,
3045 openPublishModal,
46+ openDeleteModal,
47+ openUnlinkModal,
48+ handleDuplicateUnitSubmit,
3149 getUnitUrl : jest . fn ( ) ,
50+ sections : [ ] ,
51+ updateUnitOrderByIndex : jest . fn ( ) ,
52+ handleDuplicateSectionSubmit,
53+ updateSectionOrderByIndex : jest . fn ( ) ,
54+ handleDuplicateSubsectionSubmit,
55+ updateSubsectionOrderByIndex : jest . fn ( ) ,
3256 } ) ,
3357} ) ) ;
3458
@@ -43,6 +67,12 @@ describe('InfoSidebar component', () => {
4367 beforeEach ( ( ) => {
4468 const mocks = initializeMocks ( ) ;
4569 axiosMock = mocks . axiosMock ;
70+ openDeleteModal . mockClear ( ) ;
71+ openUnlinkModal . mockClear ( ) ;
72+ handleDuplicateSectionSubmit . mockClear ( ) ;
73+ handleDuplicateUnitSubmit . mockClear ( ) ;
74+ handleDuplicateSubsectionSubmit . mockClear ( ) ;
75+ mockedNavigate . mockClear ( ) ;
4676 } ) ;
4777
4878 it ( 'renders InfoSidebar with course info if selectedContainerState is undefined' , async ( ) => {
@@ -133,4 +163,285 @@ describe('InfoSidebar component', () => {
133163 sectionId : selectedContainerState . sectionId ,
134164 } ) ;
135165 } ) ;
136- } ) ;
166+
167+ describe ( 'UnitSidebar menus' , ( ) => {
168+ const unitId = 'block-v1:UNIX+UX1+2025_T3+type@vertical+block@unit1' ;
169+ const upstreamRef = 'lb:org:lib:vertical:unit-id' ;
170+
171+ const unitData = {
172+ id : unitId ,
173+ displayName : 'unit name' ,
174+ category : 'vertical' ,
175+ hasChanges : false ,
176+ actions : { deletable : true , duplicable : true , draggable : false } ,
177+ upstreamInfo : null ,
178+ } ;
179+
180+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
181+ const renderUnitMenu = async ( data : any = unitData ) => {
182+ selectedContainerState = {
183+ currentId : unitId ,
184+ subsectionId : 'block-v1:UNIX+UX1+2025_T3+type@sequential+block@seq1' ,
185+ sectionId : 'block-v1:UNIX+UX1+2025_T3+type@chapter+block@ch1' ,
186+ } ;
187+ axiosMock . onGet ( getXBlockApiUrl ( unitId ) ) . reply ( 200 , data ) ;
188+ renderComponent ( ) ;
189+ await screen . findByText ( data . displayName ) ;
190+ await screen . findByRole ( 'button' , { name : 'Item Menu' } ) ;
191+ } ;
192+
193+ it ( 'calls openDeleteModal when Delete is clicked in unit menu' , async ( ) => {
194+ const user = userEvent . setup ( ) ;
195+ await renderUnitMenu ( ) ;
196+
197+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
198+ fireEvent . click ( menuToggle ) ;
199+
200+ const deleteBtn = await screen . findByText ( 'Delete' ) ;
201+ await user . click ( deleteBtn ) ;
202+
203+ expect ( openDeleteModal ) . toHaveBeenCalled ( ) ;
204+ } ) ;
205+
206+ it ( 'calls handleDuplicateUnitSubmit when Duplicate is clicked in unit menu' , async ( ) => {
207+ const user = userEvent . setup ( ) ;
208+ await renderUnitMenu ( ) ;
209+
210+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
211+ fireEvent . click ( menuToggle ) ;
212+
213+ const duplicateBtn = await screen . findByText ( 'Duplicate' ) ;
214+ await user . click ( duplicateBtn ) ;
215+
216+ expect ( handleDuplicateUnitSubmit ) . toHaveBeenCalled ( ) ;
217+ } ) ;
218+
219+ it ( 'calls openUnlinkModal when Unlink is clicked in unit menu' , async ( ) => {
220+ const user = userEvent . setup ( ) ;
221+ const unitWithUpstream = {
222+ ...unitData ,
223+ actions : { ...unitData . actions , unlinkable : true } ,
224+ upstreamInfo : { upstreamRef } ,
225+ } ;
226+ await renderUnitMenu ( unitWithUpstream ) ;
227+
228+ axiosMock . onDelete ( getDownstreamApiUrl ( unitId ) ) . reply ( 200 , { } ) ;
229+
230+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
231+ fireEvent . click ( menuToggle ) ;
232+
233+ const unlinkBtn = await screen . findByText ( 'Unlink from Library' ) ;
234+ await user . click ( unlinkBtn ) ;
235+
236+ expect ( openUnlinkModal ) . toHaveBeenCalledWith ( expect . objectContaining ( {
237+ value : unitWithUpstream ,
238+ sectionId : selectedContainerState ?. sectionId ,
239+ subsectionId : selectedContainerState ?. subsectionId ,
240+ } ) ) ;
241+ } ) ;
242+
243+ it ( 'navigates to library when View in Library is clicked in unit menu' , async ( ) => {
244+ const user = userEvent . setup ( ) ;
245+ const unitWithUpstream = {
246+ ...unitData ,
247+ upstreamInfo : { upstreamRef } ,
248+ } ;
249+ await renderUnitMenu ( unitWithUpstream ) ;
250+
251+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
252+ fireEvent . click ( menuToggle ) ;
253+
254+ const viewLibBtn = await screen . findByText ( 'View in Library' ) ;
255+ await user . click ( viewLibBtn ) ;
256+
257+ expect ( mockedNavigate ) . toHaveBeenCalledWith (
258+ expect . stringContaining ( '/library/' ) ,
259+ ) ;
260+ } ) ;
261+ } ) ;
262+
263+ describe ( 'SubsectionSidebar menus' , ( ) => {
264+ const subsectionId = 'block-v1:UNIX+UX1+2025_T3+type@sequential+block@sub1' ;
265+ const upstreamRef = 'lb:org:lib:sequential:sub-id' ;
266+
267+ const subsectionData = {
268+ id : subsectionId ,
269+ displayName : 'subsection name' ,
270+ category : 'sequential' ,
271+ hasChanges : false ,
272+ actions : { deletable : true , duplicable : true , draggable : false } ,
273+ upstreamInfo : null ,
274+ } ;
275+
276+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
277+ const renderSubsectionMenu = async ( data : any = subsectionData ) => {
278+ selectedContainerState = {
279+ currentId : subsectionId ,
280+ subsectionId,
281+ sectionId : 'block-v1:UNIX+UX1+2025_T3+type@chapter+block@ch1' ,
282+ } ;
283+ axiosMock . onGet ( getXBlockApiUrl ( subsectionId ) ) . reply ( 200 , data ) ;
284+ renderComponent ( ) ;
285+ await screen . findByText ( data . displayName ) ;
286+ await screen . findByRole ( 'button' , { name : 'Item Menu' } ) ;
287+ } ;
288+
289+ it ( 'calls openDeleteModal when Delete is clicked in subsection menu' , async ( ) => {
290+ const user = userEvent . setup ( ) ;
291+ await renderSubsectionMenu ( ) ;
292+
293+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
294+ fireEvent . click ( menuToggle ) ;
295+
296+ const deleteBtn = await screen . findByText ( 'Delete' ) ;
297+ await user . click ( deleteBtn ) ;
298+
299+ expect ( openDeleteModal ) . toHaveBeenCalled ( ) ;
300+ } ) ;
301+
302+ it ( 'calls handleDuplicateSubsectionSubmit when Duplicate is clicked in subsection menu' , async ( ) => {
303+ const user = userEvent . setup ( ) ;
304+ await renderSubsectionMenu ( ) ;
305+
306+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
307+ fireEvent . click ( menuToggle ) ;
308+
309+ const duplicateBtn = await screen . findByText ( 'Duplicate' ) ;
310+ await user . click ( duplicateBtn ) ;
311+
312+ expect ( handleDuplicateSubsectionSubmit ) . toHaveBeenCalled ( ) ;
313+ } ) ;
314+
315+ it ( 'calls openUnlinkModal when Unlink is clicked in subsection menu' , async ( ) => {
316+ const user = userEvent . setup ( ) ;
317+ const subsectionWithUpstream = {
318+ ...subsectionData ,
319+ actions : { ...subsectionData . actions , unlinkable : true } ,
320+ upstreamInfo : { upstreamRef } ,
321+ } ;
322+ await renderSubsectionMenu ( subsectionWithUpstream ) ;
323+
324+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
325+ fireEvent . click ( menuToggle ) ;
326+
327+ const unlinkBtn = await screen . findByText ( 'Unlink from Library' ) ;
328+ await user . click ( unlinkBtn ) ;
329+
330+ expect ( openUnlinkModal ) . toHaveBeenCalledWith ( expect . objectContaining ( {
331+ value : subsectionWithUpstream ,
332+ sectionId : selectedContainerState ?. sectionId ,
333+ } ) ) ;
334+ } ) ;
335+
336+ it ( 'navigates to library when View in Library is clicked in subsection menu' , async ( ) => {
337+ const user = userEvent . setup ( ) ;
338+ const subsectionWithUpstream = {
339+ ...subsectionData ,
340+ upstreamInfo : { upstreamRef } ,
341+ } ;
342+ await renderSubsectionMenu ( subsectionWithUpstream ) ;
343+
344+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
345+ fireEvent . click ( menuToggle ) ;
346+
347+ const viewLibBtn = await screen . findByText ( 'View in Library' ) ;
348+ await user . click ( viewLibBtn ) ;
349+
350+ expect ( mockedNavigate ) . toHaveBeenCalledWith (
351+ expect . stringContaining ( '/library/' ) ,
352+ ) ;
353+ } ) ;
354+ } ) ;
355+
356+ describe ( 'SectionSidebar menus' , ( ) => {
357+ const sectionId = 'block-v1:UNIX+UX1+2025_T3+type@chapter+block@sec1' ;
358+ const upstreamRef = 'lb:org:lib:chapter:sec-id' ;
359+
360+ const sectionData = {
361+ id : sectionId ,
362+ displayName : 'section name' ,
363+ category : 'chapter' ,
364+ hasChanges : false ,
365+ actions : { deletable : true , duplicable : true , draggable : false } ,
366+ upstreamInfo : null ,
367+ } ;
368+
369+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
370+ const renderSectionMenu = async ( data : any = sectionData ) => {
371+ selectedContainerState = {
372+ currentId : sectionId ,
373+ sectionId,
374+ } ;
375+ axiosMock . onGet ( getXBlockApiUrl ( sectionId ) ) . reply ( 200 , data ) ;
376+ renderComponent ( ) ;
377+ await screen . findByText ( data . displayName ) ;
378+ await screen . findByRole ( 'button' , { name : 'Item Menu' } ) ;
379+ } ;
380+
381+ it ( 'calls openDeleteModal when Delete is clicked in section menu' , async ( ) => {
382+ const user = userEvent . setup ( ) ;
383+ await renderSectionMenu ( ) ;
384+
385+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
386+ fireEvent . click ( menuToggle ) ;
387+
388+ const deleteBtn = await screen . findByText ( 'Delete' ) ;
389+ await user . click ( deleteBtn ) ;
390+
391+ expect ( openDeleteModal ) . toHaveBeenCalled ( ) ;
392+ } ) ;
393+
394+ it ( 'calls handleDuplicateSectionSubmit when Duplicate is clicked in section menu' , async ( ) => {
395+ const user = userEvent . setup ( ) ;
396+ await renderSectionMenu ( ) ;
397+
398+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
399+ fireEvent . click ( menuToggle ) ;
400+
401+ const duplicateBtn = await screen . findByText ( 'Duplicate' ) ;
402+ await user . click ( duplicateBtn ) ;
403+
404+ expect ( handleDuplicateSectionSubmit ) . toHaveBeenCalled ( ) ;
405+ } ) ;
406+
407+ it ( 'calls openUnlinkModal when Unlink is clicked in section menu' , async ( ) => {
408+ const user = userEvent . setup ( ) ;
409+ const sectionWithUpstream = {
410+ ...sectionData ,
411+ actions : { ...sectionData . actions , unlinkable : true } ,
412+ upstreamInfo : { upstreamRef } ,
413+ } ;
414+ await renderSectionMenu ( sectionWithUpstream ) ;
415+
416+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
417+ fireEvent . click ( menuToggle ) ;
418+
419+ const unlinkBtn = await screen . findByText ( 'Unlink from Library' ) ;
420+ await user . click ( unlinkBtn ) ;
421+
422+ expect ( openUnlinkModal ) . toHaveBeenCalledWith ( expect . objectContaining ( {
423+ value : sectionWithUpstream ,
424+ sectionId,
425+ } ) ) ;
426+ } ) ;
427+
428+ it ( 'navigates to library when View in Library is clicked in section menu' , async ( ) => {
429+ const user = userEvent . setup ( ) ;
430+ const sectionWithUpstream = {
431+ ...sectionData ,
432+ upstreamInfo : { upstreamRef } ,
433+ } ;
434+ await renderSectionMenu ( sectionWithUpstream ) ;
435+
436+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
437+ fireEvent . click ( menuToggle ) ;
438+
439+ const viewLibBtn = await screen . findByText ( 'View in Library' ) ;
440+ await user . click ( viewLibBtn ) ;
441+
442+ expect ( mockedNavigate ) . toHaveBeenCalledWith (
443+ expect . stringContaining ( '/library/' ) ,
444+ ) ;
445+ } ) ;
446+ } ) ;
447+ } ) ;
0 commit comments