fix(10): close UX-06/UX-10/UX-13 verification gaps — Escape modal close, rename/revoke toasts, StorageBrowser picker tests

This commit is contained in:
curo1305
2026-06-16 09:57:17 +02:00
parent 2605227fe0
commit 6b56763689
4 changed files with 106 additions and 6 deletions
@@ -49,6 +49,7 @@
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue'
import AppIcon from '../ui/AppIcon.vue'
const props = defineProps({
@@ -68,6 +69,12 @@ const props = defineProps({
const emit = defineEmits(['confirm', 'cancel'])
function onKeydown(e) {
if (e.key === 'Escape') handleCancel()
}
onMounted(() => window.addEventListener('keydown', onKeydown))
onUnmounted(() => window.removeEventListener('keydown', onKeydown))
function handleConfirm() {
emit('confirm')
if (props.onConfirm) props.onConfirm()
+13 -1
View File
@@ -113,9 +113,10 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import AppIcon from '../ui/AppIcon.vue'
import { useDocumentsStore } from '../../stores/documents.js'
import { useToastStore } from '../../stores/toast.js'
const props = defineProps({
doc: {
@@ -127,6 +128,7 @@ const props = defineProps({
const emit = defineEmits(['close', 'unshared'])
const docsStore = useDocumentsStore()
const toast = useToastStore()
const handle = ref('')
const permission = ref('view')
@@ -137,7 +139,12 @@ const shares = ref([])
const loadingShares = ref(false)
const updatingPermission = ref(new Set())
function onKeydown(e) {
if (e.key === 'Escape') emit('close')
}
onMounted(async () => {
window.addEventListener('keydown', onKeydown)
loadingShares.value = true
try {
const data = await docsStore.listShares(props.doc.id)
@@ -149,6 +156,10 @@ onMounted(async () => {
}
})
onUnmounted(() => {
window.removeEventListener('keydown', onKeydown)
})
async function submitShare() {
const trimmed = handle.value.trim()
if (!trimmed || submitting.value) return
@@ -201,6 +212,7 @@ async function handleRevoke(shareId) {
try {
await docsStore.revokeShare(shareId)
toast.show('Share revoked', 'success')
if (shares.value.length === 0) emit('unshared', props.doc.id)
} catch (e) {
// Re-add on failure
@@ -1,6 +1,7 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { nextTick } from 'vue'
import StorageBrowser from '../StorageBrowser.vue'
const globalStubs = {
@@ -55,8 +56,82 @@ describe('UX-02: StorageBrowser shows skeleton rows during loading', () => {
})
})
const SAMPLE_FILE = {
id: 'file-1',
original_name: 'doc.pdf',
size_bytes: 2048,
created_at: '2025-01-01T00:00:00Z',
status: 'ready',
topics: [],
is_shared: false,
}
const SAMPLE_FOLDER = { id: 'folder-1', name: 'Archive', doc_count: 0 }
describe('UX-13: StorageBrowser folder picker uses Teleport + getBoundingClientRect', () => {
it.todo('folder picker dropdown is teleported to body (escapes overflow container)')
it.todo('dropdown position reflects getBoundingClientRect of the trigger button')
it.todo('window scroll while open recalculates position')
beforeEach(() => {
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
top: 100, bottom: 140, left: 50, right: 250,
width: 200, height: 40, x: 50, y: 100,
})
Object.defineProperty(window, 'innerHeight', { value: 800, writable: true })
})
afterEach(() => {
vi.restoreAllMocks()
})
it('folder picker dropdown is teleported to body (escapes overflow container)', async () => {
const wrapper = mount(StorageBrowser, {
props: { mode: 'local', files: [SAMPLE_FILE], folders: [], rootFolders: [SAMPLE_FOLDER] },
global: { stubs: globalStubs },
attachTo: document.body,
})
const moveBtn = wrapper.find('button[title="Move to folder"]')
expect(moveBtn.exists()).toBe(true)
await moveBtn.trigger('click')
await nextTick()
const picker = document.body.querySelector('[data-test="folder-picker"]')
expect(picker).not.toBeNull()
wrapper.unmount()
})
it('dropdown position reflects getBoundingClientRect of the trigger button', async () => {
const wrapper = mount(StorageBrowser, {
props: { mode: 'local', files: [SAMPLE_FILE], folders: [], rootFolders: [SAMPLE_FOLDER] },
global: { stubs: globalStubs },
attachTo: document.body,
})
const moveBtn = wrapper.find('button[title="Move to folder"]')
await moveBtn.trigger('click')
await nextTick()
const picker = document.body.querySelector('[data-test="folder-picker"]')
expect(picker).not.toBeNull()
const style = picker.style
expect(style.top || style.bottom).toBeTruthy()
expect(style.left).toBeTruthy()
wrapper.unmount()
})
it('window scroll while open recalculates position', async () => {
const wrapper = mount(StorageBrowser, {
props: { mode: 'local', files: [SAMPLE_FILE], folders: [], rootFolders: [SAMPLE_FOLDER] },
global: { stubs: globalStubs },
attachTo: document.body,
})
const moveBtn = wrapper.find('button[title="Move to folder"]')
await moveBtn.trigger('click')
await nextTick()
const picker = document.body.querySelector('[data-test="folder-picker"]')
const initialTop = picker.style.top || picker.style.bottom
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
top: 300, bottom: 340, left: 50, right: 250,
width: 200, height: 40, x: 50, y: 300,
})
window.dispatchEvent(new Event('scroll', { bubbles: true }))
await nextTick()
const updatedTop = picker.style.top || picker.style.bottom
expect(updatedTop).not.toBe(initialTop)
wrapper.unmount()
})
})