import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { mount } from '@vue/test-utils' import { createRouter, createMemoryHistory } from 'vue-router' import { createPinia, setActivePinia } from 'pinia' import BannerManagement from '@/views/banner/BannerManagement.vue' // ===== Mock Element Plus ===== vi.mock('element-plus', () => ({ ElMessage: { success: vi.fn(), error: vi.fn(), warning: vi.fn() }, ElMessageBox: { confirm: vi.fn() }, })) // ===== Mock @element-plus/icons-vue ===== vi.mock('@element-plus/icons-vue', () => ({ Plus: { template: '' }, })) // ===== Mock banner.api ===== vi.mock('@/api/banner.api', () => ({ bannerApi: { getAll: vi.fn().mockResolvedValue([ { id: 1, imageUrl: '/api/files/1/preview', title: '轮播图1', subtitle: '副标题1', sortOrder: 1, isActive: true, createdAt: '2025-01-01T00:00:00' }, { id: 2, imageUrl: '/api/files/2/preview', title: '轮播图2', subtitle: '副标题2', sortOrder: 2, isActive: false, createdAt: '2025-01-02T00:00:00' }, ]), create: vi.fn().mockResolvedValue({}), update: vi.fn().mockResolvedValue({}), delete: vi.fn().mockResolvedValue({}), }, })) // ===== Mock request ===== vi.mock('@/utils/request', () => { const mockRequest = { get: vi.fn().mockResolvedValue({}), post: vi.fn().mockResolvedValue({}), put: vi.fn().mockResolvedValue({}), delete: vi.fn().mockResolvedValue({}), } return { default: mockRequest } }) // ===== Mock dateFormat ===== vi.mock('@/utils/dateFormat', () => ({ formatDateTime: vi.fn((val: string) => val || '-'), })) // ===== Mock URL.createObjectURL ===== if (!globalThis.URL.createObjectURL) { // @ts-ignore globalThis.URL.createObjectURL = vi.fn(() => `blob:mock-${Math.random()}`) // @ts-ignore globalThis.URL.revokeObjectURL = vi.fn() } describe('BannerManagement Component', () => { let router: any let wrapper: any const defaultStubs = { 'el-button': { template: '' }, 'el-card': { template: '
' }, 'el-input': { template: '', props: ['modelValue'], emits: ['update:modelValue'] }, 'el-icon': { template: '' }, 'el-table': { template: '
', props: ['data', 'loading'] }, 'el-table-column': true, 'el-tag': { template: '', props: ['type', 'effect', 'size'] }, 'el-dialog': { template: '
', props: ['modelValue', 'title', 'width'] }, 'el-form': { template: '
', props: ['model', 'rules', 'label-width', 'ref'] }, 'el-form-item': { template: '
', props: ['label', 'prop'] }, 'el-upload': { template: '
', props: ['http-request', 'show-file-list', 'before-upload', 'accept'] }, 'el-input-number': true, 'el-switch': true, 'el-image': true, } beforeEach(() => { const pinia = createPinia() setActivePinia(pinia) router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/', component: { template: '
' } }], }) vi.clearAllMocks() }) afterEach(() => { if (wrapper) wrapper.unmount() }) // ==================== 组件初始化 ==================== it('应该渲染轮播图管理容器', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(wrapper.find('.banner-management').exists()).toBe(true) }) it('应该渲染页面标题', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(wrapper.html()).toContain('轮播图管理') }) it('应该渲染新增轮播图按钮', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(wrapper.html()).toContain('新增轮播图') }) // ==================== 初始状态 ==================== it('初始 loading 状态为 false', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(wrapper.vm.loading).toBe(false) }) it('初始 modalVisible 为 false', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(wrapper.vm.modalVisible).toBe(false) }) it('初始 dataSource 为空数组', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(wrapper.vm.dataSource).toBeDefined() expect(Array.isArray(wrapper.vm.dataSource)).toBe(true) }) // ==================== 方法存在性 ==================== it('应该暴露 handleAdd 方法', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(typeof wrapper.vm.handleAdd).toBe('function') }) it('应该暴露 handleEdit 方法', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(typeof wrapper.vm.handleEdit).toBe('function') }) it('应该暴露 handleDelete 方法', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(typeof wrapper.vm.handleDelete).toBe('function') }) it('应该暴露 handleUpload 方法', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(typeof wrapper.vm.handleUpload).toBe('function') }) it('应该暴露 beforeUpload 方法', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(typeof wrapper.vm.beforeUpload).toBe('function') }) // ==================== 新增/编辑表单 ==================== it('点击新增后 modalVisible 应为 true', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() wrapper.vm.handleAdd() await wrapper.vm.$nextTick() expect(wrapper.vm.modalVisible).toBe(true) expect(wrapper.vm.modalTitle).toBe('新增轮播图') }) it('新增时表单 title 应为空', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() wrapper.vm.handleAdd() await wrapper.vm.$nextTick() expect(wrapper.vm.formState.title).toBe('') }) it('新增时 sortOrder 应为 0', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() wrapper.vm.handleAdd() await wrapper.vm.$nextTick() expect(wrapper.vm.formState.sortOrder).toBe(0) }) it('新增时 isActive 应为 true', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() wrapper.vm.handleAdd() await wrapper.vm.$nextTick() expect(wrapper.vm.formState.isActive).toBe(true) }) // ==================== 表单规则 ==================== it('应该定义 imageUrl 必填规则', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(wrapper.vm.formRules.imageUrl).toBeDefined() expect(wrapper.vm.formRules.imageUrl[0].required).toBe(true) }) it('应该定义 title 必填规则', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(wrapper.vm.formRules.title).toBeDefined() expect(wrapper.vm.formRules.title[0].required).toBe(true) }) it('应该定义 sortOrder 必填规则', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(wrapper.vm.formRules.sortOrder).toBeDefined() }) // ==================== extractFileId ==================== it('extractFileId 应能提取 /api/files/{id}/preview 格式的 ID', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(wrapper.vm.extractFileId('/api/files/123/preview')).toBe('123') }) it('extractFileId 应对纯数字字符串返回该数字', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(wrapper.vm.extractFileId('456')).toBe('456') }) it('extractFileId 应对 null/undefined 返回 null', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(wrapper.vm.extractFileId(null)).toBe(null) expect(wrapper.vm.extractFileId(undefined)).toBe(null) }) // ==================== beforeUpload ==================== it('beforeUpload 应拒绝非图片文件', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() const { ElMessage } = await import('element-plus') const result = wrapper.vm.beforeUpload(new File([''], 'test.txt', { type: 'text/plain' })) expect(result).toBe(false) expect(ElMessage.error).toHaveBeenCalled() }) it('beforeUpload 应拒绝超过 5MB 的图片', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() const { ElMessage } = await import('element-plus') const largeFile = new File([''], 'large.jpg', { type: 'image/jpeg' }) Object.defineProperty(largeFile, 'size', { value: 6 * 1024 * 1024 }) const result = wrapper.vm.beforeUpload(largeFile) expect(result).toBe(false) expect(ElMessage.error).toHaveBeenCalled() }) it('beforeUpload 应接受合法图片', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() const validFile = new File([''], 'small.jpg', { type: 'image/jpeg' }) const result = wrapper.vm.beforeUpload(validFile) expect(result).toBe(true) }) // ==================== 预览 ==================== it('应该暴露 previewImage 方法', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(typeof wrapper.vm.previewImage).toBe('function') }) it('初始 previewVisible 应为 false', async () => { wrapper = mount(BannerManagement, { global: { plugins: [router], stubs: defaultStubs }, }) await wrapper.vm.$nextTick() expect(wrapper.vm.previewVisible).toBe(false) }) })