Skip to content

Commit fced42e

Browse files
committed
Add tests for archive navigation order and loading state
Verify that router.push fires before boardStore.deleteBoard to prevent reactive cascade freeze. Add tests for the disabled/loading button label during the archive action.
1 parent 3a6c7c1 commit fced42e

1 file changed

Lines changed: 82 additions & 0 deletions

File tree

frontend/taskdeck-web/src/tests/components/BoardSettingsModal.spec.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)