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: '
' } 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(selector)).find(node => node.textContent?.trim() === label) expect(found, label).toBeDefined() return found! } const activeDialog = (wrapper: ReturnType) => 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('.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('[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('[aria-label="展开全屏"]')!.click(); await flushPromises() expect(dialog.findComponent(ElDialog).props('fullscreen')).toBe(true) expect(document.querySelector('[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('.el-dialog__footer button')!.disabled).toBe(true) expect(document.querySelector('.el-dialog input')!.disabled).toBe(true) rejectSave(new Error('保存失败')); await flushPromises() expect(dialog.props('modelValue')).toBe(true) expect(document.querySelector('[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('[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('[aria-label="第1项派送报价"]')!.value).toBe('0') document.querySelector('[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('[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('.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('.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) }) })