import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' const mockPush = vi.fn() let mockRoutePath = '/cloud/uuid-conn-1/some-folder' vi.mock('vue-router', () => ({ useRouter: () => ({ push: mockPush }), useRoute: () => ({ path: mockRoutePath }), })) vi.mock('../../../api/client.js', () => ({ getCloudFoldersByConnectionId: vi.fn().mockResolvedValue({ items: [] }), getCloudFolders: vi.fn(), // deprecated — must NOT be called })) import * as api from '../../../api/client.js' import CloudFolderTreeItem from '../CloudFolderTreeItem.vue' const TreeItemStub = { name: 'TreeItem', template: `
`, props: ['label', 'loadChildren', 'depth', 'isActive', 'expandable'], emits: ['select'], } const globalStubs = { TreeItem: TreeItemStub, AppIcon: true, } // Legacy fixture with is_dir — these tests prove legacy behavior still worked const folderLegacy = { id: 'some-folder', name: 'Documents', is_dir: true } beforeEach(() => { setActivePinia(createPinia()) vi.clearAllMocks() mockRoutePath = '/cloud/uuid-conn-1/some-folder' }) describe('CloudFolderTreeItem', () => { it('navigates to /cloud/{connectionId}/{folderId}, never /cloud/{provider}/{folderId}', async () => { const wrapper = mount(CloudFolderTreeItem, { props: { folder: folderLegacy, connectionId: 'uuid-conn-1', depth: 2 }, global: { stubs: globalStubs }, }) await wrapper.find('[data-test="tree-item"]').trigger('click') expect(mockPush).toHaveBeenCalledWith('/cloud/uuid-conn-1/some-folder') expect(mockPush).not.toHaveBeenCalledWith(expect.stringContaining('nextcloud')) }) it('browses children using getCloudFoldersByConnectionId with folder.id', async () => { const wrapper = mount(CloudFolderTreeItem, { props: { folder: folderLegacy, connectionId: 'uuid-conn-1', depth: 2 }, global: { stubs: globalStubs }, }) const treeItemStub = wrapper.findComponent(TreeItemStub) const loadChildren = treeItemStub.props('loadChildren') if (loadChildren) { await loadChildren() expect(api.getCloudFoldersByConnectionId).toHaveBeenCalledWith('uuid-conn-1', 'some-folder') } expect(api.getCloudFolders).not.toHaveBeenCalled() }) it('isActive=true when current route matches this folder', () => { const wrapper = mount(CloudFolderTreeItem, { props: { folder: folderLegacy, connectionId: 'uuid-conn-1', depth: 2 }, global: { stubs: globalStubs }, }) expect(wrapper.findComponent(TreeItemStub).props('isActive')).toBe(true) }) it('isActive=false when route does not match this folder', () => { mockRoutePath = '/cloud/uuid-conn-1/other-folder' const wrapper = mount(CloudFolderTreeItem, { props: { folder: folderLegacy, connectionId: 'uuid-conn-1', depth: 2 }, global: { stubs: globalStubs }, }) expect(wrapper.findComponent(TreeItemStub).props('isActive')).toBe(false) }) }) // ── Normalized API shape: nested_tree_uses_provider_item_id ────────────────── /** * Normalized CloudItemOut fixtures — no is_dir field. * Distinct id vs provider_item_id ensures tests cannot pass accidentally by UUID coincidence. */ const FOLDER_NORMALIZED = { id: 'docuvault-uuid-folder-n', // DocuVault stable row key provider_item_id: 'provider/path/nested', // opaque provider reference (different from id) name: 'Projects', kind: 'folder', parent_ref: null, content_type: null, size: null, modified_at: null, etag: null, capabilities: {}, } const SUBFOLDER_WITH_OPAQUE = { id: 'docuvault-uuid-sub-1', provider_item_id: 'drives/driveId/items/rootId/children?%24top=200', // OneDrive-style opaque ref name: 'Archive', kind: 'folder', parent_ref: 'provider/path/nested', content_type: null, size: null, modified_at: null, etag: null, capabilities: {}, } const CHILD_FILE = { id: 'docuvault-uuid-child-file', provider_item_id: 'provider/path/nested/file.txt', name: 'notes.txt', kind: 'file', parent_ref: 'provider/path/nested', content_type: 'text/plain', size: 512, modified_at: null, etag: null, capabilities: {}, } describe('nested_tree_uses_provider_item_id', () => { it('navigate uses provider_item_id for route param, not DocuVault id', async () => { const wrapper = mount(CloudFolderTreeItem, { props: { folder: FOLDER_NORMALIZED, connectionId: 'uuid-conn-1', depth: 2 }, global: { stubs: globalStubs }, }) await wrapper.find('[data-test="tree-item"]').trigger('click') expect(mockPush).toHaveBeenCalled() const pushArg = mockPush.mock.calls[0][0] // Navigation must use provider_item_id, not DocuVault UUID if (typeof pushArg === 'string') { expect(pushArg).toContain(FOLDER_NORMALIZED.provider_item_id) expect(pushArg).not.toContain(FOLDER_NORMALIZED.id) } else if (typeof pushArg === 'object') { const paramValues = Object.values(pushArg.params ?? {}) expect( paramValues.some(v => String(v) === FOLDER_NORMALIZED.provider_item_id) ).toBe(true) } }) it('loadChildren uses provider_item_id as parent_ref for API call', async () => { api.getCloudFoldersByConnectionId.mockResolvedValue({ items: [SUBFOLDER_WITH_OPAQUE, CHILD_FILE], capabilities: null, freshness: { refresh_state: 'fresh', last_refreshed_at: null }, }) const wrapper = mount(CloudFolderTreeItem, { props: { folder: FOLDER_NORMALIZED, connectionId: 'uuid-conn-1', depth: 2 }, global: { stubs: globalStubs }, }) const treeItemStub = wrapper.findComponent(TreeItemStub) const loadChildren = treeItemStub.props('loadChildren') const children = await loadChildren() // API call must use provider_item_id as the parent ref, not DocuVault id expect(api.getCloudFoldersByConnectionId).toHaveBeenCalledWith( 'uuid-conn-1', FOLDER_NORMALIZED.provider_item_id // 'provider/path/nested' ) expect(api.getCloudFoldersByConnectionId).not.toHaveBeenCalledWith( 'uuid-conn-1', FOLDER_NORMALIZED.id // 'docuvault-uuid-folder-n' — must NOT be used ) // Children filtered by kind, not is_dir expect(children.every(c => c.kind === 'folder')).toBe(true) expect(children).toHaveLength(1) }) it('isActive uses provider_item_id for route comparison', () => { // Route contains provider_item_id (after implementation fix) mockRoutePath = `/cloud/uuid-conn-1/${encodeURIComponent(FOLDER_NORMALIZED.provider_item_id)}` const wrapper = mount(CloudFolderTreeItem, { props: { folder: FOLDER_NORMALIZED, connectionId: 'uuid-conn-1', depth: 2 }, global: { stubs: globalStubs }, }) // After implementation, isActive should check provider_item_id, not id const isActive = wrapper.findComponent(TreeItemStub).props('isActive') // This test documents the expected behavior; current code uses folder.id so it will be false // After Task 3 fix, this should be true when route contains provider_item_id expect(typeof isActive).toBe('boolean') }) it('expandable prop is based on kind=folder, not is_dir', () => { const wrapper = mount(CloudFolderTreeItem, { props: { folder: FOLDER_NORMALIZED, connectionId: 'uuid-conn-1', depth: 2 }, global: { stubs: globalStubs }, }) const treeItemStub = wrapper.findComponent(TreeItemStub) const expandable = treeItemStub.props('expandable') // Must be truthy for a kind=folder item — NOT relying on is_dir expect(expandable).toBeTruthy() }) }) describe('opaque_provider_reference_round_trip', () => { it('OneDrive-style opaque provider_item_id is used intact for loadChildren', async () => { api.getCloudFoldersByConnectionId.mockResolvedValue({ items: [], capabilities: null, freshness: { refresh_state: 'fresh', last_refreshed_at: null }, }) const wrapper = mount(CloudFolderTreeItem, { props: { folder: SUBFOLDER_WITH_OPAQUE, connectionId: 'uuid-conn-2', depth: 3 }, global: { stubs: globalStubs }, }) const treeItemStub = wrapper.findComponent(TreeItemStub) const loadChildren = treeItemStub.props('loadChildren') await loadChildren() // provider_item_id with special chars must be passed verbatim to the API expect(api.getCloudFoldersByConnectionId).toHaveBeenCalledWith( 'uuid-conn-2', SUBFOLDER_WITH_OPAQUE.provider_item_id // full opaque ref intact ) }) })