import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { setActivePinia, createPinia } from 'pinia' import { createRouter, createMemoryHistory } from 'vue-router' import FileManagerView from '../views/FileManagerView.vue' vi.mock('../api/client.js', () => ({ listFolders: vi.fn().mockResolvedValue({ items: [] }), listDocuments: vi.fn().mockResolvedValue({ items: [], total: 0 }), getFolder: vi.fn().mockResolvedValue({ id: 'f1', name: 'Test', breadcrumb: [] }), createFolder: vi.fn(), renameFolder: vi.fn(), deleteFolder: vi.fn(), moveDocument: vi.fn(), deleteDocument: vi.fn().mockResolvedValue(null), getSharedWithMe: vi.fn().mockResolvedValue([]), listTopics: vi.fn().mockResolvedValue([]), getMyQuota: vi.fn().mockResolvedValue({ used_bytes: 0, limit_bytes: 104857600 }), })) vi.mock('../stores/auth.js', () => ({ useAuthStore: () => ({ user: { email: 'test@example.com', role: 'user' }, accessToken: 'fake-token', fetchQuota: vi.fn().mockResolvedValue(null), }), })) vi.mock('../stores/topics.js', () => ({ useTopicsStore: () => ({ topics: [], loading: false, fetchTopics: vi.fn().mockResolvedValue(null), }), })) vi.mock('../components/ui/BreadcrumbBar.vue', () => ({ default: { template: '', props: ['segments', 'rootLabel'], emits: ['navigate'] }, })) vi.mock('../components/upload/DropZone.vue', () => ({ default: { template: '
', emits: ['files-selected'] }, })) vi.mock('../components/upload/UploadProgress.vue', () => ({ default: { template: '', props: ['items'] }, })) vi.mock('../components/folders/FolderDeleteModal.vue', () => ({ default: { template: '', props: ['folder'], emits: ['confirm', 'cancel'] }, })) vi.mock('../components/sharing/ShareModal.vue', () => ({ default: { template: '', props: ['doc'], emits: ['close'] }, })) vi.mock('../components/documents/SearchBar.vue', () => ({ default: { template: '', props: ['modelValue'] }, })) vi.mock('../components/documents/SortControls.vue', () => ({ default: { template: '', props: ['sort', 'order'], emits: ['change'] }, })) vi.mock('../components/topics/TopicBadge.vue', () => ({ default: { template: '', props: ['name', 'color'] }, })) function makeRouter() { return createRouter({ history: createMemoryHistory(), routes: [ { path: '/', component: FileManagerView }, { path: '/folders/:folderId', component: FileManagerView }, ], }) } async function mountFileManager(path = '/') { setActivePinia(createPinia()) const router = makeRouter() await router.push(path) await router.isReady() const w = mount(FileManagerView, { global: { plugins: [router], stubs: { QuotaBar: true, AppSpinner: true } }, }) await flushPromises() return w } describe('UX-05: "/" focuses the search bar', () => { it('FileManagerView exposes focusSearch() that delegates to browserRef.focusSearch', async () => { const w = await mountFileManager() const focusSpy = vi.fn() const browserRef = w.vm.$.setupState.browserRef if (browserRef?.value) browserRef.value.focusSearch = focusSpy expect(typeof w.vm.focusSearch).toBe('function') }) it('calling focusSearch() on the exposed interface does not throw when no ref mounted', async () => { const w = await mountFileManager() expect(() => w.vm.focusSearch()).not.toThrow() }) it('focusSearch is part of defineExpose (accessible via vm)', async () => { const w = await mountFileManager() expect(w.vm.focusSearch).toBeDefined() }) }) describe('UX-06: "Escape" clears active search (when no input focused)', () => { it('FileManagerView exposes clearSearch() method', async () => { const w = await mountFileManager() expect(typeof w.vm.clearSearch).toBe('function') }) it('calling clearSearch() does not throw', async () => { const w = await mountFileManager() expect(() => w.vm.clearSearch()).not.toThrow() }) }) describe('UX-07: "U" triggers the upload picker', () => { it('FileManagerView exposes triggerUpload() method', async () => { const w = await mountFileManager() expect(typeof w.vm.triggerUpload).toBe('function') }) it('calling triggerUpload() does not throw', async () => { const w = await mountFileManager() expect(() => w.vm.triggerUpload()).not.toThrow() }) }) describe('UX-08: "N" starts new folder input', () => { it('FileManagerView exposes startNewFolder() method', async () => { const w = await mountFileManager() expect(typeof w.vm.startNewFolder).toBe('function') }) it('calling startNewFolder() via exposed interface delegates to browserRef without throwing', async () => { const w = await mountFileManager() expect(() => w.vm.startNewFolder()).not.toThrow() }) }) describe('Gap 4: getFileManagerInstance resolves to actual component, not RouterView proxy', () => { it('matched.find(r => r.instances?.default) returns an object with focusSearch defined when mounted via router-view', async () => { setActivePinia(createPinia()) const router = makeRouter() await router.push('/') await router.isReady() // Mount via a router-view wrapper so Vue Router populates r.instances.default const { defineComponent, h } = await import('vue') const { RouterView } = await import('vue-router') const App = defineComponent({ render: () => h(RouterView) }) mount(App, { global: { plugins: [router], stubs: { QuotaBar: true, AppSpinner: true } }, }) await flushPromises() const instance = router.currentRoute.value.matched.find(r => r.instances?.default)?.instances?.default expect(instance).not.toBeNull() expect(instance).toBeDefined() expect(typeof instance.focusSearch).toBe('function') }) })