From 71f55b84a9af7a1b8354685fdbae2d155537b742 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Mon, 15 Jun 2026 20:43:58 +0200 Subject: [PATCH] test(10-10): promote OsDragOverlay stubs to 8 RED failing tests - Replace 7 it.todo stubs with 8 real tests covering UX-09 contract - Tests dispatch DragEvent/Event with synthetic dataTransfer on window - Covers: hidden by default, Files type guard, depth counter, nested enter/leave, drop emit, drop reset, z-[9998] class on overlay element - All 8 tests RED (OsDragOverlay.vue does not yet exist) --- .../layout/__tests__/OsDragOverlay.test.js | 163 +++++++++++++++++- 1 file changed, 155 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/layout/__tests__/OsDragOverlay.test.js b/frontend/src/components/layout/__tests__/OsDragOverlay.test.js index 904f0fc..bf2aad1 100644 --- a/frontend/src/components/layout/__tests__/OsDragOverlay.test.js +++ b/frontend/src/components/layout/__tests__/OsDragOverlay.test.js @@ -1,11 +1,158 @@ -import { describe, it } from 'vitest' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { mount } from '@vue/test-utils' +import OsDragOverlay from '../OsDragOverlay.vue' + +function makeDragEvent(type, types = ['Files'], files = []) { + const event = new Event(type, { bubbles: true, cancelable: true }) + Object.defineProperty(event, 'dataTransfer', { + value: { types, files, preventDefault: vi.fn() }, + writable: false, + }) + event.preventDefault = vi.fn() + return event +} describe('UX-09: OsDragOverlay shows when OS files are dragged over the window', () => { - it.todo('overlay hidden by default (dragDepth=0)') - it.todo('dragenter with dataTransfer.types including "Files" shows overlay') - it.todo('dragenter without "Files" type is ignored (in-app element drag)') - it.todo('dragleave decrements depth; overlay hides when depth reaches 0') - it.todo('drop event emits files-dropped with the file list') - it.todo('drop resets dragDepth to 0 and hides overlay') - it.todo('overlay is teleported to body with z-[9998] (below toast z-[9999])') + it('overlay hidden by default (dragDepth=0)', () => { + const w = mount(OsDragOverlay, { + attachTo: document.body, + global: { stubs: { Teleport: true, AppIcon: true } }, + }) + expect(w.vm.showOverlay).toBe(false) + expect(w.vm.dragDepth).toBe(0) + w.unmount() + }) + + it('dragenter with dataTransfer.types including "Files" shows overlay (dragDepth=1)', async () => { + const w = mount(OsDragOverlay, { + attachTo: document.body, + global: { stubs: { Teleport: true, AppIcon: true } }, + }) + const event = makeDragEvent('dragenter', ['Files']) + window.dispatchEvent(event) + await w.vm.$nextTick() + expect(w.vm.showOverlay).toBe(true) + expect(w.vm.dragDepth).toBe(1) + w.unmount() + }) + + it('dragenter without "Files" type is ignored (in-app element drag)', async () => { + const w = mount(OsDragOverlay, { + attachTo: document.body, + global: { stubs: { Teleport: true, AppIcon: true } }, + }) + const event = makeDragEvent('dragenter', ['text/plain']) + window.dispatchEvent(event) + await w.vm.$nextTick() + expect(w.vm.showOverlay).toBe(false) + expect(w.vm.dragDepth).toBe(0) + w.unmount() + }) + + it('dragleave decrements depth; overlay hides when depth reaches 0', async () => { + const w = mount(OsDragOverlay, { + attachTo: document.body, + global: { stubs: { Teleport: true, AppIcon: true } }, + }) + window.dispatchEvent(makeDragEvent('dragenter', ['Files'])) + await w.vm.$nextTick() + expect(w.vm.dragDepth).toBe(1) + expect(w.vm.showOverlay).toBe(true) + + window.dispatchEvent(new Event('dragleave')) + await w.vm.$nextTick() + expect(w.vm.dragDepth).toBe(0) + expect(w.vm.showOverlay).toBe(false) + w.unmount() + }) + + it('nested dragenter+dragleave maintains overlay until depth=0', async () => { + const w = mount(OsDragOverlay, { + attachTo: document.body, + global: { stubs: { Teleport: true, AppIcon: true } }, + }) + window.dispatchEvent(makeDragEvent('dragenter', ['Files'])) + window.dispatchEvent(makeDragEvent('dragenter', ['Files'])) + await w.vm.$nextTick() + expect(w.vm.dragDepth).toBe(2) + expect(w.vm.showOverlay).toBe(true) + + window.dispatchEvent(new Event('dragleave')) + await w.vm.$nextTick() + expect(w.vm.dragDepth).toBe(1) + expect(w.vm.showOverlay).toBe(true) + + window.dispatchEvent(new Event('dragleave')) + await w.vm.$nextTick() + expect(w.vm.dragDepth).toBe(0) + expect(w.vm.showOverlay).toBe(false) + w.unmount() + }) + + it('drop event emits files-dropped with the file list', async () => { + const w = mount(OsDragOverlay, { + attachTo: document.body, + global: { stubs: { Teleport: true, AppIcon: true } }, + }) + window.dispatchEvent(makeDragEvent('dragenter', ['Files'])) + await w.vm.$nextTick() + + const file = new File(['x'], 'a.txt', { type: 'text/plain' }) + const dropEvent = new Event('drop', { bubbles: true, cancelable: true }) + Object.defineProperty(dropEvent, 'dataTransfer', { + value: { files: [file] }, + writable: false, + }) + dropEvent.preventDefault = vi.fn() + window.dispatchEvent(dropEvent) + await w.vm.$nextTick() + + const emitted = w.emitted('files-dropped') + expect(emitted).toBeTruthy() + expect(emitted[0][0]).toHaveLength(1) + expect(emitted[0][0][0]).toBeInstanceOf(File) + w.unmount() + }) + + it('drop resets dragDepth to 0 and hides overlay', async () => { + const w = mount(OsDragOverlay, { + attachTo: document.body, + global: { stubs: { Teleport: true, AppIcon: true } }, + }) + window.dispatchEvent(makeDragEvent('dragenter', ['Files'])) + window.dispatchEvent(makeDragEvent('dragenter', ['Files'])) + window.dispatchEvent(makeDragEvent('dragenter', ['Files'])) + await w.vm.$nextTick() + expect(w.vm.dragDepth).toBe(3) + + const dropEvent = new Event('drop', { bubbles: true, cancelable: true }) + Object.defineProperty(dropEvent, 'dataTransfer', { + value: { files: [] }, + writable: false, + }) + dropEvent.preventDefault = vi.fn() + window.dispatchEvent(dropEvent) + await w.vm.$nextTick() + + expect(w.vm.dragDepth).toBe(0) + expect(w.vm.showOverlay).toBe(false) + + window.dispatchEvent(new Event('dragleave')) + await w.vm.$nextTick() + expect(w.vm.dragDepth).toBe(0) + w.unmount() + }) + + it('overlay is teleported to body with z-[9998] (below toast z-[9999])', async () => { + const w = mount(OsDragOverlay, { + attachTo: document.body, + }) + window.dispatchEvent(makeDragEvent('dragenter', ['Files'])) + await w.vm.$nextTick() + + const overlay = document.querySelector('[data-test="os-drag-overlay"]') + expect(overlay).toBeTruthy() + expect(overlay.classList.contains('z-[9998]')).toBe(true) + w.unmount() + }) })