/** * T030: ForecastOrderDetail.vue 详情抽屉组件测试 * GREEN phase — 组件已实现,测试真实功能 * * 覆盖场景: * 1. 6 信息分组按状态条件展示 * 2. 操作按钮按角色+状态动态显隐 * 3. 评审面板切换 * 4. 变更记录子表渲染 * 5. 转FO 确认弹窗 */ import { describe, it, expect, beforeEach, vi } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { nextTick } from 'vue' import { createPinia, setActivePinia } from 'pinia' import ElementPlus from 'element-plus' // Mock vue-i18n vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key, }), })) // Mock API vi.mock('@/api/forecast/forecast-order', () => ({ getById: vi.fn().mockResolvedValue({ code: 800, data: null, }), withdrawForecastOrder: vi.fn(), discardForecastOrder: vi.fn(), reviewAccept: vi.fn(), reviewReject: vi.fn(), resubmit: vi.fn(), findChangeOrdersByForecastId: vi.fn().mockResolvedValue({ code: 800, data: [], }), })) vi.mock('@/utils', () => ({ reqIsSucceed: vi.fn((res: unknown) => (res as { code: number }).code === 800), })) vi.mock('@/utils/errorHandler', () => ({ handleApiError: vi.fn(), })) vi.mock('element-plus', async () => { const actual = await vi.importActual('element-plus') return { ...(actual as object), ElMessage: { success: vi.fn(), error: vi.fn(), info: vi.fn() }, ElMessageBox: { confirm: vi.fn(() => Promise.resolve()), alert: vi.fn() }, } }) import ForecastOrderDetail from '@/views/forecast-order/ForecastOrderDetail.vue' import { useForecastOrderDetailStore } from '@/store/modules/forecast-order/forecastOrderDetailStore' // Mock $t global property const mockT = (key: string) => key const mockI18nPlugin = { install(app: any) { app.config.globalProperties.$t = mockT }, } const mockDetail = { forecastOrderId: 1, forecastOrderNo: 'FO-001', foStatus: 'SCHEDULED' as const, forecastOrderType: 'NORMAL', tradeType: 'DOMESTIC' as const, submitter: 1001, submitTime: '2026-06-29 10:00:00', customerCode: 'CUS001', customerName: '测试客户', productCode: 'PROD001', productName: '测试产品', productCategory: '成品', materialLevel: 'FINISHED' as const, standardization: 'STANDARD' as const, minOrderQty: 100, forecastQty: 1000, unit: '公斤', shippingPoint: 'SHANGHAI' as const, expectedDeliveryDate: '2026-07-15', customsManualNo: null, rawMaterialOrigin: null, processingFlag: 0, designatedFactory: null, planSuggestedQty: 800, planReplyDate: '2026-07-20', planReplyComment: '同意', rejectReason: null, actualScheduleQty: 800, convertedQty: 200, releasedQty: 0, relatedSoNos: null, reasonDesc: '测试', lowCarbonRatio: null, forceConvertDeadline: null, forecastReplyTime: null, groupCustomerCode: null, groupCustomerName: null, creationDate: '2026-06-29', createdBy: 1001, lastUpdateDate: '2026-06-29', lastUpdatedBy: 1001, deleteFlag: 0, versionNum: 1, } describe('ForecastOrderDetail.vue - T030 (5条)', () => { beforeEach(() => { document.body.innerHTML = '' setActivePinia(createPinia()) }) it('1. 详情抽屉渲染基本信息卡片', async () => { const wrapper = mount(ForecastOrderDetail, { props: { visible: true, forecastOrderId: 1 }, global: { plugins: [ElementPlus, mockI18nPlugin] }, attachTo: document.body, }) await nextTick() await flushPromises() // 预设 store detail 数据 const store = useForecastOrderDetailStore() store.detail = { ...mockDetail } await nextTick() const html = document.body.innerHTML expect(html).toContain('forecast-order.detail.basicInfo') expect(html).toContain('forecast-order.detail.customerInfo') expect(html).toContain('forecast-order.detail.materialInfo') }) it('2. 操作按钮按角色+状态动态显隐', async () => { const wrapper = mount(ForecastOrderDetail, { props: { visible: true, forecastOrderId: 1 }, global: { plugins: [ElementPlus, mockI18nPlugin] }, attachTo: document.body, }) await nextTick() const store = useForecastOrderDetailStore() store.detail = { ...mockDetail, foStatus: 'SCHEDULED' as const } store.currentRole = 'salesman' await nextTick() const html = document.body.innerHTML // SCHEDULED + sales 应包含 change 和 convert 按钮 expect(html).toContain('forecast-order.action.submitChange') expect(html).toContain('forecast-order.action.convertToOrder') }) it('3. 计划员点击评审 → ReplyDialog 显示', async () => { const wrapper = mount(ForecastOrderDetail, { props: { visible: true, forecastOrderId: 1 }, global: { plugins: [ElementPlus, mockI18nPlugin] }, attachTo: document.body, }) await nextTick() const store = useForecastOrderDetailStore() store.detail = { ...mockDetail, foStatus: 'SUBMITTED' as const } store.currentRole = 'planner' await nextTick() const html = document.body.innerHTML // 应包含评审接收/驳回按钮 expect(html).toContain('forecast-order.action.reviewAccept') expect(html).toContain('forecast-order.action.reviewReject') }) it('4. 变更记录子表渲染', async () => { const wrapper = mount(ForecastOrderDetail, { props: { visible: true, forecastOrderId: 1 }, global: { plugins: [ElementPlus, mockI18nPlugin] }, attachTo: document.body, }) await nextTick() const store = useForecastOrderDetailStore() store.detail = { ...mockDetail } store.changeList = [] store.changeListLoading = false await nextTick() const html = document.body.innerHTML expect(html).toContain('forecast-order.detail.changeHistory') }) it('5. 点击转订单 → TransferFoDialog 弹出', async () => { const wrapper = mount(ForecastOrderDetail, { props: { visible: true, forecastOrderId: 1 }, global: { plugins: [ElementPlus, mockI18nPlugin] }, attachTo: document.body, }) await nextTick() const store = useForecastOrderDetailStore() store.detail = { ...mockDetail, foStatus: 'SCHEDULED' as const } store.currentRole = 'salesman' await nextTick() // 找到转订单按钮并点击 const buttons = document.querySelectorAll('.el-button') const convertBtn = Array.from(buttons).find( (b) => b.textContent?.includes('forecast-order.action.convertToOrder'), ) if (convertBtn) { ;(convertBtn as HTMLElement).click() await nextTick() } const html = document.body.innerHTML // TransferFoDialog 应该弹出 expect(html).toContain('forecast-order.transfer.title') }) })