@@ -13,7 +13,7 @@ jest.mock('@src/course-outline/outline-sidebar/OutlineSidebarContext', () => ({
1313 ...jest . requireActual ( '@src/course-outline/outline-sidebar/OutlineSidebarContext' ) . useOutlineSidebarContext ( ) ,
1414 selectedContainerState,
1515 clearSelection : jest . fn ( ) ,
16- setSelectedContainerState : jest . fn ( ) ,
16+ setSelectedContainerState : mockSetSelectedContainerState ,
1717 } ) ,
1818} ) ) ;
1919
@@ -32,6 +32,12 @@ const handleDuplicateSectionSubmit = jest.fn();
3232const handleDuplicateUnitSubmit = jest . fn ( ) ;
3333const handleDuplicateSubsectionSubmit = jest . fn ( ) ;
3434const mockedNavigate = jest . fn ( ) ;
35+ const updateUnitOrderByIndex = jest . fn ( ) ;
36+ const updateSubsectionOrderByIndex = jest . fn ( ) ;
37+ const updateSectionOrderByIndex = jest . fn ( ) ;
38+ const mockSetSelectedContainerState = jest . fn ( ) ;
39+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40+ let mockSections : any [ ] = [ ] ;
3541
3642jest . mock ( 'react-router-dom' , ( ) => ( {
3743 ...jest . requireActual ( 'react-router-dom' ) ,
@@ -47,12 +53,12 @@ jest.mock('@src/CourseAuthoringContext', () => ({
4753 openUnlinkModal,
4854 handleDuplicateUnitSubmit,
4955 getUnitUrl : jest . fn ( ) ,
50- sections : [ ] ,
51- updateUnitOrderByIndex : jest . fn ( ) ,
56+ sections : mockSections ,
57+ updateUnitOrderByIndex,
5258 handleDuplicateSectionSubmit,
53- updateSectionOrderByIndex : jest . fn ( ) ,
59+ updateSectionOrderByIndex,
5460 handleDuplicateSubsectionSubmit,
55- updateSubsectionOrderByIndex : jest . fn ( ) ,
61+ updateSubsectionOrderByIndex,
5662 } ) ,
5763} ) ) ;
5864
@@ -73,6 +79,11 @@ describe('InfoSidebar component', () => {
7379 handleDuplicateUnitSubmit . mockClear ( ) ;
7480 handleDuplicateSubsectionSubmit . mockClear ( ) ;
7581 mockedNavigate . mockClear ( ) ;
82+ updateUnitOrderByIndex . mockClear ( ) ;
83+ updateSubsectionOrderByIndex . mockClear ( ) ;
84+ updateSectionOrderByIndex . mockClear ( ) ;
85+ mockSetSelectedContainerState . mockClear ( ) ;
86+ mockSections = [ ] ;
7687 } ) ;
7788
7889 it ( 'renders InfoSidebar with course info if selectedContainerState is undefined' , async ( ) => {
@@ -258,6 +269,102 @@ describe('InfoSidebar component', () => {
258269 expect . stringContaining ( '/library/' ) ,
259270 ) ;
260271 } ) ;
272+
273+ it ( 'copies location ID to clipboard when Copy Location is clicked' , async ( ) => {
274+ const user = userEvent . setup ( ) ;
275+ const writeText = jest . fn ( ) . mockResolvedValue ( undefined ) ;
276+ Object . defineProperty ( navigator , 'clipboard' , {
277+ value : { writeText } ,
278+ writable : true ,
279+ configurable : true ,
280+ } ) ;
281+ await renderUnitMenu ( ) ;
282+
283+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
284+ fireEvent . click ( menuToggle ) ;
285+
286+ const copyLocationBtn = await screen . findByText ( 'Copy Location ID' ) ;
287+ await user . click ( copyLocationBtn ) ;
288+
289+ expect ( writeText ) . toHaveBeenCalledWith ( 'unit1' ) ;
290+ } ) ;
291+
292+ describe ( 'handleMove' , ( ) => {
293+ const seqId = 'block-v1:UNIX+UX1+2025_T3+type@sequential+block@seq1' ;
294+ const chId = 'block-v1:UNIX+UX1+2025_T3+type@chapter+block@ch1' ;
295+ const draggableUnitData = {
296+ ...unitData ,
297+ actions : { ...unitData . actions , draggable : true } ,
298+ } ;
299+
300+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
301+ const makeMovableUnit = ( id : string ) : any => ( {
302+ id,
303+ actions : { draggable : true } ,
304+ childInfo : { children : [ ] } ,
305+ } ) ;
306+
307+ const renderDraggableUnitMenu = async ( ) => {
308+ mockSections = [ {
309+ id : chId ,
310+ childInfo : {
311+ children : [ {
312+ id : seqId ,
313+ childInfo : {
314+ children : [
315+ makeMovableUnit ( 'unit0' ) ,
316+ makeMovableUnit ( unitId ) ,
317+ makeMovableUnit ( 'unit2' ) ,
318+ ] ,
319+ } ,
320+ } ] ,
321+ } ,
322+ } ] ;
323+ selectedContainerState = {
324+ currentId : unitId ,
325+ subsectionId : seqId ,
326+ sectionId : chId ,
327+ sectionIndex : 0 ,
328+ index : 1 ,
329+ } ;
330+ axiosMock . onGet ( getXBlockApiUrl ( unitId ) ) . reply ( 200 , draggableUnitData ) ;
331+ renderComponent ( ) ;
332+ await screen . findByText ( draggableUnitData . displayName ) ;
333+ await screen . findByRole ( 'button' , { name : 'Item Menu' } ) ;
334+ } ;
335+
336+ it ( 'calls updateUnitOrderByIndex and setSelectedContainerState when Move Up is clicked' , async ( ) => {
337+ const user = userEvent . setup ( ) ;
338+ await renderDraggableUnitMenu ( ) ;
339+
340+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
341+ fireEvent . click ( menuToggle ) ;
342+
343+ const moveUpBtn = await screen . findByText ( 'Move Up' ) ;
344+ await user . click ( moveUpBtn ) ;
345+
346+ expect ( updateUnitOrderByIndex ) . toHaveBeenCalled ( ) ;
347+ expect ( mockSetSelectedContainerState ) . toHaveBeenCalledWith (
348+ expect . objectContaining ( { index : 0 , subsectionId : seqId , sectionId : chId } ) ,
349+ ) ;
350+ } ) ;
351+
352+ it ( 'calls updateUnitOrderByIndex and setSelectedContainerState when Move Down is clicked' , async ( ) => {
353+ const user = userEvent . setup ( ) ;
354+ await renderDraggableUnitMenu ( ) ;
355+
356+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
357+ fireEvent . click ( menuToggle ) ;
358+
359+ const moveDownBtn = await screen . findByText ( 'Move Down' ) ;
360+ await user . click ( moveDownBtn ) ;
361+
362+ expect ( updateUnitOrderByIndex ) . toHaveBeenCalled ( ) ;
363+ expect ( mockSetSelectedContainerState ) . toHaveBeenCalledWith (
364+ expect . objectContaining ( { index : 2 , subsectionId : seqId , sectionId : chId } ) ,
365+ ) ;
366+ } ) ;
367+ } ) ;
261368 } ) ;
262369
263370 describe ( 'SubsectionSidebar menus' , ( ) => {
@@ -351,6 +458,79 @@ describe('InfoSidebar component', () => {
351458 expect . stringContaining ( '/library/' ) ,
352459 ) ;
353460 } ) ;
461+
462+ describe ( 'handleMove' , ( ) => {
463+ const chId = 'block-v1:UNIX+UX1+2025_T3+type@chapter+block@ch1' ;
464+ const draggableSubsectionData = {
465+ ...subsectionData ,
466+ actions : { ...subsectionData . actions , draggable : true } ,
467+ } ;
468+
469+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
470+ const makeMovableSubsection = ( id : string ) : any => ( {
471+ id,
472+ actions : { draggable : true , childAddable : true } ,
473+ childInfo : { children : [ ] } ,
474+ } ) ;
475+
476+ const renderDraggableSubsectionMenu = async ( ) => {
477+ mockSections = [ {
478+ id : chId ,
479+ actions : { childAddable : true } ,
480+ upstreamInfo : null ,
481+ childInfo : {
482+ children : [
483+ makeMovableSubsection ( 'sub0' ) ,
484+ makeMovableSubsection ( subsectionId ) ,
485+ makeMovableSubsection ( 'sub2' ) ,
486+ ] ,
487+ } ,
488+ } ] ;
489+ selectedContainerState = {
490+ currentId : subsectionId ,
491+ subsectionId,
492+ sectionId : chId ,
493+ sectionIndex : 0 ,
494+ index : 1 ,
495+ } ;
496+ axiosMock . onGet ( getXBlockApiUrl ( subsectionId ) ) . reply ( 200 , draggableSubsectionData ) ;
497+ renderComponent ( ) ;
498+ await screen . findByText ( draggableSubsectionData . displayName ) ;
499+ await screen . findByRole ( 'button' , { name : 'Item Menu' } ) ;
500+ } ;
501+
502+ it ( 'calls updateSubsectionOrderByIndex and setSelectedContainerState when Move Up is clicked' , async ( ) => {
503+ const user = userEvent . setup ( ) ;
504+ await renderDraggableSubsectionMenu ( ) ;
505+
506+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
507+ fireEvent . click ( menuToggle ) ;
508+
509+ const moveUpBtn = await screen . findByText ( 'Move Up' ) ;
510+ await user . click ( moveUpBtn ) ;
511+
512+ expect ( updateSubsectionOrderByIndex ) . toHaveBeenCalled ( ) ;
513+ expect ( mockSetSelectedContainerState ) . toHaveBeenCalledWith (
514+ expect . objectContaining ( { index : 0 , sectionId : chId , sectionIndex : 0 } ) ,
515+ ) ;
516+ } ) ;
517+
518+ it ( 'calls updateSubsectionOrderByIndex and setSelectedContainerState when Move Down is clicked' , async ( ) => {
519+ const user = userEvent . setup ( ) ;
520+ await renderDraggableSubsectionMenu ( ) ;
521+
522+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
523+ fireEvent . click ( menuToggle ) ;
524+
525+ const moveDownBtn = await screen . findByText ( 'Move Down' ) ;
526+ await user . click ( moveDownBtn ) ;
527+
528+ expect ( updateSubsectionOrderByIndex ) . toHaveBeenCalled ( ) ;
529+ expect ( mockSetSelectedContainerState ) . toHaveBeenCalledWith (
530+ expect . objectContaining ( { index : 2 , sectionId : chId , sectionIndex : 0 } ) ,
531+ ) ;
532+ } ) ;
533+ } ) ;
354534 } ) ;
355535
356536 describe ( 'SectionSidebar menus' , ( ) => {
@@ -443,5 +623,82 @@ describe('InfoSidebar component', () => {
443623 expect . stringContaining ( '/library/' ) ,
444624 ) ;
445625 } ) ;
626+
627+ describe ( 'handleMove' , ( ) => {
628+ const draggableSectionData = {
629+ ...sectionData ,
630+ actions : { ...sectionData . actions , draggable : true } ,
631+ } ;
632+
633+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
634+ const makeMovableSection = ( id : string ) : any => ( {
635+ id,
636+ actions : { draggable : true } ,
637+ childInfo : { children : [ ] } ,
638+ } ) ;
639+
640+ const renderDraggableSectionMenu = async ( ) => {
641+ mockSections = [
642+ makeMovableSection ( 'sec0' ) ,
643+ makeMovableSection ( sectionId ) ,
644+ makeMovableSection ( 'sec2' ) ,
645+ ] ;
646+ selectedContainerState = {
647+ currentId : sectionId ,
648+ sectionId,
649+ index : 1 ,
650+ } ;
651+ axiosMock . onGet ( getXBlockApiUrl ( sectionId ) ) . reply ( 200 , draggableSectionData ) ;
652+ renderComponent ( ) ;
653+ await screen . findByText ( draggableSectionData . displayName ) ;
654+ await screen . findByRole ( 'button' , { name : 'Item Menu' } ) ;
655+ } ;
656+
657+ it ( 'renders Move Up/Down as disabled when index is undefined' , async ( ) => {
658+ mockSections = [ makeMovableSection ( sectionId ) ] ;
659+ selectedContainerState = { currentId : sectionId , sectionId } ;
660+ axiosMock . onGet ( getXBlockApiUrl ( sectionId ) ) . reply ( 200 , draggableSectionData ) ;
661+ renderComponent ( ) ;
662+ await screen . findByText ( draggableSectionData . displayName ) ;
663+
664+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
665+ fireEvent . click ( menuToggle ) ;
666+
667+ expect ( await screen . findByText ( 'Move Up' ) ) . toBeInTheDocument ( ) ;
668+ expect ( screen . getByText ( 'Move Down' ) ) . toBeInTheDocument ( ) ;
669+ } ) ;
670+
671+ it ( 'calls updateSectionOrderByIndex and setSelectedContainerState when Move Up is clicked' , async ( ) => {
672+ const user = userEvent . setup ( ) ;
673+ await renderDraggableSectionMenu ( ) ;
674+
675+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
676+ fireEvent . click ( menuToggle ) ;
677+
678+ const moveUpBtn = await screen . findByText ( 'Move Up' ) ;
679+ await user . click ( moveUpBtn ) ;
680+
681+ expect ( updateSectionOrderByIndex ) . toHaveBeenCalledWith ( 1 , 0 ) ;
682+ expect ( mockSetSelectedContainerState ) . toHaveBeenCalledWith (
683+ expect . objectContaining ( { index : 0 } ) ,
684+ ) ;
685+ } ) ;
686+
687+ it ( 'calls updateSectionOrderByIndex and setSelectedContainerState when Move Down is clicked' , async ( ) => {
688+ const user = userEvent . setup ( ) ;
689+ await renderDraggableSectionMenu ( ) ;
690+
691+ const menuToggle = screen . getByRole ( 'button' , { name : 'Item Menu' } ) ;
692+ fireEvent . click ( menuToggle ) ;
693+
694+ const moveDownBtn = await screen . findByText ( 'Move Down' ) ;
695+ await user . click ( moveDownBtn ) ;
696+
697+ expect ( updateSectionOrderByIndex ) . toHaveBeenCalledWith ( 1 , 2 ) ;
698+ expect ( mockSetSelectedContainerState ) . toHaveBeenCalledWith (
699+ expect . objectContaining ( { index : 2 } ) ,
700+ ) ;
701+ } ) ;
702+ } ) ;
446703 } ) ;
447704} ) ;
0 commit comments