From 113b77e92f74c6acd114934f336be8afe102897d Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 11 Apr 2026 20:41:48 +0200 Subject: [PATCH] fix(modal): skip showModal if enabled flipped false before mount Re-check props.enabled inside the dialogRef watcher. The watcher fires once Vue mounts the , but the caller may have flipped enabled back to false between the openDialog() call and the mount flush. In that case the prop state is disabled and we must not open the dialog. Addresses augmentcode review on #2604. --- frontend/src/components/misc/Modal.test.ts | 28 ++++++++++++++++++++++ frontend/src/components/misc/Modal.vue | 5 +++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/misc/Modal.test.ts b/frontend/src/components/misc/Modal.test.ts index b55586fbb..d8d134f85 100644 --- a/frontend/src/components/misc/Modal.test.ts +++ b/frontend/src/components/misc/Modal.test.ts @@ -154,6 +154,34 @@ describe('Modal.vue — open race condition (#2590)', () => { wrapper.unmount() }) + it('does not open the dialog if enabled flips back to false before mount', async () => { + // Regression guard: the dialogRef watcher fires once the + // element mounts. If props.enabled has flipped back to false by the + // time the mount happens, the watcher must not call showModal(). + const wrapper = mount(Modal, { + attachTo: document.body, + props: {enabled: false}, + slots: {default: '

hi

'}, + }) + + // Flip enabled true then false within the same tick, before the mount + // flush can complete. + wrapper.setProps({enabled: true}) + wrapper.setProps({enabled: false}) + await flushPromises() + await nextTick() + await new Promise(resolve => setTimeout(resolve, 200)) + await flushPromises() + await nextTick() + + // showModal must not have been called — the final prop state is + // disabled. + expect(showModalSpy).not.toHaveBeenCalled() + expect(document.querySelector('dialog.modal-dialog')).toBeNull() + + wrapper.unmount() + }) + it('clears data-closing when re-opened mid-close transition', async () => { // Regression guard: if the user toggles enabled back to true while the // 150ms close transition is still in flight, the is still diff --git a/frontend/src/components/misc/Modal.vue b/frontend/src/components/misc/Modal.vue index a875965e9..bedd49efb 100644 --- a/frontend/src/components/misc/Modal.vue +++ b/frontend/src/components/misc/Modal.vue @@ -147,9 +147,12 @@ watch( // Actually call showModal() the moment the element is mounted. // `dialogRef` is populated by Vue during the render flush after // `showDialog.value = true`, so this fires deterministically, no matter -// how many flushes the renderer needs (see #2590). +// how many flushes the renderer needs (see #2590). We re-check +// `props.enabled` here because the prop can flip back to `false` between +// `openDialog()` and the mount flush, in which case we must not open. watch(dialogRef, (dialog) => { if (!dialog) return + if (!props.enabled) return delete dialog.dataset.closing dialog.showModal() })