diff --git a/frontend/src/components/cloud/CloudFolderTreeItem.vue b/frontend/src/components/cloud/CloudFolderTreeItem.vue
index 4f5d26d..ae1ca69 100644
--- a/frontend/src/components/cloud/CloudFolderTreeItem.vue
+++ b/frontend/src/components/cloud/CloudFolderTreeItem.vue
@@ -4,6 +4,7 @@
:expandable="folder.is_dir"
:load-children="loadChildren"
:depth="depth"
+ :is-active="isActive"
@select="navigate"
>
@@ -15,7 +16,7 @@
v-for="child in children"
:key="child.id"
:folder="child"
- :provider="provider"
+ :connection-id="connectionId"
:depth="depth + 1"
/>
@@ -23,25 +24,33 @@
diff --git a/frontend/src/components/cloud/CloudProviderTreeItem.vue b/frontend/src/components/cloud/CloudProviderTreeItem.vue
index cb521c3..bfa0c16 100644
--- a/frontend/src/components/cloud/CloudProviderTreeItem.vue
+++ b/frontend/src/components/cloud/CloudProviderTreeItem.vue
@@ -1,8 +1,9 @@
@@ -13,7 +14,7 @@
v-for="folder in children"
:key="folder.id"
:folder="folder"
- :provider="connection.provider"
+ :connection-id="connection.id"
:depth="depth + 1"
/>
@@ -22,7 +23,7 @@
diff --git a/frontend/src/components/cloud/__tests__/CloudFolderTreeItem.test.js b/frontend/src/components/cloud/__tests__/CloudFolderTreeItem.test.js
new file mode 100644
index 0000000..f9e30ed
--- /dev/null
+++ b/frontend/src/components/cloud/__tests__/CloudFolderTreeItem.test.js
@@ -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: `
`,
+ 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)
+ })
+})
diff --git a/frontend/src/components/cloud/__tests__/CloudProviderTreeItem.test.js b/frontend/src/components/cloud/__tests__/CloudProviderTreeItem.test.js
new file mode 100644
index 0000000..227ae86
--- /dev/null
+++ b/frontend/src/components/cloud/__tests__/CloudProviderTreeItem.test.js
@@ -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: `
`,
+ 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)
+ })
+})
diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue
index eeed7f0..0a79630 100644
--- a/frontend/src/components/layout/AppSidebar.vue
+++ b/frontend/src/components/layout/AppSidebar.vue
@@ -96,7 +96,7 @@
Cloud Storage
diff --git a/frontend/src/components/layout/__tests__/AppSidebar.test.js b/frontend/src/components/layout/__tests__/AppSidebar.test.js
new file mode 100644
index 0000000..963489b
--- /dev/null
+++ b/frontend/src/components/layout/__tests__/AppSidebar.test.js
@@ -0,0 +1,76 @@
+/**
+ * AppSidebar cloud active-state tests.
+ *
+ * Verifies that the "Cloud Storage" nav link is active only when the route
+ * is exactly /cloud (the overview), NOT when a connection or folder is active.
+ */
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { mount, flushPromises } from '@vue/test-utils'
+import { setActivePinia, createPinia } from 'pinia'
+import { createRouter, createMemoryHistory } from 'vue-router'
+
+vi.mock('../../../api/client.js', () => ({
+ listFolders: vi.fn().mockResolvedValue({ items: [] }),
+ getSharedWithMe: vi.fn().mockResolvedValue([]),
+ listCloudConnections: vi.fn().mockResolvedValue({ items: [] }),
+ listTopics: vi.fn().mockResolvedValue({ topics: [] }),
+ getMyQuota: vi.fn().mockResolvedValue({ used_bytes: 0, limit_bytes: 104857600 }),
+}))
+
+const STUBS = {
+ FolderTreeItem: true,
+ CloudProviderTreeItem: true,
+ QuotaBar: true,
+ EmptyState: true,
+}
+
+function makeRouter(initialPath = '/cloud') {
+ const router = createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ { path: '/', component: { template: '' } },
+ { path: '/settings', component: { template: '' } },
+ { path: '/topics', component: { template: '' } },
+ { path: '/shared', component: { template: '' } },
+ { path: '/cloud', component: { template: '' } },
+ { path: '/cloud/:connectionId/:folderId', component: { template: '' } },
+ ],
+ })
+ router.push(initialPath)
+ return router
+}
+
+import AppSidebar from '../AppSidebar.vue'
+
+beforeEach(() => {
+ setActivePinia(createPinia())
+ vi.clearAllMocks()
+})
+
+describe('AppSidebar cloud active state', () => {
+ it('Cloud Storage link has nav-link-active class at /cloud (exact)', async () => {
+ const router = makeRouter('/cloud')
+ await router.isReady()
+ const wrapper = mount(AppSidebar, {
+ global: { plugins: [router], stubs: STUBS },
+ })
+ await flushPromises()
+ // Find the Cloud Storage router-link — it should be active on /cloud
+ const cloudLink = wrapper.findAll('a').find(a => a.text().includes('Cloud Storage'))
+ expect(cloudLink).toBeTruthy()
+ expect(cloudLink.classes()).toContain('nav-link-active')
+ })
+
+ it('Cloud Storage link does NOT have nav-link-active when inside a connection folder', async () => {
+ const router = makeRouter('/cloud/uuid-conn-1/root')
+ await router.isReady()
+ const wrapper = mount(AppSidebar, {
+ global: { plugins: [router], stubs: STUBS },
+ })
+ await flushPromises()
+ const cloudLink = wrapper.findAll('a').find(a => a.text().includes('Cloud Storage'))
+ expect(cloudLink).toBeTruthy()
+ // Must NOT be active — only the connection/folder node is active
+ expect(cloudLink.classes()).not.toContain('nav-link-active')
+ })
+})