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() })