import { createRequire } from 'node:module' 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 { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork' import AppDialog from '@/shared/components/AppDialog.vue' import FeeItemsView from './FeeItemsView.vue' import * as api from '../api/platformFeeItems' // Preserve the validator constructor interop through Element Plus's supported Node export. const { default: ElementPlus, ElCheckbox, ElDialog, ElInputNumber, ElSelect } = createRequire(import.meta.url)('element-plus') as typeof import('element-plus') vi.mock('../api/platformFeeItems', () => ({ listPlatformFeeItems: vi.fn(), getPlatformFeeItem: vi.fn(), createPlatformFeeItem: vi.fn(), updatePlatformFeeItem: vi.fn(), changePlatformFeeItemStatus: vi.fn(), deletePlatformFeeItem: vi.fn() })) let record: api.PlatformFeeItemResource const privateId = '9223372036854775807' const table = { props: ['data'], template: '
' } 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! } async function openView() { const wrapper = mount(FeeItemsView, { attachTo: document.body, global: { plugins: [ElementPlus], stubs: { DataTable: table, QueryToolbar: true } } }); await flushPromises(); return wrapper } async function openEditor(create = false) { const wrapper = await openView(); button(create ? '新增费用项目' : '编辑').click(); await flushPromises(); return wrapper } async function setText(placeholder: string, value: string) { const input = document.querySelector(`.el-dialog [placeholder="${placeholder}"]`)! input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); await flushPromises() } async function check(wrapper: Awaited>, label: string, value: boolean) { const checkbox = wrapper.findAllComponents(ElCheckbox).find(node => node.props('ariaLabel') === label)! expect(checkbox, label).toBeDefined(); await checkbox.get('input').setValue(value); await flushPromises() } const currentDialog = (wrapper: Awaited>) => wrapper.findAllComponents(AppDialog).find(dialog => dialog.props('modelValue'))! enableAutoUnmount(afterEach) beforeEach(() => { vi.resetAllMocks(); document.body.innerHTML = ''; setActivePinia(createPinia()) const auth = useAuthStore(); auth.token = 'test-session'; auth.permissions = ['firstleg:fee-item:view','firstleg:fee-item:create','firstleg:fee-item:edit','firstleg:fee-item:delete'] useAccessPreviewStore().initializeAfterLogin() record = { id: privateId, category: '操作费', tradeScope: 'DOMESTIC', feeCode: 'FEE-TEST', supplierCategory: null, feeTypeName: '测试操作费', defaultPrice: 0, pickupPayable: false, pickupReceivable: true, warehousePayable: false, warehouseReceivable: true, allocationFee: false, description: '适用说明', status: 'ACTIVE' } vi.mocked(api.listPlatformFeeItems).mockImplementation(async () => ({ records: [record], page: 1, size: 20, total: 1, pages: 1 })) vi.mocked(api.getPlatformFeeItem).mockImplementation(async () => record) vi.mocked(api.createPlatformFeeItem).mockImplementation(async () => record) vi.mocked(api.updatePlatformFeeItem).mockImplementation(async () => record) vi.mocked(api.changePlatformFeeItemStatus).mockResolvedValue(null) vi.mocked(api.deletePlatformFeeItem).mockResolvedValue(null) }) describe('fee item editing and confirmations', () => { it('creates from a centered white dialog with a zero default and all direction flags', async () => { const wrapper = await openEditor(true) expect(wrapper.findComponent(AppDialog).props()).toMatchObject({ title: '新增费用项目', width: 'min(760px, 94vw)' }) expect(document.querySelector('.el-dialog')?.classList.contains('admin-density-dialog')).toBe(true) expect(document.querySelectorAll('.el-dialog .density-dialog-section')).toHaveLength(3) expect(document.querySelector('.fee-item-editor .density-dialog-section[data-tone]')).toBeNull() expect(wrapper.findComponent(ElDialog).props('alignCenter')).toBe(true) expect(document.querySelector('.el-drawer')).toBeNull() await setText('请输入费用名称', ' 新增操作费 '); await setText('例如 仓储成本', ' 操作费 ') wrapper.findComponent(ElInputNumber).vm.$emit('update:modelValue', 0) await check(wrapper, '提货环节应付', true); await check(wrapper, '提货环节应收', true) await check(wrapper, '仓库环节应付', true); await check(wrapper, '仓库环节应收', true) wrapper.findComponent(ElSelect).vm.$emit('update:modelValue', 'TRUCKING') await wrapper.findAllComponents(ElCheckbox).find(node => node.text() === '作为分摊费用')!.get('input').setValue(true) await setText('请输入适用场景或核算说明', ' 新说明 ') button('保存费用项目', '.el-dialog__footer button').click(); await flushPromises() expect(api.createPlatformFeeItem).toHaveBeenCalledWith({ category: '操作费', tradeScope: 'DOMESTIC', feeTypeName: '新增操作费', defaultPrice: 0, supplierCategory: 'TRUCKING', pickupPayable: true, pickupReceivable: true, warehousePayable: true, warehouseReceivable: true, allocationFee: true, description: '新说明' }) expect(useUnsavedWorkStore().entries).toHaveLength(0) }) it('validates required fields and payable supplier category without losing entered values', async () => { const wrapper = await openEditor(true) button('保存费用项目', '.el-dialog__footer button').click(); await flushPromises() expect(api.createPlatformFeeItem).not.toHaveBeenCalled() await vi.waitFor(() => expect(document.querySelector('.el-dialog')?.textContent).toContain('请输入费用名称')) await setText('请输入费用名称', '新费用'); await setText('例如 仓储成本', '操作费') await check(wrapper, '提货环节应付', true) button('保存费用项目', '.el-dialog__footer button').click(); await flushPromises() expect(api.createPlatformFeeItem).not.toHaveBeenCalled() expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(true) expect(document.querySelector('.el-dialog [placeholder="请输入费用名称"]')!.value).toBe('新费用') }) it('edits the same long ID and clears the supplier payload when payable directions are removed', async () => { record = { ...record, pickupPayable: true, supplierCategory: 'TRUCKING', defaultPrice: 25 } const wrapper = await openEditor() expect(wrapper.findComponent(AppDialog).props('title')).toBe('编辑费用项目 · 测试操作费') expect(document.querySelector('.el-dialog')?.textContent).not.toContain(privateId) wrapper.findComponent(ElInputNumber).vm.$emit('update:modelValue', null) await check(wrapper, '提货环节应付', false) expect(document.querySelector('.el-dialog [placeholder="请选择供应商类别"]')).toBeNull() button('保存费用项目', '.el-dialog__footer button').click(); await flushPromises() expect(api.updatePlatformFeeItem).toHaveBeenCalledWith(privateId, expect.objectContaining({ defaultPrice: null, supplierCategory: null, pickupPayable: false })) }) it('locks every close path during save and preserves the dirty draft after rejection', async () => { let rejectSave!: (error: Error) => void vi.mocked(api.updatePlatformFeeItem).mockImplementation(() => new Promise((_resolve,reject) => { rejectSave = reject })) const wrapper = await openEditor(); await setText('请输入费用名称', '修改后费用') expect(useUnsavedWorkStore().entries[0]).toMatchObject({ key: 'fee-item-editor', dirty: true, requiredPermission: 'firstleg:fee-item:edit' }) 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) rejectSave(new Error('保存失败')); await flushPromises() expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(true) expect(document.querySelector('.el-dialog [placeholder="请输入费用名称"]')!.value).toBe('修改后费用') expect(useUnsavedWorkStore().entries[0]?.dirty).toBe(true) vi.mocked(api.updatePlatformFeeItem).mockResolvedValue(record) button('保存费用项目', '.el-dialog__footer button').click(); await flushPromises() expect(api.updatePlatformFeeItem).toHaveBeenCalledTimes(2) expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(false) }) it('keeps unsaved registration and the existing permission-change closure', async () => { const wrapper = await openEditor() expect(useUnsavedWorkStore().entries[0]?.dirty).toBe(false) await setText('请输入费用名称', '权限变化前的修改') expect(useUnsavedWorkStore().entries[0]?.dirty).toBe(true) useAuthStore().permissions = ['firstleg:fee-item:view']; useAccessPreviewStore().initializeAfterLogin(); await flushPromises() expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(false) expect(useUnsavedWorkStore().entries).toHaveLength(0) expect(api.updatePlatformFeeItem).not.toHaveBeenCalled() }) it.each(['ACTIVE','DISABLED'] as const)('identifies the %s status confirmation and retains original status requests', async status => { record.status = status const verb = status === 'ACTIVE' ? '停用' : '启用' const wrapper = await openView(); button(verb, '.real-row button').click(); await flushPromises() expect(currentDialog(wrapper).props('title')).toBe(`${verb}费用项目 · 测试操作费`) expect(currentDialog(wrapper).props('confirmType')).toBe(status === 'ACTIVE' ? 'warning' : 'success') expect(api.changePlatformFeeItemStatus).not.toHaveBeenCalled() button(`确认${verb}`, '.el-dialog__footer button').click(); await flushPromises() expect(api.changePlatformFeeItemStatus).toHaveBeenCalledWith(privateId, status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE') }) it('keeps a server-rejected delete confirmation open with its original target', async () => { vi.mocked(api.deletePlatformFeeItem).mockRejectedValueOnce(new Error('项目已使用')).mockResolvedValueOnce(null) const wrapper = await openView(); button('删除', '.real-row button').click(); await flushPromises() expect(currentDialog(wrapper).props()).toMatchObject({ title: '删除费用项目 · 测试操作费', confirmType: 'danger' }) expect(document.querySelector('.el-dialog')?.textContent).toContain('已被业务单据使用') button('确认删除', '.el-dialog__footer button').click(); await flushPromises() expect(currentDialog(wrapper).props('modelValue')).toBe(true) button('确认删除', '.el-dialog__footer button').click(); await flushPromises() expect(api.deletePlatformFeeItem).toHaveBeenNthCalledWith(2, privateId) expect(wrapper.findAllComponents(AppDialog).every(dialog => !dialog.props('modelValue'))).toBe(true) }) it('cancels a confirmation without changing status or deleting data', async () => { const wrapper = await openView(); button('停用', '.real-row button').click(); await flushPromises() button('取消', '.el-dialog__footer button').click(); await flushPromises() expect(wrapper.findAllComponents(AppDialog).every(dialog => !dialog.props('modelValue'))).toBe(true) expect(api.changePlatformFeeItemStatus).not.toHaveBeenCalled(); expect(api.deletePlatformFeeItem).not.toHaveBeenCalled() }) })