import ElementPlus, { ElDialog, ElSelect, ElMessageBox } from 'element-plus' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAuthStore } from '@/core/auth/auth' import { useAccessPreviewStore } from '@/core/access/accessPreview' import AppDialog from '@/shared/components/AppDialog.vue' import CarrierRatesView from './CarrierRatesView.vue' import CarrierRateDetailView from './CarrierRateDetailView.vue' import * as api from '../api/platformPriceBooks' import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' import { listAllPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses' const { push } = vi.hoisted(() => ({ push: vi.fn() })) const privateId = '9007199254740993123' const customerId = '9007199254740993124' const domesticId = '9007199254740993125' const overseasId = '9007199254740993126' vi.mock('vue-router', () => ({ useRouter: () => ({ push }), useRoute: () => ({ params: { versionId: '9007199254740993123' } }) })) vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn() })) vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn() })) vi.mock('../api/platformPriceBooks', () => ({ downloadPlatformPriceBookSource: vi.fn(), listPlatformPriceBooks: vi.fn(), uploadPlatformPriceBook: vi.fn(), voidPlatformPriceBook: vi.fn(), getPlatformPriceBook: vi.fn(), publishPlatformPriceBook: vi.fn() })) let detail: api.PlatformPriceBookDetail const customer: PlatformTenantResource = { id: customerId, tenantCode: 'C-TEST', tenantName: '测试客户', status: 'ACTIVE', legalName: null, legalRepresentative: null, unifiedSocialCreditCode: null, contactName: null, contactPhone: null, contactEmail: null, servicePackage: null, contactWechat: null, salesOwnerName: null, paymentTermDays: null, enterpriseType: null, establishedDate: null, registeredCapital: null, registeredCapitalCurrency: null, registeredAddress: null, internalRemark: null, createdAt: null, approvedAt: null } const warehouse = (id: string, warehouseCode: string, warehouseType: 'DOMESTIC' | 'OVERSEAS'): PlatformWarehouseResource => ({ id, tenantId: '0', warehouseCode, internalCode: null, warehouseName: warehouseCode, warehouseType, countryCode: warehouseType === 'DOMESTIC' ? 'CN' : 'US', timezone: 'Asia/Shanghai', contactName: null, contactPhone: null, contactEmail: null, status: 'ACTIVE', remark: null }) const table = { props: ['data'], template: '
' } const plugins = [ElementPlus] function button(label: string, selector = 'button') { const found = Array.from(document.querySelectorAll(selector)).find(node => node.textContent?.trim() === label) expect(found, label).toBeDefined(); return found! } async function list() { const wrapper = mount(CarrierRatesView, { attachTo: document.body, global: { plugins, stubs: { DataTable: table, QueryToolbar: true } } }); await flushPromises(); return wrapper } async function upload() { const wrapper = await list(); button('上传价格表').click(); await flushPromises(); return wrapper } async function choose(wrapper: Awaited>, file: File) { Object.defineProperty(wrapper.get('input[type="file"]').element, 'files', { configurable: true, value: [file] }) await wrapper.get('input[type="file"]').trigger('change'); await flushPromises() } async function detailView() { const wrapper = mount(CarrierRateDetailView, { attachTo: document.body, global: { plugins } }); await flushPromises(); return wrapper } function setReason(value: string) { const input = document.querySelector('.el-message-box input')! input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })) } enableAutoUnmount(afterEach) afterEach(() => ElMessageBox.close()) beforeEach(() => { vi.resetAllMocks(); document.body.innerHTML = ''; setActivePinia(createPinia()) const auth = useAuthStore(); auth.token = 'test-session'; auth.permissions = ['firstleg:fba-pricing:rate:view','firstleg:fba-pricing:rate:edit','platform:warehouse:list'] useAccessPreviewStore().initializeAfterLogin() detail = { version: { id: privateId, customerId: '0', versionNo: 7, originalFileName: '测试价格表.xlsx', sourceFileId: 'private-file', sourceSha256: null, status: 'PENDING_CONFIRMATION', channelCount: 0, rateCount: 1, blockingErrorCount: 0, warningCount: 0, parseIssuesJson: null, createdBy: null, createdAt: null, publishedBy: null, publishedAt: null, supersededAt: null, voidedBy: null, voidedAt: null, voidReason: null }, channels: [], issues: [], rates: [{ id: null, channelCode: 'CHANNEL', priceModel: 'FBA_WAREHOUSE_RATE', originWarehouseId: null, originWarehouseCode: 'CN-TEST', originWarehouseName: '国内仓', destinationType: 'OVERSEAS', destinationCode: 'US-TEST', destinationWarehouseId: null, destinationName: '海外测试仓', regionCode: null, pricingTaxMode: 'TAX_INCLUDED', chargeUnit: 'KG', weightFromKg: null, unitPrice: 12.5, currency: 'CNY', sourceSheetName: 'Sheet1', sourceRowNumber: 8, sourceDestinationText: '海外测试仓', remark: null }] } vi.mocked(listPlatformTenants).mockResolvedValue({ records: [customer], total: 1, page: 1, size: 200, pages: 1 }) vi.mocked(listAllPlatformWarehouses).mockResolvedValue([warehouse(domesticId, 'CN-TEST', 'DOMESTIC'), warehouse(overseasId, 'US-TEST', 'OVERSEAS')]) vi.mocked(api.listPlatformPriceBooks).mockImplementation(async () => [detail.version]) vi.mocked(api.getPlatformPriceBook).mockImplementation(async () => detail) vi.mocked(api.uploadPlatformPriceBook).mockImplementation(async () => detail) vi.mocked(api.voidPlatformPriceBook).mockImplementation(async () => ({ ...detail, version: { ...detail.version, status: 'VOIDED' } })) vi.mocked(api.publishPlatformPriceBook).mockImplementation(async () => ({ ...detail, version: { ...detail.version, status: 'ACTIVE' } })) }) describe('ship price book dialogs', () => { it.each(['0', customerId])('uploads the original XLSX in scope %s then navigates to formal detail', async scope => { const wrapper = await upload() expect(wrapper.findComponent(AppDialog).props()).toMatchObject({ title: '上传船期价格表', width: 'min(560px, calc(100vw - 28px))' }) expect(document.querySelector('[aria-label="上传价格表资料"]')?.textContent).toContain('报价范围') wrapper.findComponent(ElSelect).vm.$emit('update:modelValue', scope); await flushPromises() const file = new File(['xlsx'], '完整价格表名称'.repeat(15) + '.xlsx') await choose(wrapper, file) expect(document.querySelector('.upload-file-name')?.textContent).toBe(file.name) expect(button('上传并解析', '.el-dialog__footer button').disabled).toBe(false) button('上传并解析', '.el-dialog__footer button').click(); await flushPromises() expect(api.uploadPlatformPriceBook).toHaveBeenCalledWith(file, scope) expect(push).toHaveBeenCalledWith({ name: 'fba-carrier-rate-detail', params: { versionId: privateId } }) expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(false) }) it.each(['extension','size'] as const)('invalid %s replacement clears the selected file and disables upload', async invalid => { const wrapper = await upload(); await choose(wrapper, new File(['xlsx'], 'valid.xlsx')) const file = new File(['bad'], invalid === 'extension' ? 'invalid.csv' : 'large.xlsx') if (invalid === 'size') Object.defineProperty(file, 'size', { value: 10 * 1024 * 1024 + 1 }) await choose(wrapper, file) expect(document.querySelector('.el-dialog .el-alert--error')?.textContent).toContain(invalid === 'extension' ? 'XLSX' : '10MB') expect(button('上传并解析', '.el-dialog__footer button').disabled).toBe(true) expect(document.querySelector('.upload-file-name')?.textContent).toBe('尚未选择文件') expect(api.uploadPlatformPriceBook).not.toHaveBeenCalled() }) it('locks upload controls and close paths while processing and retains context on failure', async () => { let rejectUpload!: (error: Error) => void vi.mocked(api.uploadPlatformPriceBook).mockImplementation(() => new Promise((_resolve,reject) => { rejectUpload = reject })) const wrapper = await upload(); wrapper.findComponent(ElSelect).vm.$emit('update:modelValue', customerId); await flushPromises() const file = new File(['xlsx'], 'retry.xlsx'); await choose(wrapper, file) button('上传并解析', '.el-dialog__footer button').click(); await flushPromises() expect(wrapper.findComponent(ElDialog).props()).toMatchObject({ showClose: false, closeOnPressEscape: false, closeOnClickModal: false }) expect(button('取消', '.el-dialog__footer button').disabled).toBe(true) expect(wrapper.findComponent(ElSelect).props('disabled')).toBe(true) expect(wrapper.get('input[type="file"]').attributes('disabled')).toBeDefined() rejectUpload(new Error('解析失败')); await flushPromises() expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(true) expect(wrapper.findComponent(ElSelect).props('modelValue')).toBe(customerId) expect(document.querySelector('.upload-file-name')?.textContent).toBe(file.name) expect(push).not.toHaveBeenCalled() vi.mocked(api.uploadPlatformPriceBook).mockResolvedValue(detail) button('上传并解析', '.el-dialog__footer button').click(); await flushPromises() expect(api.uploadPlatformPriceBook).toHaveBeenLastCalledWith(file, customerId) }) it('identifies void scope/version and validates the original required reason before writing', async () => { detail.version.customerId = customerId await list(); button('作废', '.table-row button').click(); await flushPromises() expect(document.querySelector('.el-message-box__title')?.textContent).toBe('作废待确认价格表 · V7 · 客户专属 · 测试客户') expect(document.querySelector('.el-message-box')?.classList.contains('admin-density-confirm')).toBe(true) button('确认作废', '.el-message-box button').click(); await flushPromises() expect(api.voidPlatformPriceBook).not.toHaveBeenCalled() setReason('x'.repeat(501)); await flushPromises(); button('确认作废', '.el-message-box button').click(); await flushPromises() expect(api.voidPlatformPriceBook).not.toHaveBeenCalled() setReason(' 测试作废原因 '); await flushPromises(); button('确认作废', '.el-message-box button').click(); await flushPromises() expect(api.voidPlatformPriceBook).toHaveBeenCalledWith(privateId, '测试作废原因') }) it('cancels void without sending a write', async () => { await list(); button('作废', '.table-row button').click(); await flushPromises() button('取消', '.el-message-box button').click(); await flushPromises() expect(api.voidPlatformPriceBook).not.toHaveBeenCalled() }) it.each(['0', customerId])('identifies publish scope %s and submits the original complete warehouse mapping after confirmation', async scope => { detail.version.customerId = scope await detailView() expect(button('确认并立即生效').disabled).toBe(false) button('确认并立即生效').click(); await flushPromises() const scopeText = scope === '0' ? '平台公共报价' : '客户专属 · 测试客户' const box = document.querySelector('.el-message-box')! expect(box.querySelector('.el-message-box__title')?.textContent).toBe(`确认并生效船期价格表 · V7 · ${scopeText}`) expect(box.textContent).toContain('同一报价范围'); expect(box.textContent).not.toContain(privateId); expect(box.textContent).not.toContain(customerId) expect(api.publishPlatformPriceBook).not.toHaveBeenCalled() button('确认并立即生效', '.el-message-box button').click(); await flushPromises() expect(api.publishPlatformPriceBook).toHaveBeenCalledWith(privateId, { originMappings: [{ originWarehouseCode: 'CN-TEST', warehouseId: domesticId }], overseasMappings: [{ sourceDestinationName: '海外测试仓', warehouseId: overseasId }] }) expect(document.querySelector('.density-detail-title')?.textContent).toBe('船期价格表 · V7') expect(document.querySelectorAll('.el-dialog')).toHaveLength(0) }) it('keeps publish disabled for a parsing blocker and never opens confirmation', async () => { detail.issues = [{ severity: 'BLOCKING', issueCode: 'INVALID_PRICE', message: '原文件价格无效', sourceSheetName: 'Sheet1', sourceRowNumber: 8, sourceCell: 'C8' }] await detailView() expect(button('确认并立即生效').disabled).toBe(true) expect(document.querySelector('.el-message-box')).toBeNull() expect(api.publishPlatformPriceBook).not.toHaveBeenCalled() }) it('hides upload and void for a list-only account', async () => { useAuthStore().permissions = ['firstleg:fba-pricing:rate:view']; useAccessPreviewStore().initializeAfterLogin() const wrapper = await list() expect(wrapper.findAll('button').map(node => node.text())).toEqual(['查看详情','原文件']) expect(api.uploadPlatformPriceBook).not.toHaveBeenCalled(); expect(api.voidPlatformPriceBook).not.toHaveBeenCalled() }) })