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, RouterLink: true, EmptyState: true, } function makeRouter() { return createRouter({ history: createMemoryHistory(), routes: [ { path: '/', component: { template: '
' } }, { path: '/settings', component: { template: '' } }, { path: '/topics', component: { template: '' } }, { path: '/shared', component: { template: '' } }, { path: '/cloud', component: { template: '' } }, { path: '/admin', component: { template: '' } }, ], }) } async function mountSidebar(storeOverrides = {}) { setActivePinia(createPinia()) const { useFoldersStore } = await import('../../../stores/folders.js') const { useTopicsStore } = await import('../../../stores/topics.js') const { useCloudConnectionsStore } = await import('../../../stores/cloudConnections.js') const foldersStore = useFoldersStore() const topicsStore = useTopicsStore() const cloudStore = useCloudConnectionsStore() if (storeOverrides.folders !== undefined) Object.assign(foldersStore, storeOverrides.folders) if (storeOverrides.topics !== undefined) Object.assign(topicsStore, storeOverrides.topics) if (storeOverrides.cloud !== undefined) Object.assign(cloudStore, storeOverrides.cloud) const router = makeRouter() await router.push('/') await router.isReady() const AppSidebar = (await import('../AppSidebar.vue')).default const wrapper = mount(AppSidebar, { global: { plugins: [router], stubs: STUBS, }, }) await flushPromises() if (storeOverrides.folders !== undefined) Object.assign(foldersStore, storeOverrides.folders) if (storeOverrides.topics !== undefined) Object.assign(topicsStore, storeOverrides.topics) if (storeOverrides.cloud !== undefined) Object.assign(cloudStore, storeOverrides.cloud) await wrapper.vm.$nextTick() return wrapper } describe('UX-03: AppSidebar shows skeleton placeholders while loading', () => { beforeEach(() => { vi.clearAllMocks() vi.resetModules() }) it('renders skeleton rows in folder section when loadingRoots=true', async () => { const { listFolders } = await import('../../../api/client.js') let resolveListFolders listFolders.mockReturnValueOnce(new Promise(r => { resolveListFolders = r })) setActivePinia(createPinia()) const router = makeRouter() await router.push('/') await router.isReady() const AppSidebar = (await import('../AppSidebar.vue')).default const wrapper = mount(AppSidebar, { global: { plugins: [router], stubs: { ...STUBS, EmptyState: false } }, }) const pulses = wrapper.findAll('.animate-pulse') expect(pulses.length).toBeGreaterThanOrEqual(3) resolveListFolders({ items: [] }) await flushPromises() wrapper.unmount() }) it('renders skeleton rows in topics section when topicsStore.loading=true', async () => { vi.resetModules() vi.doMock('../../../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 }), })) setActivePinia(createPinia()) const { useTopicsStore } = await import('../../../stores/topics.js') const topicsStore = useTopicsStore() topicsStore.loading = true topicsStore.topics = [] const router = makeRouter() await router.push('/') await router.isReady() const AppSidebar = (await import('../AppSidebar.vue')).default const wrapper = mount(AppSidebar, { global: { plugins: [router], stubs: STUBS }, }) await flushPromises() topicsStore.loading = true topicsStore.topics = [] await wrapper.vm.$nextTick() const pulses = wrapper.findAll('.animate-pulse') expect(pulses.length).toBeGreaterThanOrEqual(3) wrapper.unmount() }) it('renders skeleton rows in cloud section when loadingCloudConnections=true', async () => { vi.resetModules() vi.doMock('../../../api/client.js', () => ({ listFolders: vi.fn().mockResolvedValue({ items: [] }), getSharedWithMe: vi.fn().mockResolvedValue([]), listCloudConnections: vi.fn().mockReturnValue(new Promise(() => {})), listTopics: vi.fn().mockResolvedValue({ topics: [] }), getMyQuota: vi.fn().mockResolvedValue({ used_bytes: 0, limit_bytes: 104857600 }), })) setActivePinia(createPinia()) const { useCloudConnectionsStore } = await import('../../../stores/cloudConnections.js') const cloudStore = useCloudConnectionsStore() cloudStore.loading = true cloudStore.connections = [] const router = makeRouter() await router.push('/') await router.isReady() const AppSidebar = (await import('../AppSidebar.vue')).default const wrapper = mount(AppSidebar, { global: { plugins: [router], stubs: STUBS }, }) cloudStore.loading = true cloudStore.connections = [] await wrapper.vm.$nextTick() const pulses = wrapper.findAll('.animate-pulse') expect(pulses.length).toBeGreaterThanOrEqual(3) wrapper.unmount() }) }) describe('UX-01 (sidebar micro): EmptyState size=sm appears when each section is empty', () => { beforeEach(() => { vi.clearAllMocks() vi.resetModules() }) it('folders empty: EmptyState size=sm with icon=folder', async () => { vi.doMock('../../../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 }), })) setActivePinia(createPinia()) const { useFoldersStore } = await import('../../../stores/folders.js') const foldersStore = useFoldersStore() const router = makeRouter() await router.push('/') await router.isReady() const AppSidebar = (await import('../AppSidebar.vue')).default const wrapper = mount(AppSidebar, { global: { plugins: [router], stubs: { ...STUBS, EmptyState: false } }, }) await flushPromises() foldersStore.rootFolders = [] await wrapper.vm.$nextTick() const emptyStates = wrapper.findAll('empty-state-stub,emptystate-stub,[data-testid="empty-state"]') const html = wrapper.html() expect(html).toContain('icon="folder"') wrapper.unmount() }) it('topics empty: EmptyState size=sm with icon=tag', async () => { vi.doMock('../../../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 }), })) setActivePinia(createPinia()) const { useTopicsStore } = await import('../../../stores/topics.js') const topicsStore = useTopicsStore() const router = makeRouter() await router.push('/') await router.isReady() const AppSidebar = (await import('../AppSidebar.vue')).default const wrapper = mount(AppSidebar, { global: { plugins: [router], stubs: { ...STUBS, EmptyState: false } }, }) await flushPromises() topicsStore.topics = [] topicsStore.loading = false await wrapper.vm.$nextTick() const html = wrapper.html() expect(html).toContain('icon="tag"') wrapper.unmount() }) it('cloud empty: EmptyState size=sm with icon=cloud and a Settings link in #cta', async () => { vi.doMock('../../../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 }), })) setActivePinia(createPinia()) const { useCloudConnectionsStore } = await import('../../../stores/cloudConnections.js') const cloudStore = useCloudConnectionsStore() const router = makeRouter() await router.push('/') await router.isReady() const AppSidebar = (await import('../AppSidebar.vue')).default const wrapper = mount(AppSidebar, { global: { plugins: [router], stubs: { ...STUBS, EmptyState: false } }, }) await flushPromises() cloudStore.connections = [] cloudStore.loading = false await wrapper.vm.$nextTick() const html = wrapper.html() expect(html).toContain('icon="cloud"') wrapper.unmount() }) }) describe('UX-14: AppSidebar no longer renders an inline "New" folder button', () => { beforeEach(() => { vi.clearAllMocks() vi.resetModules() }) it('no