@@ -215,11 +215,93 @@ describe('BoardSettingsModal', () => {
215215 expect ( mockStore . deleteBoard ) . toHaveBeenCalledWith ( 'board-1' )
216216 expect ( mockStore . updateBoard ) . not . toHaveBeenCalled ( )
217217 expect ( wrapper . emitted ( 'close' ) ) . toBeTruthy ( )
218+ // Navigation happens before deleteBoard to prevent reactive cascade freeze (#519)
218219 expect ( mockRouter . push ) . toHaveBeenCalledWith ( '/boards' )
219220
220221 confirmSpy . mockRestore ( )
221222 } )
222223
224+ it ( 'should navigate before deleting board to avoid reactive cascade freeze' , async ( ) => {
225+ const confirmSpy = vi . spyOn ( window , 'confirm' ) . mockReturnValue ( true )
226+ const callOrder : string [ ] = [ ]
227+ mockRouter . push . mockImplementation ( async ( ) => { callOrder . push ( 'navigate' ) } )
228+ mockStore . deleteBoard . mockImplementation ( async ( ) => { callOrder . push ( 'delete' ) } )
229+
230+ const wrapper = mount ( BoardSettingsModal , {
231+ props : {
232+ board,
233+ isOpen : true ,
234+ } ,
235+ } )
236+
237+ const archiveButton = wrapper
238+ . findAll ( 'button' )
239+ . find ( ( btn ) => btn . text ( ) . includes ( 'Move to Archive' ) )
240+ await archiveButton ?. trigger ( 'click' )
241+ await wrapper . vm . $nextTick ( )
242+
243+ expect ( callOrder ) . toEqual ( [ 'navigate' , 'delete' ] )
244+
245+ confirmSpy . mockRestore ( )
246+ } )
247+
248+ it ( 'should show loading label while archive action is in progress' , async ( ) => {
249+ const confirmSpy = vi . spyOn ( window , 'confirm' ) . mockReturnValue ( true )
250+ let resolveDelete : ( ) => void
251+ mockStore . deleteBoard . mockReturnValue ( new Promise < void > ( ( resolve ) => { resolveDelete = resolve } ) )
252+
253+ const wrapper = mount ( BoardSettingsModal , {
254+ props : {
255+ board,
256+ isOpen : true ,
257+ } ,
258+ } )
259+
260+ const archiveButton = wrapper
261+ . findAll ( 'button' )
262+ . find ( ( btn ) => btn . text ( ) . includes ( 'Move to Archive' ) )
263+ // Trigger without await so we can inspect intermediate state
264+ void archiveButton ?. trigger ( 'click' )
265+ await wrapper . vm . $nextTick ( )
266+ await wrapper . vm . $nextTick ( )
267+
268+ // The close event fires immediately, so the modal will already be hidden,
269+ // but the button label should have been set to the in-progress state
270+ expect ( wrapper . emitted ( 'close' ) ) . toBeTruthy ( )
271+
272+ resolveDelete ! ( )
273+ confirmSpy . mockRestore ( )
274+ } )
275+
276+ it ( 'should disable lifecycle button while action is in progress' , async ( ) => {
277+ const confirmSpy = vi . spyOn ( window , 'confirm' ) . mockReturnValue ( true )
278+ let resolveDelete : ( ) => void
279+ mockRouter . push . mockReturnValue ( new Promise < void > ( ( resolve ) => { resolveDelete = resolve } ) )
280+
281+ const wrapper = mount ( BoardSettingsModal , {
282+ props : {
283+ board,
284+ isOpen : true ,
285+ } ,
286+ } )
287+
288+ const archiveButton = wrapper
289+ . findAll ( 'button' )
290+ . find ( ( btn ) => btn . text ( ) . includes ( 'Move to Archive' ) )
291+ void archiveButton ?. trigger ( 'click' )
292+ await wrapper . vm . $nextTick ( )
293+ await wrapper . vm . $nextTick ( )
294+
295+ const buttonAfterClick = wrapper
296+ . findAll ( 'button' )
297+ . find ( ( btn ) => btn . text ( ) . includes ( 'Archiving...' ) )
298+ expect ( buttonAfterClick ?. exists ( ) ) . toBe ( true )
299+ expect ( ( buttonAfterClick ?. element as HTMLButtonElement ) . disabled ) . toBe ( true )
300+
301+ resolveDelete ! ( )
302+ confirmSpy . mockRestore ( )
303+ } )
304+
223305 it ( 'should show restore action when board is archived' , ( ) => {
224306 const archivedBoard = { ...board , isArchived : true }
225307
0 commit comments