import { afterEach, describe, expect, it, vi } from 'vitest' import { flushPromises, mount, type VueWrapper } from '@vue/test-utils' import { createMemoryHistory, createRouter } from 'vue-router' import { NTooltip } from 'naive-ui' import AssetLibraryPage from '@/features/project-assets/AssetLibraryPage.vue' let wrapper: VueWrapper | undefined afterEach(() => { wrapper?.unmount() wrapper = undefined document.body.innerHTML = '' vi.unstubAllGlobals() }) describe('全局资产库', () => { it('单次读取并直接显示全部剧本图片,支持按图片与剧本搜索', async () => { const assets = [ { id: 'asset-scene', projectId: 'project-one', name: '雨夜街道', type: 'image', category: 'scene', mimeType: 'image/png', extension: 'png', size: 4096, publicUrl: '/storage/scene.png', metadata: { originalName: 'rain-street-original.png' }, createdAt: '2026-09-22T00:00:00Z', updatedAt: '2026-09-22T00:00:00Z', project: { id: 'project-one', title: '记忆当铺', topic: '记忆交易', status: 'completed' } }, { id: 'asset-character', projectId: 'project-two', name: '林默正面照', type: 'image', category: 'character', mimeType: 'image/jpeg', extension: 'jpg', size: 8192, publicUrl: '/storage/character.jpg', metadata: null, createdAt: '2026-09-22T01:00:00Z', updatedAt: '2026-09-22T01:00:00Z', project: { id: 'project-two', title: '天降甘霖', topic: '乡村故事', status: 'completed' } } ] const projects = assets.map(asset => ({ ...asset.project, style: null, createdAt: '', updatedAt: '' })) const fetcher = vi.fn().mockImplementation(async input => { const url = String(input) return new Response(JSON.stringify({ data: url.endsWith('/projects') ? projects : assets })) }) vi.stubGlobal('fetch', fetcher) const router = createRouter({ history: createMemoryHistory(), routes: [ { path: '/assets', component: AssetLibraryPage }, { path: '/projects/:projectId/assets', component: { template: '
' } } ] }) await router.push('/assets') wrapper = mount(AssetLibraryPage, { attachTo: document.body, global: { plugins: [router] } }) await flushPromises() expect(fetcher).toHaveBeenCalledTimes(2) expect(fetcher).toHaveBeenCalledWith('/api/assets', expect.objectContaining({ method: 'GET' })) expect(wrapper.findAll('.asset-library-card')).toHaveLength(2) expect(wrapper.text()).toContain('雨夜街道') expect(wrapper.text()).toContain('林默正面照') expect(wrapper.text()).toContain('记忆当铺') expect(wrapper.findAllComponents(NTooltip)).toHaveLength(2) const title = wrapper.get('.asset-library-title') expect(title.classes()).toContain('asset-library-title') expect(wrapper.findComponent(NTooltip).props('disabled')).toBe(true) Object.defineProperties(title.element, { scrollWidth: { configurable: true, value: 240 }, clientWidth: { configurable: true, value: 120 } }) await title.trigger('mouseenter') expect(wrapper.findComponent(NTooltip).props('disabled')).toBe(false) expect(wrapper.get('.asset-library-card').text()).not.toContain('rain-street-original.png') expect(wrapper.get('.asset-library-card').text()).not.toContain('4.0 KB') await wrapper.get('[aria-label="预览图片"]').trigger('click') await flushPromises() expect(document.querySelector('[role="dialog"][aria-label="素材详情与管理"]')).toBeNull() expect(document.querySelector('.n-image-preview-container')).not.toBeNull() await wrapper.get('[aria-label="查看素材详情"]').trigger('click') await flushPromises() const dialog = document.querySelector('[role="dialog"][aria-label="素材详情与管理"]')! expect(dialog).not.toBeNull() expect(dialog.textContent).toContain('rain-street-original.png') expect(dialog.textContent).toContain('image/png') expect(dialog.textContent).toContain('4.0 KB') expect(router.currentRoute.value.path).toBe('/assets') await wrapper.get('input[aria-label="搜索资产图片"]').setValue('天降甘霖') expect(wrapper.findAll('.asset-library-card')).toHaveLength(1) expect(wrapper.text()).toContain('林默正面照') expect(wrapper.text()).not.toContain('雨夜街道') }) })