Files
admin-web/src/modules/overseas-master-data/pages/PricingDialogs.test.ts
T

197 lines
14 KiB
TypeScript

import ElementPlus, { ElDatePicker, ElDialog, ElInputNumber, ElMessageBox, ElSelect } from 'element-plus'
import { createPinia, setActivePinia } from 'pinia'
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
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 ExchangeRatesView from './ExchangeRatesView.vue'
import OverseasQuotesView from './OverseasQuotesView.vue'
import * as api from '../api/overseasMasterData'
import { overseasMasterDataPermissions as permission } from '../permissions'
import type { MonthlyExchangeRate, OverseasWarehouseQuoteDetail } from '../types'
vi.mock('@/modules/downloads', () => ({ useExportTask: () => vi.fn() }))
vi.mock('../api/overseasMasterData', () => ({
listQuotes: vi.fn(), listQuoteWarehouseOptions: vi.fn(), getQuote: vi.fn(), createQuote: vi.fn(), updateQuote: vi.fn(), changeQuoteStatus: vi.fn(),
listExchangeRates: vi.fn(), getExchangeRateSummary: vi.fn(), getExchangeRate: vi.fn(), createExchangeRate: vi.fn(), updateExchangeRate: vi.fn(), changeExchangeRateStatus: vi.fn(),
}))
const privateId = '9223372036854775807'
let detail: OverseasWarehouseQuoteDetail
let rate: MonthlyExchangeRate
const tableStub = { props: ['data'], template: '<div><slot name="toolbar-actions"/><div class="real-row"><slot v-if="data?.length" name="actions" :row="data[0]"/></div></div>' }
function mountView(component: typeof ExchangeRatesView | typeof OverseasQuotesView) {
return mount(component, { attachTo: document.body, global: { plugins: [ElementPlus], stubs: { DataTable: tableStub, QueryToolbar: true } } })
}
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!
}
const activeDialog = (wrapper: ReturnType<typeof mountView>) => wrapper.findAllComponents(AppDialog).find(dialog => dialog.props('modelValue'))!
async function openEditor(component: typeof ExchangeRatesView | typeof OverseasQuotesView, label = '编辑') {
const wrapper = mountView(component); await flushPromises()
button(label).click(); await flushPromises()
return wrapper
}
enableAutoUnmount(afterEach)
afterEach(() => ElMessageBox.close())
beforeEach(() => {
vi.resetAllMocks()
document.body.innerHTML = ''
setActivePinia(createPinia())
const auth = useAuthStore(); auth.token = 'test-session'; auth.permissions = Object.values(permission)
useAccessPreviewStore().initializeAfterLogin()
detail = { summary: { id: privateId, name: '报价测试', warehouseId: '9223372036854775806', warehouseName: '合作仓', warehouseCode: 'US-LAX', countryCode: 'US', providerName: '服务商', currency: 'USD', effectiveStart: '2026-10-01', effectiveEnd: '2026-12-31', versionCode: 'V2', status: 'ACTIVE', updatedAt: '', updatedBy: '测试员', recordVersion: 9, remark: '原备注' }, warehouseFees: [{ item: '卸货', billingUnit: '托', unitPrice: 0, minimumCharge: null, remark: '保留费用备注' }], transitPrices: [{ fbaWarehouseCode: 'LAX9', deliveryMethod: '卡车', estimatedMiles: 12.5, price: 20, minimumCharge: 0, remark: '保留派送备注' }], createdBy: '测试员', createdAt: '' }
rate = { month: '2026-10', baseCurrency: 'USD', quoteCurrency: 'CNY', usdToCnyAverage: 7.12345678, status: 'ACTIVE', recordVersion: 4, updatedBy: '测试员', updatedAt: '', remark: '汇率备注' }
vi.mocked(api.listQuotes).mockImplementation(async () => ({ records: [detail.summary], total: 1, pages: 1, page: 1, size: 20 }))
vi.mocked(api.listQuoteWarehouseOptions).mockResolvedValue([])
vi.mocked(api.getQuote).mockImplementation(async () => detail)
vi.mocked(api.createQuote).mockImplementation(async () => detail)
vi.mocked(api.updateQuote).mockImplementation(async () => detail)
vi.mocked(api.changeQuoteStatus).mockImplementation(async () => detail)
vi.mocked(api.listExchangeRates).mockImplementation(async () => ({ records: [rate], total: 1, pages: 1, page: 1, size: 20 }))
vi.mocked(api.getExchangeRateSummary).mockResolvedValue({ latestMonth: null, latestUsdToCnyAverage: null, updatedAt: null, updatedBy: null })
vi.mocked(api.getExchangeRate).mockImplementation(async () => rate)
vi.mocked(api.createExchangeRate).mockImplementation(async () => rate)
vi.mocked(api.updateExchangeRate).mockImplementation(async () => rate)
vi.mocked(api.changeExchangeRateStatus).mockImplementation(async () => rate)
})
describe('exchange rate and overseas quote dialogs', () => {
it('edits the original monthly version with a readonly month and eight decimal places', async () => {
const wrapper = await openEditor(ExchangeRatesView)
const dialog = activeDialog(wrapper)
expect(dialog.props('title')).toBe('编辑月度汇率 · 2026-10')
expect(dialog.findComponent(ElDatePicker).exists()).toBe(false)
expect(document.querySelector('[aria-label="月度汇率资料"]')?.textContent).toContain('2026-10')
expect(dialog.findComponent(ElInputNumber).props('precision')).toBe(8)
const remark = document.querySelector<HTMLTextAreaElement>('.el-dialog textarea')!
remark.value = '修改后的备注'; remark.dispatchEvent(new Event('input', { bubbles: true })); await flushPromises()
button('确定', '.el-dialog__footer button').click(); await flushPromises()
expect(api.updateExchangeRate).toHaveBeenCalledWith('2026-10', { recordVersion: 4, usdToCnyAverage: 7.12345678, remark: '修改后的备注' })
expect(api.createExchangeRate).not.toHaveBeenCalled()
})
it('keeps month selection on create and validates a positive average before writing', async () => {
const wrapper = await openEditor(ExchangeRatesView, '更新月度汇率')
const dialog = activeDialog(wrapper)
expect(dialog.findComponent(ElDatePicker).exists()).toBe(true)
button('确定', '.el-dialog__footer button').click(); await flushPromises()
expect(api.createExchangeRate).not.toHaveBeenCalled()
dialog.findComponent(ElDatePicker).vm.$emit('update:modelValue', '2026-11')
dialog.findComponent(ElInputNumber).vm.$emit('update:modelValue', 7.00000001)
await flushPromises()
button('确定', '.el-dialog__footer button').click(); await flushPromises()
expect(api.createExchangeRate).toHaveBeenCalledWith({ month: '2026-11', usdToCnyAverage: 7.00000001, remark: null })
})
it('preserves quote edits and hidden detail values across fullscreen and sends only update fields', async () => {
const wrapper = await openEditor(OverseasQuotesView)
const dialog = activeDialog(wrapper)
expect(dialog.props()).toMatchObject({ title: '编辑海外仓报价 · 报价测试', fullscreenable: true })
const base = document.querySelector('[aria-label="报价基础信息"]')!
expect(base.textContent).toContain('合作仓 · US-LAX')
expect(base.textContent).toContain('V2')
expect(base.textContent).not.toContain(privateId)
expect(dialog.findAllComponents(ElSelect)).toHaveLength(1)
const price = document.querySelector<HTMLInputElement>('[aria-label="第1项费用单价"]')!
price.value = '1.25'; price.dispatchEvent(new Event('input', { bubbles: true })); price.dispatchEvent(new Event('change', { bubbles: true })); await flushPromises()
document.querySelector<HTMLButtonElement>('[aria-label="展开全屏"]')!.click(); await flushPromises()
expect(dialog.findComponent(ElDialog).props('fullscreen')).toBe(true)
expect(document.querySelector<HTMLInputElement>('[aria-label="第1项费用单价"]')!.value).toBe('1.25')
button('保存报价', '.el-dialog__footer button').click(); await flushPromises()
expect(api.updateQuote).toHaveBeenCalledWith(privateId, expect.objectContaining({ recordVersion: 9, warehouseFees: [{ ...detail.warehouseFees[0], unitPrice: 1.25 }], transitPrices: detail.transitPrices }))
const payload = vi.mocked(api.updateQuote).mock.calls[0]![1]
expect(payload).not.toHaveProperty('warehouseId'); expect(payload).not.toHaveProperty('versionCode')
expect(detail.warehouseFees[0]!.unitPrice).toBe(0)
})
it('retains draft on save failure and locks close and field edits during submission', async () => {
let rejectSave!: (cause: Error) => void
vi.mocked(api.updateQuote).mockImplementation(() => new Promise((_resolve, reject) => { rejectSave = reject }))
const wrapper = await openEditor(OverseasQuotesView)
const dialog = activeDialog(wrapper)
button('保存报价', '.el-dialog__footer button').click(); await flushPromises()
expect(dialog.findComponent(ElDialog).props()).toMatchObject({ showClose: false, closeOnPressEscape: false, closeOnClickModal: false })
expect(document.querySelector<HTMLButtonElement>('.el-dialog__footer button')!.disabled).toBe(true)
expect(document.querySelector<HTMLInputElement>('.el-dialog input')!.disabled).toBe(true)
rejectSave(new Error('保存失败')); await flushPromises()
expect(dialog.props('modelValue')).toBe(true)
expect(document.querySelector<HTMLInputElement>('[aria-label="第1项费用项目"]')!.value).toBe('卸货')
expect(dialog.findComponent(ElDialog).props('showClose')).toBe(true)
})
it('keeps create warehouse and version selectable and original add/delete defaults', async () => {
const wrapper = await openEditor(OverseasQuotesView, '新增报价')
const dialog = activeDialog(wrapper)
expect(dialog.findAllComponents(ElSelect)).toHaveLength(2)
expect(document.querySelector<HTMLInputElement>('[placeholder="请输入版本号"]')!.value).toBe('V1.0')
expect(document.querySelectorAll('[aria-label="编辑仓储作业费用"] tbody tr')).toHaveLength(1)
button('+ 添加费用').click(); button('+ 添加派送价格').click(); await flushPromises()
expect(document.querySelectorAll('[aria-label="编辑仓储作业费用"] tbody tr')).toHaveLength(2)
expect(document.querySelector<HTMLInputElement>('[aria-label="第1项派送报价"]')!.value).toBe('0')
document.querySelector<HTMLButtonElement>('[aria-label="删除第2项费用"]')!.click(); await flushPromises()
expect(document.querySelectorAll('[aria-label="编辑仓储作业费用"] tbody tr')).toHaveLength(1)
button('保存报价', '.el-dialog__footer button').click(); await flushPromises()
expect(api.createQuote).not.toHaveBeenCalled()
})
it('shows full detail facts, translated status, zero and missing values without private IDs', async () => {
detail.summary.effectiveEnd = null
const wrapper = await openEditor(OverseasQuotesView, '详情')
const dialog = activeDialog(wrapper)
expect(dialog.props()).toMatchObject({ title: '海外仓报价详情 · 报价测试', fullscreenable: true, showConfirm: false, width: 'min(980px, calc(100vw - 28px))' })
const summary = document.querySelector('[aria-label="报价摘要"]')!
expect(summary.textContent).toContain('启用'); expect(summary.textContent).toContain('—'); expect(summary.textContent).not.toContain('ACTIVE'); expect(summary.textContent).not.toContain(privateId)
const fees = document.querySelector('[aria-label="仓储作业费用"] .el-table__body')!
expect(fees.textContent).toContain('保留费用备注')
expect(fees.querySelectorAll('td')[2]!.textContent).toBe('0')
expect(fees.querySelectorAll('td')[3]!.textContent).toBe('—')
const transit = document.querySelector('[aria-label="中转派送价格"] .el-table__body')!
expect(transit.querySelectorAll('td')[2]!.textContent).toBe('12.5')
expect(transit.querySelectorAll('td')[4]!.textContent).toBe('0')
document.querySelector<HTMLButtonElement>('[aria-label="展开全屏"]')!.click(); await flushPromises()
expect(dialog.findComponent(ElDialog).props('fullscreen')).toBe(true)
expect(api.updateQuote).not.toHaveBeenCalled()
})
it('shows compact empty detail sections and complete long names', async () => {
detail.summary.name = '完整报价名称'.repeat(30); detail.summary.status = 'INACTIVE'
detail.warehouseFees = []; detail.transitPrices = []
await openEditor(OverseasQuotesView, '详情')
expect(document.querySelector('[aria-label="报价摘要"]')?.textContent).toContain(detail.summary.name)
expect(document.querySelector('[aria-label="报价摘要"]')?.textContent).toContain('停用')
expect(document.querySelector('.el-dialog')?.textContent).toContain('暂无仓储作业费用')
expect(document.querySelector('.el-dialog')?.textContent).toContain('暂无中转派送价格')
})
it.each(['ACTIVE', 'INACTIVE'] as const)('identifies the %s rate status change and waits for confirmation', async status => {
rate.status = status
const verb = status === 'ACTIVE' ? '停用' : '启用'
mountView(ExchangeRatesView); await flushPromises(); button(verb, '.real-row button').click(); await flushPromises()
expect(document.querySelector('.el-message-box__title')?.textContent).toBe(`${verb}月度汇率 · 2026-10`)
expect(api.changeExchangeRateStatus).not.toHaveBeenCalled()
document.querySelector<HTMLButtonElement>('.el-message-box__btns .el-button--primary')!.click(); await flushPromises()
expect(api.changeExchangeRateStatus).toHaveBeenCalledWith('2026-10', status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE', 4, null)
})
it.each(['ACTIVE', 'INACTIVE'] as const)('identifies the %s quote status change and waits for confirmation', async status => {
detail.summary.status = status
const verb = status === 'ACTIVE' ? '停用' : '启用'
mountView(OverseasQuotesView); await flushPromises(); button(verb, '.real-row button').click(); await flushPromises()
expect(document.querySelector('.el-message-box__title')?.textContent).toBe(`${verb}海外仓报价 · 报价测试`)
expect(api.changeQuoteStatus).not.toHaveBeenCalled()
document.querySelector<HTMLButtonElement>('.el-message-box__btns .el-button--primary')!.click(); await flushPromises()
expect(api.changeQuoteStatus).toHaveBeenCalledWith(privateId, status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE', 9)
})
it('keeps quote details available for list-only access without write entries', async () => {
useAuthStore().permissions = [permission.quoteList]; useAccessPreviewStore().initializeAfterLogin()
const wrapper = mountView(OverseasQuotesView); await flushPromises()
expect(wrapper.findAll('button').map(node => node.text())).toEqual(['详情'])
button('详情').click(); await flushPromises()
expect(activeDialog(wrapper).props('showConfirm')).toBe(false)
})
})