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
This commit is contained in:
curo1305
2026-06-15 20:23:41 +02:00
parent a1885122a1
commit 3fcc300ebe
@@ -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: '<div/>' } },
{ path: '/settings', component: { template: '<div/>' } },
{ path: '/topics', component: { template: '<div/>' } },
{ path: '/shared', component: { template: '<div/>' } },
{ path: '/cloud', component: { template: '<div/>' } },
{ path: '/admin', component: { template: '<div/>' } },
],
})
}
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', () => { describe('UX-03: AppSidebar shows skeleton placeholders while loading', () => {
it.todo('renders skeleton rows in folder section when loadingRoots=true') beforeEach(() => {
it.todo('renders skeleton rows in topics section when topicsStore.loading=true') vi.clearAllMocks()
it.todo('renders skeleton rows in cloud section when loadingCloudConnections=true') vi.resetModules()
it.todo('Loading… text is removed from all three sections') })
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', () => { describe('UX-01 (sidebar micro): EmptyState size=sm appears when each section is empty', () => {
it.todo('folders empty: EmptyState size=sm with icon=folder') beforeEach(() => {
it.todo('topics empty: EmptyState size=sm with icon=tag') vi.clearAllMocks()
it.todo('cloud empty: EmptyState size=sm with icon=cloud and a Settings link in #cta') 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', () => { describe('UX-14: AppSidebar no longer renders an inline "New" folder button', () => {
it.todo('no <button> with text "New" exists in the rendered template') beforeEach(() => {
it.todo('startNewFolder/cancelNewFolder/submitNewFolder methods are removed') vi.clearAllMocks()
it.todo('newFolderName/showNewFolderInput state is removed') vi.resetModules()
})
it('no <button> with text "New" exists in the rendered template', 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 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()
const buttons = wrapper.findAll('button')
const newButtons = buttons.filter(b => b.text().trim() === 'New')
expect(newButtons.length).toBe(0)
wrapper.unmount()
})
it('startNewFolder/cancelNewFolder/submitNewFolder methods are removed', 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 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()
expect(wrapper.vm.startNewFolder).toBeUndefined()
expect(wrapper.vm.cancelNewFolder).toBeUndefined()
expect(wrapper.vm.submitNewFolder).toBeUndefined()
wrapper.unmount()
})
it('newFolderName/showNewFolderInput state is removed (no inline folder input in DOM)', 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 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()
const inputs = wrapper.findAll('input[placeholder="Folder name"]')
expect(inputs.length).toBe(0)
wrapper.unmount()
})
}) })