feat(12-06): UUID-only cloud navigation, breadcrumb hierarchy, route-aware sidebar
- CloudProviderTreeItem: getCloudFoldersByConnectionId(connection.id) + /cloud/{uuid}/root
- CloudFolderTreeItem: connectionId prop replaces provider slug in browse + route
- AppSidebar: Cloud Storage active only on exact /cloud; connection nodes use per-connection isActive
- New tests: CloudProviderTreeItem, CloudFolderTreeItem, AppSidebar cloud active-state
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
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: `<div data-test="tree-item" @click="$emit('select')"><slot name="children" :children="[]" /></div>`,
|
||||
props: ['label', 'loadChildren', 'depth', 'isActive', 'expandable'],
|
||||
emits: ['select'],
|
||||
}
|
||||
|
||||
const globalStubs = {
|
||||
TreeItem: TreeItemStub,
|
||||
AppIcon: true,
|
||||
}
|
||||
|
||||
const folder = { 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, 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, 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, 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, connectionId: 'uuid-conn-1', depth: 2 },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
expect(wrapper.findComponent(TreeItemStub).props('isActive')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
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'
|
||||
|
||||
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 CloudProviderTreeItem from '../CloudProviderTreeItem.vue'
|
||||
|
||||
const TreeItemStub = {
|
||||
name: 'TreeItem',
|
||||
template: `<div data-test="tree-item" @click="$emit('select')"><slot name="children" :children="[]" /></div>`,
|
||||
props: ['label', 'loadChildren', 'depth', 'isActive'],
|
||||
emits: ['select'],
|
||||
}
|
||||
|
||||
const globalStubs = {
|
||||
TreeItem: TreeItemStub,
|
||||
CloudFolderTreeItem: true,
|
||||
AppIcon: true,
|
||||
}
|
||||
|
||||
const connection = {
|
||||
id: 'uuid-conn-1',
|
||||
provider: 'nextcloud',
|
||||
display_name: 'My Nextcloud',
|
||||
status: 'ACTIVE',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
mockRoutePath = '/cloud'
|
||||
})
|
||||
|
||||
describe('CloudProviderTreeItem', () => {
|
||||
it('uses connection.id (UUID) for browse, never provider slug', async () => {
|
||||
const wrapper = mount(CloudProviderTreeItem, {
|
||||
props: { connection },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
// Call loadChildren from the stub's prop
|
||||
const treeItemStub = wrapper.findComponent(TreeItemStub)
|
||||
const loadChildren = treeItemStub.props('loadChildren')
|
||||
if (loadChildren) {
|
||||
await loadChildren()
|
||||
expect(api.getCloudFoldersByConnectionId).toHaveBeenCalledWith('uuid-conn-1', '')
|
||||
expect(api.getCloudFoldersByConnectionId).not.toHaveBeenCalledWith('nextcloud', expect.anything())
|
||||
}
|
||||
// getCloudFolders (deprecated) must never be called
|
||||
expect(api.getCloudFolders).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('navigates to /cloud/{uuid}/root on select, never /cloud/{provider}', async () => {
|
||||
const wrapper = mount(CloudProviderTreeItem, {
|
||||
props: { connection },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
await wrapper.find('[data-test="tree-item"]').trigger('click')
|
||||
expect(mockPush).toHaveBeenCalledWith('/cloud/uuid-conn-1/root')
|
||||
expect(mockPush).not.toHaveBeenCalledWith(expect.stringContaining('nextcloud'))
|
||||
})
|
||||
|
||||
it('isActive=true when route matches connection root', () => {
|
||||
mockRoutePath = '/cloud/uuid-conn-1/root'
|
||||
const wrapper = mount(CloudProviderTreeItem, {
|
||||
props: { connection },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
expect(wrapper.findComponent(TreeItemStub).props('isActive')).toBe(true)
|
||||
})
|
||||
|
||||
it('isActive=false when route is /cloud (overview)', () => {
|
||||
mockRoutePath = '/cloud'
|
||||
const wrapper = mount(CloudProviderTreeItem, {
|
||||
props: { connection },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
expect(wrapper.findComponent(TreeItemStub).props('isActive')).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user