import ElementPlus, { ElDialog, ElMessageBox, ElOption } 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 CustomerOverseasStorageFeePanel from './CustomerOverseasStorageFeePanel.vue' import OverseasStorageFeesView from './pages/OverseasStorageFeesView.vue' import { getCustomerOverseasStorageFees, listOverseasStorageFees, updateCustomerOverseasStorageFees, type OverseasStorageFeeConfig } from './overseasStorageFee' vi.mock('./overseasStorageFee', () => ({ getCustomerOverseasStorageFees: vi.fn(), listOverseasStorageFees: vi.fn(), updateCustomerOverseasStorageFees: vi.fn(), })) const customerId = '9007199254740993' function configuration(): OverseasStorageFeeConfig { return { id: 'fee-1', customerId, customerName: '费用测试客户', currency: 'USD', version: 7, createdAt: null, updatedAt: null, tiers: [ { id: 'storage', feeType: 'STORAGE', billingUnit: 'CBM', startDay: 1, endDay: null, unitPrice: 0.123456, sortNo: 1 }, { id: 'return', feeType: 'RETURN_STORAGE', billingUnit: 'PALLET', startDay: 1, endDay: null, unitPrice: 2, sortNo: 1 }, ] } } function button(text: string) { const found = [...document.querySelectorAll('button')].find(item => item.textContent?.trim() === text) expect(found, text).toBeDefined() return found! } function changeNumber(input: HTMLInputElement, value: string) { input.value = value input.dispatchEvent(new Event('input', { bubbles: true })) input.dispatchEvent(new Event('change', { bubbles: true })) input.dispatchEvent(new Event('blur', { bubbles: true })) } async function open(entry: 'detail' | 'list') { const wrapper = entry === 'detail' ? mount(CustomerOverseasStorageFeePanel, { attachTo: document.body, props: { customerId, customerName: '费用测试客户' }, global: { plugins: [ElementPlus] } }) : mount(OverseasStorageFeesView, { attachTo: document.body, global: { plugins: [ElementPlus] } }) await flushPromises() if (entry === 'detail') button('编辑费用').click() else { const action = document.querySelector('.el-table__body .row-actions button') expect(action).not.toBeNull() action!.click() } await flushPromises() return wrapper } enableAutoUnmount(afterEach) afterEach(() => { vi.restoreAllMocks(); ElMessageBox.close() }) beforeEach(() => { vi.resetAllMocks() document.body.innerHTML = '' setActivePinia(createPinia()) const auth = useAuthStore() auth.token = 'test-session' auth.permissions = ['platform:customer:list', 'platform:customer:edit', 'platform:overseas-storage-fee:edit'] useAccessPreviewStore().initializeAfterLogin() vi.mocked(getCustomerOverseasStorageFees).mockImplementation(async () => configuration()) vi.mocked(listOverseasStorageFees).mockImplementation(async () => ({ records: [configuration()], total: 1, page: 1, size: 20 })) vi.mocked(updateCustomerOverseasStorageFees).mockImplementation(async () => configuration()) }) describe('customer overseas fee dialog entry points', () => { it.each(['detail', 'list'] as const)('retains %s currency choices, values, version and full customer ID on save', async entry => { const wrapper = await open(entry) const dialog = wrapper.findComponent(AppDialog) expect(dialog.props('title')).toBe(`${entry === 'detail' ? '编辑海外仓费用' : '编辑海外仓阶梯费用'} · 费用测试客户`) expect(dialog.props('fullscreenable')).toBe(true) expect(dialog.findAllComponents(ElOption).map(option => option.props('value'))).toEqual(entry === 'detail' ? ['USD', 'CNY', 'HKD', 'EUR', 'GBP'] : ['USD', 'CNY', 'EUR']) const prices = document.querySelectorAll('.customer-fee-tier-table input[placeholder^="请输入"]') expect(prices).toHaveLength(4) const price = document.querySelector(entry === 'detail' ? 'input[placeholder="请输入价格"]' : 'input[placeholder="请输入单价"]')! changeNumber(price, '0.654321') await flushPromises() button('新增阶梯').click() await flushPromises() expect(document.querySelectorAll('.customer-fee-tier-table tbody tr')).toHaveLength(3) document.querySelector('button[aria-label="展开全屏"]')!.click() await flushPromises() expect(document.querySelector('.customer-density-dialog.is-fullscreen')).not.toBeNull() expect(price.value).toBe('0.654321') button(entry === 'detail' ? '保存费用' : '确定').click() await flushPromises() expect(updateCustomerOverseasStorageFees).toHaveBeenCalledWith(customerId, { customerId, customerName: '费用测试客户', currency: 'USD', version: 7, tiers: [ { feeType: 'STORAGE', billingUnit: 'CBM', startDay: 1, endDay: 1, unitPrice: 0.654321 }, { feeType: 'STORAGE', billingUnit: 'CBM', startDay: 2, endDay: null, unitPrice: 0.654321 }, { feeType: 'RETURN_STORAGE', billingUnit: 'PALLET', startDay: 1, endDay: null, unitPrice: 2 }, ], }) expect(dialog.props('modelValue')).toBe(false) }) it('applies dirty confirmation to the detail header close and preserves inputs when declined', async () => { const wrapper = await open('detail') const price = document.querySelector('input[placeholder="请输入价格"]')! changeNumber(price, '9.123456') await flushPromises() const confirmation = vi.spyOn(ElMessageBox, 'confirm').mockRejectedValueOnce('cancel') document.querySelector('.customer-density-dialog .el-dialog__headerbtn')!.click() await flushPromises() expect(confirmation).toHaveBeenCalledWith(expect.stringContaining('尚未保存'), '放弃编辑 · 费用测试客户', expect.objectContaining({ customClass: 'admin-density-confirm' })) expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(true) expect(price.value).toBe('9.123456') const done = vi.fn() const closing = wrapper.findComponent(AppDialog).props('beforeClose')!(done) await flushPromises() Array.from(document.querySelectorAll('.el-message-box .el-button--primary')).at(-1)!.click() await closing expect(done).toHaveBeenCalledOnce() button('取消').click() await flushPromises() Array.from(document.querySelectorAll('.el-message-box .el-button--primary')).at(-1)!.click() await flushPromises() expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(false) expect(updateCustomerOverseasStorageFees).not.toHaveBeenCalled() }) it.each(['detail', 'list'] as const)('keeps %s inputs and context when saving fails', async entry => { vi.mocked(updateCustomerOverseasStorageFees).mockRejectedValueOnce(new Error('费用版本冲突')) const wrapper = await open(entry) button(entry === 'detail' ? '保存费用' : '确定').click() await flushPromises() expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(true) expect(document.body.textContent).toContain(entry === 'detail' ? '海外仓费用保存失败,请刷新后重试' : '保存失败,请刷新后重试') expect(document.querySelectorAll('.customer-fee-tier-table tbody tr')).toHaveLength(2) }) it.each(['detail', 'list'] as const)('locks %s close actions during save', async entry => { let finish!: (value: OverseasStorageFeeConfig) => void vi.mocked(updateCustomerOverseasStorageFees).mockReturnValueOnce(new Promise(resolve => { finish = resolve })) const wrapper = await open(entry) button(entry === 'detail' ? '保存费用' : '确定').click() await flushPromises() expect(wrapper.findComponent(AppDialog).props('confirmLoading')).toBe(true) expect(wrapper.findComponent(ElDialog).props('showClose')).toBe(false) expect(wrapper.findComponent(ElDialog).props('closeOnPressEscape')).toBe(false) finish(configuration()) await flushPromises() expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(false) }) })