diff --git a/app/components/DefectForm.vue b/app/components/DefectForm.vue index db79d4e..be7caea 100644 --- a/app/components/DefectForm.vue +++ b/app/components/DefectForm.vue @@ -2,6 +2,9 @@ // File a defect against a board section (C3, F2). Driven by `sectionId`: when a // section is clicked the modal opens pre-targeted to it; pick a project, type // what's wrong, submit. Reporter and timestamp are added server-side. +// +// Uses the native (DaisyUI `modal`), so Escape, focus-trapping and the +// backdrop come for free; the parent-controlled `sectionId` drives open/close. import { sections } from '~/board/definition' interface Project { @@ -19,6 +22,7 @@ interface Defect { const props = defineProps<{ sectionId: string | null }>() const emit = defineEmits<{ close: []; filed: [Defect] }>() +const dialog = ref() const project = ref(null) const verbatim = ref('') const busy = ref(false) @@ -31,15 +35,22 @@ const canSubmit = computed( () => project.value !== null && verbatim.value.trim().length > 0, ) -// Each time a (new) section opens, start from a clean form. -watch( - () => props.sectionId, - () => { +// Mirror the parent-controlled section into the dialog's open state, resetting +// the form each time a (new) section opens. +function sync() { + const el = dialog.value + if (!el) return + if (props.sectionId !== null) { project.value = null verbatim.value = '' error.value = '' - }, -) + if (!el.open) el.showModal() + } else if (el.open) { + el.close() + } +} +onMounted(sync) +watch(() => props.sectionId, sync) async function submit() { if (!canSubmit.value || busy.value || !props.sectionId) return @@ -64,11 +75,15 @@ async function submit() { diff --git a/tests/defect-form.nuxt.spec.ts b/tests/defect-form.nuxt.spec.ts index 75b1d8f..7c204f7 100644 --- a/tests/defect-form.nuxt.spec.ts +++ b/tests/defect-form.nuxt.spec.ts @@ -22,23 +22,43 @@ beforeEach(() => { ) }) -async function pickProject(wrapper: Awaited>) { +type Wrapper = Awaited> +const dialogEl = (w: Wrapper) => w.find('dialog').element as HTMLDialogElement + +async function pickProject(wrapper: Wrapper) { await wrapper.find('[data-test="project-input"]').trigger('focus') await wrapper.find('[data-test="project-option"]').trigger('click') } describe('DefectForm', () => { - it('opens for the clicked section and shows its name', async () => { + it('opens the native dialog for the clicked section and shows its name', async () => { const wrapper = await mountSuspended(DefectForm, { props: { sectionId: 'macroplan' } }) await flushPromises() - expect(wrapper.find('dialog').classes()).toContain('modal-open') + expect(dialogEl(wrapper).open).toBe(true) expect(wrapper.text()).toContain('Macroplan') }) - it('is closed when no section is selected', async () => { + it('stays closed when no section is selected', async () => { const wrapper = await mountSuspended(DefectForm, { props: { sectionId: null } }) - expect(wrapper.find('dialog').classes()).not.toContain('modal-open') + await flushPromises() + expect(dialogEl(wrapper).open).toBe(false) + }) + + it('closes the dialog when the section is cleared', async () => { + const wrapper = await mountSuspended(DefectForm, { props: { sectionId: 'macroplan' } }) + await flushPromises() + await wrapper.setProps({ sectionId: null }) + await flushPromises() + expect(dialogEl(wrapper).open).toBe(false) + }) + + it('emits close when the dialog is dismissed (Escape/backdrop fire native close)', async () => { + const wrapper = await mountSuspended(DefectForm, { props: { sectionId: 'macroplan' } }) + await flushPromises() + + dialogEl(wrapper).dispatchEvent(new Event('close')) + expect(wrapper.emitted('close')).toBeTruthy() }) it('cannot submit until both a project and verbatim are provided', async () => { @@ -48,7 +68,6 @@ describe('DefectForm', () => { expect(wrapper.find('[data-test="submit"]').attributes('disabled')).toBeDefined() await pickProject(wrapper) - // project but no verbatim → still blocked expect(wrapper.find('[data-test="submit"]').attributes('disabled')).toBeDefined() await wrapper.find('[data-test="verbatim"]').setValue('builds are flaky') @@ -61,7 +80,7 @@ describe('DefectForm', () => { await pickProject(wrapper) await wrapper.find('[data-test="verbatim"]').setValue('builds are flaky') - await wrapper.find('form').trigger('submit') + await wrapper.find('[data-test="defect-form"]').trigger('submit') await vi.waitFor(() => expect(wrapper.emitted('filed')).toBeTruthy()) expect(filed).toEqual({