diff --git a/frontend/src/test/views/HomeView.test.ts b/frontend/src/test/views/HomeView.test.ts index 1cf2df8..05d1ee6 100644 --- a/frontend/src/test/views/HomeView.test.ts +++ b/frontend/src/test/views/HomeView.test.ts @@ -1031,7 +1031,11 @@ describe('HomeView', () => { // Two-step confirm: clicking "Remove this frame" opens the in-sheet // confirmation panel; clicking "Yes, remove" inside it actually fires // the API + closes the sheet. - it('Remove this frame opens an in-sheet confirmation panel', async () => { + // The confirmation modal is rendered via , so it lives + // outside the wrapper's element tree. Tests have to query document + // directly. The Remove button itself stays in the sheet; opening the + // modal does NOT replace it. + it('Remove this frame opens a popup modal teleported to body', async () => { const devicesStore = useDevicesStore() devicesStore.devices = [makeDevice({ id: 5, name: 'Den' })] vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue() @@ -1042,19 +1046,17 @@ describe('HomeView', () => { await wrapper.findComponent({ name: 'FrameCard' }).vm.$emit('edit', 5) await flushPromises() - expect(wrapper.find('.home-view__remove-confirm').exists()).toBe(false) + expect(document.querySelector('.home-view__remove-modal')).toBeNull() await wrapper.find('.home-view__remove').trigger('click') await flushPromises() - expect(wrapper.find('.home-view__remove-confirm').exists()).toBe(true) - expect(wrapper.find('.home-view__remove-confirm-body').text()).toContain('selling or giving away') - // The original primary button is replaced by the confirm panel — no - // accidental "Remove" double-click can fire the API directly. - expect(wrapper.find('.home-view__remove').exists()).toBe(false) + const modal = document.querySelector('.home-view__remove-modal') + expect(modal).not.toBeNull() + expect(modal!.querySelector('.home-view__remove-confirm-body')!.textContent).toContain('selling or giving away') }) - it('Yes, remove inside the confirm panel calls the store and closes the sheet', async () => { + it('Yes, remove inside the modal calls the store and closes both', async () => { const devicesStore = useDevicesStore() devicesStore.devices = [makeDevice({ id: 5 })] vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue() @@ -1066,15 +1068,18 @@ describe('HomeView', () => { await flushPromises() await wrapper.find('.home-view__remove').trigger('click') await flushPromises() - await wrapper.find('.home-view__remove-confirm-btn').trigger('click') + + const confirmBtn = document.querySelector('.home-view__remove-confirm-btn') as HTMLButtonElement + confirmBtn.click() await flushPromises() expect(removeSpy).toHaveBeenCalledWith(5) const sheet = wrapper.findComponent({ name: 'BaseBottomSheet' }) expect(sheet.props('modelValue')).toBe(false) + expect(document.querySelector('.home-view__remove-modal')).toBeNull() }) - it('Cancel inside the confirm panel returns to the normal sheet without calling removeDevice', async () => { + it('Cancel inside the modal closes it without calling removeDevice', async () => { const devicesStore = useDevicesStore() devicesStore.devices = [makeDevice({ id: 5 })] vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue() @@ -1086,14 +1091,43 @@ describe('HomeView', () => { await flushPromises() await wrapper.find('.home-view__remove').trigger('click') await flushPromises() - await wrapper.find('.home-view__remove-cancel').trigger('click') + + const cancelBtn = document.querySelector('.home-view__remove-cancel') as HTMLButtonElement + cancelBtn.click() await flushPromises() expect(removeSpy).not.toHaveBeenCalled() - expect(wrapper.find('.home-view__remove-confirm').exists()).toBe(false) + expect(document.querySelector('.home-view__remove-modal')).toBeNull() + // The settings sheet stays open and the Remove button still shows. expect(wrapper.find('.home-view__remove').exists()).toBe(true) }) + // Backdrop-click closes the modal — common modal expectation. + it('clicking the backdrop closes the modal without removing', async () => { + const devicesStore = useDevicesStore() + devicesStore.devices = [makeDevice({ id: 5 })] + vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue() + const removeSpy = vi.spyOn(devicesStore, 'removeDevice').mockResolvedValue() + + const wrapper = mountView() + await flushPromises() + await wrapper.findComponent({ name: 'FrameCard' }).vm.$emit('edit', 5) + await flushPromises() + await wrapper.find('.home-view__remove').trigger('click') + await flushPromises() + + const backdrop = document.querySelector('.home-view__remove-modal') as HTMLElement + // @click.self on the backdrop fires the close — happy-dom needs a real + // dispatchEvent on the element itself with target=backdrop. + const ev = new MouseEvent('click', { bubbles: true }) + Object.defineProperty(ev, 'target', { value: backdrop, configurable: true }) + backdrop.dispatchEvent(ev) + await flushPromises() + + expect(removeSpy).not.toHaveBeenCalled() + expect(document.querySelector('.home-view__remove-modal')).toBeNull() + }) + // Update the existing propagation-note text test to assert the new // "disconnect power" hint that lets users force an immediate refresh. it('propagation note mentions disconnecting power as an immediate-refresh gesture', async () => { diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index c95a86a..48e89b1 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -226,39 +226,49 @@ {{ saving ? 'Saving…' : 'Save' }} - - -
-

- Remove this frame? -

-

- Use this if you’re selling or giving away the frame. It deletes - this frame from your account and unlinks it from your photos so the - next owner can claim it fresh. This can’t be undone. -

-
- - -
-
+ + + + +
+
+

+ Remove this frame? +

+

+ Use this if you’re selling or giving away the frame. It deletes + this frame from your account and unlinks it from your photos so + the next owner can claim it fresh. This can’t be undone. +

+
+ + +
+
+
+
+
+