Files
admin-web/src/modules/fba-pricing/carrier-rates/pages/PriceBookDialogs.test.ts
T

162 lines
13 KiB
TypeScript

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: '<div><slot name="toolbar-actions"/><div class="table-row"><slot v-if="data?.length" name="actions" :row="data[0]"/></div></div>' }
const plugins = [ElementPlus]
function button(label: string, selector = 'button') {
const found = Array.from(document.querySelectorAll<HTMLButtonElement>(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<ReturnType<typeof upload>>, 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<HTMLInputElement>('.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()
})
})