@@ -9,10 +9,17 @@ import PrintSchedules from '../index'
99
1010const PRINT_URL = 'http://localhost:3000/api/v1/reports/print'
1111
12- // Working mill/year comes from the mill/year context; drive it directly so the page under test does
13- // not depend on the provider or a mill-context fetch.
14- const ctx = vi . hoisted ( ( ) => ( { millId : 514 as number | null , year : 2021 as number | null } ) )
15- vi . mock ( '@/context/millYear/useMillYear' , ( ) => ( { default : ( ) => ( { ...ctx } ) } ) )
12+ // Drive the mill/year context guard directly so the page under test does not depend on the provider or
13+ // a mill-context fetch — and so `isCurrent` / `contextMissing` can be controlled per test.
14+ const guard = vi . hoisted ( ( ) => ( {
15+ millId : 514 as number | null ,
16+ year : 2021 as number | null ,
17+ contextMissing : false ,
18+ isCurrent : ( ) => true ,
19+ } ) )
20+ vi . mock ( '@/hooks/useScheduleContextGuard' , ( ) => ( {
21+ useScheduleContextGuard : ( ) => guard ,
22+ } ) )
1623
1724// The tombstone self-sources the working context (and would fetch /v1/mill-context); stub it — this
1825// suite is about the selection form + download, not the header.
@@ -27,8 +34,10 @@ vi.mock('@/utils/download', async (importOriginal) => ({
2734} ) )
2835
2936beforeEach ( ( ) => {
30- ctx . millId = 514
31- ctx . year = 2021
37+ guard . millId = 514
38+ guard . year = 2021
39+ guard . contextMissing = false
40+ guard . isCurrent = ( ) => true
3241 vi . mocked ( triggerDownload ) . mockReset ( )
3342} )
3443
@@ -37,21 +46,21 @@ afterEach(() => {
3746} )
3847
3948describe ( 'PrintSchedules' , ( ) => {
40- it ( 'renders schedules/options with the deferred ones disabled ("coming soon" ), Generate disabled' , ( ) => {
49+ it ( 'renders schedules/options ( deferred disabled, Schedule information default-checked ), Generate disabled' , ( ) => {
4150 render ( < PrintSchedules /> )
4251 expect ( screen . getByRole ( 'checkbox' , { name : 'Select all schedules' } ) ) . toBeInTheDocument ( )
43- // Renderable schedules + content options are enabled.
4452 expect ( screen . getByRole ( 'checkbox' , { name : 'Schedule 7A' } ) ) . toBeEnabled ( )
4553 expect ( screen . getByRole ( 'checkbox' , { name : 'Schedule 11' } ) ) . toBeEnabled ( )
46- expect ( screen . getByRole ( 'checkbox' , { name : 'Comments' } ) ) . toBeEnabled ( )
54+ // S06 default: Schedule Information is pre-checked.
55+ expect ( screen . getByRole ( 'checkbox' , { name : 'Schedule information' } ) ) . toBeChecked ( )
4756 // Deferred schedules + the Mill info report are shown but disabled with a coming-soon note.
4857 expect ( screen . getByRole ( 'checkbox' , { name : 'Schedule 1 (coming soon)' } ) ) . toBeDisabled ( )
49- expect ( screen . getByRole ( 'checkbox' , { name : 'Schedule 8 (coming soon)' } ) ) . toBeDisabled ( )
5058 expect (
5159 screen . getByRole ( 'checkbox' , { name : 'Mill information report (coming soon)' } ) ,
5260 ) . toBeDisabled ( )
53- // Nothing selected yet → Generate is disabled.
61+ // No schedule selected yet → Generate is disabled; Clear is available .
5462 expect ( screen . getByRole ( 'button' , { name : / G e n e r a t e P D F / } ) ) . toBeDisabled ( )
63+ expect ( screen . getByRole ( 'button' , { name : 'Clear' } ) ) . toBeInTheDocument ( )
5564 } )
5665
5766 it ( '"Select all schedules" checks only the renderable schedules' , async ( ) => {
@@ -60,7 +69,6 @@ describe('PrintSchedules', () => {
6069 for ( const label of [ 'Schedule 5' , 'Schedule 7B' , 'Schedule 9' , 'Schedule 11' ] ) {
6170 expect ( screen . getByRole ( 'checkbox' , { name : label } ) ) . toBeChecked ( )
6271 }
63- // A deferred schedule stays disabled and unchecked.
6472 expect ( screen . getByRole ( 'checkbox' , { name : 'Schedule 1 (coming soon)' } ) ) . not . toBeChecked ( )
6573 } )
6674
@@ -82,29 +90,89 @@ describe('PrintSchedules', () => {
8290
8391 await waitFor ( ( ) => expect ( vi . mocked ( triggerDownload ) ) . toHaveBeenCalledTimes ( 1 ) )
8492 expect ( screen . getByText ( / g e n e r a t e d a n d d o w n l o a d e d / i) ) . toBeInTheDocument ( )
85- expect ( sentBody ) . toMatchObject ( { schedule5 : true , printComments : true , allSchedules : false } )
93+ expect ( sentBody ) . toMatchObject ( {
94+ schedule5 : true ,
95+ printComments : true ,
96+ printScheduleInformation : true ,
97+ allSchedules : false ,
98+ } )
8699 expect ( vi . mocked ( triggerDownload ) . mock . calls [ 0 ] [ 1 ] ) . toBe ( 'schedules_print.pdf' )
87100 } )
88101
89- it ( 'shows the server error detail when the selection is rejected ' , async ( ) => {
102+ it ( 'passes through the verbatim server detail for a 400 rejection ' , async ( ) => {
90103 server . use (
91104 http . post ( PRINT_URL , ( ) =>
92105 HttpResponse . json ( { detail : 'Select at least one print option.' } , { status : 400 } ) ,
93106 ) ,
94107 )
95108 render ( < PrintSchedules /> )
96109
110+ // Schedule Information is on by default, so ticking a schedule is enough to enable Generate.
97111 await userEvent . click ( screen . getByRole ( 'checkbox' , { name : 'Schedule 5' } ) )
98- await userEvent . click ( screen . getByRole ( 'checkbox' , { name : 'Schedule information' } ) )
99112 await userEvent . click ( screen . getByRole ( 'button' , { name : / G e n e r a t e P D F / } ) )
100113
101114 expect ( await screen . findByText ( 'Select at least one print option.' ) ) . toBeInTheDocument ( )
102115 expect ( vi . mocked ( triggerDownload ) ) . not . toHaveBeenCalled ( )
103116 } )
104117
118+ it ( 'shows a friendly message (not "Schedule not found") when the selection has no data (404)' , async ( ) => {
119+ server . use (
120+ http . post ( PRINT_URL , ( ) =>
121+ HttpResponse . json ( { detail : 'Schedule not found.' } , { status : 404 } ) ,
122+ ) ,
123+ )
124+ render ( < PrintSchedules /> )
125+
126+ await userEvent . click ( screen . getByRole ( 'checkbox' , { name : 'Schedule 5' } ) )
127+ await userEvent . click ( screen . getByRole ( 'button' , { name : / G e n e r a t e P D F / } ) )
128+
129+ expect (
130+ await screen . findByText ( 'No data to print for the selected schedules.' ) ,
131+ ) . toBeInTheDocument ( )
132+ expect ( screen . queryByText ( 'Schedule not found.' ) ) . toBeNull ( )
133+ expect ( vi . mocked ( triggerDownload ) ) . not . toHaveBeenCalled ( )
134+ } )
135+
136+ it ( 'ignores a stale response when the mill/year context changed mid-render (no download)' , async ( ) => {
137+ let handled = false
138+ server . use (
139+ http . post ( PRINT_URL , ( ) => {
140+ handled = true
141+ return new HttpResponse ( new Blob ( [ '%PDF-1.4 mock' ] ) , {
142+ headers : { 'Content-Type' : 'application/pdf' } ,
143+ } )
144+ } ) ,
145+ )
146+ // The dispatch-time guard reports the context is no longer current when the response comes back.
147+ guard . isCurrent = ( ) => false
148+ render ( < PrintSchedules /> )
149+
150+ await userEvent . click ( screen . getByRole ( 'checkbox' , { name : 'Schedule 5' } ) )
151+ await userEvent . click ( screen . getByRole ( 'button' , { name : / G e n e r a t e P D F / } ) )
152+
153+ await waitFor ( ( ) => expect ( handled ) . toBe ( true ) )
154+ // The stale PDF must not download, and no "done" banner appears under the new context.
155+ expect ( vi . mocked ( triggerDownload ) ) . not . toHaveBeenCalled ( )
156+ expect ( screen . queryByText ( / g e n e r a t e d a n d d o w n l o a d e d / i) ) . toBeNull ( )
157+ } )
158+
159+ it ( 'Clear resets to the default (schedules cleared, Schedule Information re-checked)' , async ( ) => {
160+ render ( < PrintSchedules /> )
161+
162+ await userEvent . click ( screen . getByRole ( 'checkbox' , { name : 'Schedule 5' } ) )
163+ await userEvent . click ( screen . getByRole ( 'checkbox' , { name : 'Comments' } ) )
164+ await userEvent . click ( screen . getByRole ( 'checkbox' , { name : 'Schedule information' } ) ) // uncheck default
165+ expect ( screen . getByRole ( 'checkbox' , { name : 'Schedule information' } ) ) . not . toBeChecked ( )
166+
167+ await userEvent . click ( screen . getByRole ( 'button' , { name : 'Clear' } ) )
168+
169+ expect ( screen . getByRole ( 'checkbox' , { name : 'Schedule 5' } ) ) . not . toBeChecked ( )
170+ expect ( screen . getByRole ( 'checkbox' , { name : 'Comments' } ) ) . not . toBeChecked ( )
171+ expect ( screen . getByRole ( 'checkbox' , { name : 'Schedule information' } ) ) . toBeChecked ( )
172+ } )
173+
105174 it ( 'gates on a missing mill/year context instead of showing the form' , ( ) => {
106- ctx . millId = null
107- ctx . year = null
175+ guard . contextMissing = true
108176 render ( < PrintSchedules /> )
109177 expect ( screen . getByText ( 'Select a mill and reporting year' ) ) . toBeInTheDocument ( )
110178 expect ( screen . queryByRole ( 'button' , { name : / G e n e r a t e P D F / } ) ) . toBeNull ( )
0 commit comments