@@ -4,26 +4,36 @@ import userEvent from '@testing-library/user-event';
44import { useCourseItemData } from '@src/course-outline/data/apiHooks' ;
55import { ReleaseSection } from './ReleaseSection' ;
66
7- // Make useStateWithCallback synchronous so callbacks call onChange immediately
7+ // Make useStateWithCallback synchronous so callbacks call onChange immediately.
8+ // Also handles the { value, skipCallback } object form used by the external-sync useEffect.
89jest . mock ( '@src/hooks' , ( ) => ( {
910 useStateWithCallback : ( defaultValue : any , cb ?: any ) => {
1011 const { useState } = jest . requireActual ( 'react' ) ;
1112 const [ state , setState ] = useState ( defaultValue ) ;
1213 const wrappedSetState = ( val : any ) => {
13- const newVal = typeof val === 'function' ? val ( state ) : val ;
14+ let newVal ;
15+ let skip = false ;
16+ if ( typeof val === 'object' && val !== null && 'value' in val && 'skipCallback' in val ) {
17+ newVal = val . value ;
18+ skip = val . skipCallback ;
19+ } else {
20+ newVal = typeof val === 'function' ? val ( state ) : val ;
21+ }
1422 setState ( newVal ) ;
15- if ( cb ) { cb ( newVal ) ; }
23+ if ( cb && ! skip ) { cb ( newVal ) ; }
1624 } ;
1725 return [ state , wrappedSetState ] ;
1826 } ,
1927} ) ) ;
2028
21- // Mock DatepickerControl so we can trigger onChange easily
29+ // Mock DatepickerControl so we can trigger onChange and inspect the current value.
2230jest . mock ( '@src/generic/datepicker-control' , ( ) => ( {
2331 DATEPICKER_TYPES : { date : 'date' , time : 'time' } ,
24- DatepickerControl : ( { onChange, type } : any ) => (
32+ DatepickerControl : ( { onChange, type, value } : any ) => (
2533 < button
2634 type = "button"
35+ data-testid = { `datepicker-${ type } ` }
36+ data-value = { value }
2737 onClick = { ( ) => onChange ( type === 'date' ? '2025-12-31' : '12:00' ) }
2838 >
2939 { type }
@@ -60,4 +70,26 @@ describe('ReleaseSection', () => {
6070 await user . click ( screen . getByRole ( 'button' , { name : 'time' } ) ) ;
6171 expect ( onChange ) . toHaveBeenCalledWith ( '12:00' ) ;
6272 } ) ;
73+
74+ it ( 'syncs displayed value when itemData.start changes externally without calling onChange' , ( ) => {
75+ // Simulate initial state (e.g. subsection has a release date set)
76+ const initialDate = '2024-01-01T00:00:00Z' ;
77+ mockUseCourseItemData . mockReturnValue ( { data : { start : initialDate } } ) ;
78+ const onChange = jest . fn ( ) ;
79+ const { rerender } = render ( < ReleaseSection itemId = "i" onChange = { onChange } /> ) ;
80+
81+ expect ( screen . getByTestId ( 'datepicker-date' ) ) . toHaveAttribute ( 'data-value' , initialDate ) ;
82+
83+ // Simulate the kebab-menu configure modal saving a new release date:
84+ // the mutation fires, the section refetches, and setQueryData updates the
85+ // subsection cache — causing useCourseItemData to return the new date.
86+ const updatedDate = '2025-06-15T00:00:00Z' ;
87+ mockUseCourseItemData . mockReturnValue ( { data : { start : updatedDate } } ) ;
88+ rerender ( < ReleaseSection itemId = "i" onChange = { onChange } /> ) ;
89+
90+ // The sidebar must reflect the updated date...
91+ expect ( screen . getByTestId ( 'datepicker-date' ) ) . toHaveAttribute ( 'data-value' , updatedDate ) ;
92+ // ...but must NOT trigger onChange (which would fire a redundant mutation)
93+ expect ( onChange ) . not . toHaveBeenCalled ( ) ;
94+ } ) ;
6395} ) ;
0 commit comments