197 lines
14 KiB
TypeScript
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)
|
|
})
|
|
})
|