From 3fcc300ebe4a7c2e1c613a8b1623678ddc6fdb2c Mon Sep 17 00:00:00 2001 From: curo1305 Date: Mon, 15 Jun 2026 20:23:41 +0200 Subject: [PATCH] test(10-07): promote AppSidebar.empty stubs to RED tests (UX-01, UX-03, UX-14) - Replace 10 it.todo stubs with 9 real tests across 3 describe blocks - UX-03: 3 tests verify skeleton rows during loading (folders/topics/cloud) - UX-01 sidebar micro: 3 tests verify EmptyState size=sm for each empty section - UX-14: 3 tests verify "New" button gone, methods removed, no inline folder input - 8/9 tests RED (1 passes trivially: input hidden while showNewFolderInput=false) - Adapted for script setup (Composition API) rather than Options API as described in plan --- .../layout/__tests__/AppSidebar.empty.test.js | 367 +++++++++++++++++- 1 file changed, 356 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/layout/__tests__/AppSidebar.empty.test.js b/frontend/src/components/layout/__tests__/AppSidebar.empty.test.js index 5e5dd9d..5781c29 100644 --- a/frontend/src/components/layout/__tests__/AppSidebar.empty.test.js +++ b/frontend/src/components/layout/__tests__/AppSidebar.empty.test.js @@ -1,20 +1,365 @@ -import { describe, it } from 'vitest' +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', () => { - it.todo('renders skeleton rows in folder section when loadingRoots=true') - it.todo('renders skeleton rows in topics section when topicsStore.loading=true') - it.todo('renders skeleton rows in cloud section when loadingCloudConnections=true') - it.todo('Loading… text is removed from all three sections') + 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', () => { - it.todo('folders empty: EmptyState size=sm with icon=folder') - it.todo('topics empty: EmptyState size=sm with icon=tag') - it.todo('cloud empty: EmptyState size=sm with icon=cloud and a Settings link in #cta') + 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', () => { - it.todo('no