@@ -4,8 +4,14 @@ import { Assignment } from "@/types/assignment";
44import { notify } from "@components/ui/notify" ;
55
66import {
7+ BulkUpdateAssignmentsTrigger ,
8+ getBulkEnforceDueToastCopy ,
9+ getBulkUpdateErrorToastCopy ,
10+ getBulkVisibilityToastCopy ,
711 getEnforceDueToastCopy ,
812 getVisibilityToastCopy ,
13+ saveBulkEnforceDue ,
14+ saveBulkVisibility ,
915 saveEnforceDue ,
1016 saveVisibility ,
1117 UpdateAssignmentTrigger
@@ -140,5 +146,137 @@ describe("getVisibilityToastCopy", () => {
140146 } )
141147 ) . toBe ( "Assignment visibility is scheduled" ) ;
142148 } ) ;
143- } ) ;
144149
150+ const makeBulkTrigger = ( result : {
151+ succeeded : number ;
152+ failed : number ;
153+ } ) : BulkUpdateAssignmentsTrigger => vi . fn ( ( ) => ( { unwrap : ( ) => Promise . resolve ( result ) } ) ) ;
154+
155+ const makeRejectingBulkTrigger = ( ) : BulkUpdateAssignmentsTrigger =>
156+ vi . fn ( ( ) => ( { unwrap : ( ) => Promise . reject ( new Error ( "bulk update failed" ) ) } ) ) ;
157+
158+ describe ( "saveBulkVisibility" , ( ) => {
159+ beforeEach ( ( ) => {
160+ vi . clearAllMocks ( ) ;
161+ } ) ;
162+
163+ it ( "merges the update into every assignment and toasts a bulk success" , async ( ) => {
164+ const trigger = makeBulkTrigger ( { succeeded : 2 , failed : 0 } ) ;
165+ const assignments = [
166+ makeAssignment ( { id : 1 } ) ,
167+ makeAssignment ( { id : 2 , name : "Homework 2" } )
168+ ] ;
169+ const update = { visible : true , visible_on : null , hidden_on : null } ;
170+
171+ await saveBulkVisibility ( trigger , assignments , update ) ;
172+
173+ expect ( trigger ) . toHaveBeenCalledWith (
174+ assignments . map ( ( assignment ) => ( { ...assignment , ...update } ) )
175+ ) ;
176+ expect ( notify . success ) . toHaveBeenCalledWith ( "2 assignments now visible" ) ;
177+ expect ( notify . error ) . not . toHaveBeenCalled ( ) ;
178+ } ) ;
179+
180+ it ( "toasts both success and error copies on a partial failure" , async ( ) => {
181+ const trigger = makeBulkTrigger ( { succeeded : 1 , failed : 2 } ) ;
182+
183+ await saveBulkVisibility ( trigger , [ makeAssignment ( ) , makeAssignment ( ) , makeAssignment ( ) ] , {
184+ visible : false ,
185+ visible_on : null ,
186+ hidden_on : null
187+ } ) ;
188+
189+ expect ( notify . success ) . toHaveBeenCalledWith ( "1 assignment now hidden" ) ;
190+ expect ( notify . error ) . toHaveBeenCalledWith ( "Couldn't update 2 assignments. Try again." ) ;
191+ } ) ;
192+
193+ it ( "only toasts the error copy when the mutation rejects" , async ( ) => {
194+ await saveBulkVisibility ( makeRejectingBulkTrigger ( ) , [ makeAssignment ( ) ] , {
195+ visible : true ,
196+ visible_on : null ,
197+ hidden_on : null
198+ } ) ;
199+
200+ expect ( notify . success ) . not . toHaveBeenCalled ( ) ;
201+ expect ( notify . error ) . toHaveBeenCalledWith ( "Couldn't update 1 assignment. Try again." ) ;
202+ } ) ;
203+
204+ it ( "does nothing for an empty selection" , async ( ) => {
205+ const trigger = makeBulkTrigger ( { succeeded : 0 , failed : 0 } ) ;
206+
207+ await saveBulkVisibility ( trigger , [ ] , { visible : true , visible_on : null , hidden_on : null } ) ;
208+
209+ expect ( trigger ) . not . toHaveBeenCalled ( ) ;
210+ expect ( notify . success ) . not . toHaveBeenCalled ( ) ;
211+ expect ( notify . error ) . not . toHaveBeenCalled ( ) ;
212+ } ) ;
213+ } ) ;
214+
215+ describe ( "saveBulkEnforceDue" , ( ) => {
216+ beforeEach ( ( ) => {
217+ vi . clearAllMocks ( ) ;
218+ } ) ;
219+
220+ it ( "applies the enforce_due patch to every assignment and toasts success" , async ( ) => {
221+ const trigger = makeBulkTrigger ( { succeeded : 2 , failed : 0 } ) ;
222+ const assignments = [ makeAssignment ( { id : 1 } ) , makeAssignment ( { id : 2 } ) ] ;
223+
224+ await saveBulkEnforceDue ( trigger , assignments , true ) ;
225+
226+ expect ( trigger ) . toHaveBeenCalledWith (
227+ assignments . map ( ( assignment ) => ( { ...assignment , enforce_due : true } ) )
228+ ) ;
229+ expect ( notify . success ) . toHaveBeenCalledWith ( "Late submissions not allowed for 2 assignments" ) ;
230+ } ) ;
231+
232+ it ( "toasts the allowed copy when enforce_due is turned off" , async ( ) => {
233+ await saveBulkEnforceDue (
234+ makeBulkTrigger ( { succeeded : 1 , failed : 0 } ) ,
235+ [ makeAssignment ( ) ] ,
236+ false
237+ ) ;
238+
239+ expect ( notify . success ) . toHaveBeenCalledWith ( "Late submissions allowed for 1 assignment" ) ;
240+ } ) ;
241+
242+ it ( "does nothing for an empty selection" , async ( ) => {
243+ const trigger = makeBulkTrigger ( { succeeded : 0 , failed : 0 } ) ;
244+
245+ await saveBulkEnforceDue ( trigger , [ ] , true ) ;
246+
247+ expect ( trigger ) . not . toHaveBeenCalled ( ) ;
248+ } ) ;
249+ } ) ;
250+
251+ describe ( "bulk toast copy helpers" , ( ) => {
252+ it ( "describes bulk visibility per mode with singular and plural subjects" , ( ) => {
253+ expect (
254+ getBulkVisibilityToastCopy ( 1 , { visible : true , visible_on : null , hidden_on : null } )
255+ ) . toBe ( "1 assignment now visible" ) ;
256+ expect (
257+ getBulkVisibilityToastCopy ( 3 , { visible : false , visible_on : null , hidden_on : null } )
258+ ) . toBe ( "3 assignments now hidden" ) ;
259+ expect (
260+ getBulkVisibilityToastCopy ( 2 , {
261+ visible : false ,
262+ visible_on : "2026-06-20T00:00:00" ,
263+ hidden_on : null
264+ } )
265+ ) . toBe ( "Visibility scheduled for 2 assignments" ) ;
266+ } ) ;
267+
268+ it ( "describes bulk enforce_due copy" , ( ) => {
269+ expect ( getBulkEnforceDueToastCopy ( 2 , true ) ) . toBe (
270+ "Late submissions not allowed for 2 assignments"
271+ ) ;
272+ expect ( getBulkEnforceDueToastCopy ( 1 , false ) ) . toBe (
273+ "Late submissions allowed for 1 assignment"
274+ ) ;
275+ } ) ;
276+
277+ it ( "phrases the bulk error as couldn't-verb plus a fix" , ( ) => {
278+ expect ( getBulkUpdateErrorToastCopy ( 1 ) ) . toBe ( "Couldn't update 1 assignment. Try again." ) ;
279+ expect ( getBulkUpdateErrorToastCopy ( 4 ) ) . toBe ( "Couldn't update 4 assignments. Try again." ) ;
280+ } ) ;
281+ } ) ;
282+ } ) ;
0 commit comments