Files
admin-web/src/modules/fee-items/pages/FeeItemDialogs.test.ts
T

156 lines
12 KiB
TypeScript

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 AppDrawer from '@/shared/components/AppDrawer.vue'
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, ElDrawer, 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: '<div><slot name="toolbar-actions"/><div class="real-row"><slot v-if="data?.length" name="actions" :row="data[0]"/></div></div>' }
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!
}
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<HTMLInputElement>(`.el-drawer [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<ReturnType<typeof openView>>, 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<ReturnType<typeof openView>>) => 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 the original compact drawer with a zero default and all direction flags', async () => {
const wrapper = await openEditor(true)
expect(wrapper.findComponent(AppDrawer).props()).toMatchObject({ title: '新增费用项目', size: 'min(620px, 94vw)' })
expect(document.querySelector('.el-drawer')?.classList.contains('admin-density-drawer--editor')).toBe(true)
expect(document.querySelectorAll('.el-drawer .density-dialog-section')).toHaveLength(3)
expect(document.querySelector('.el-dialog')).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-drawer__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-drawer__footer button').click(); await flushPromises()
expect(api.createPlatformFeeItem).not.toHaveBeenCalled()
await vi.waitFor(() => expect(document.querySelector('.el-drawer')?.textContent).toContain('请输入费用名称'))
await setText('请输入费用名称', '新费用'); await setText('例如 仓储成本', '操作费')
await check(wrapper, '提货环节应付', true)
button('保存费用项目', '.el-drawer__footer button').click(); await flushPromises()
expect(api.createPlatformFeeItem).not.toHaveBeenCalled()
expect(wrapper.findComponent(AppDrawer).props('modelValue')).toBe(true)
expect(document.querySelector<HTMLInputElement>('.el-drawer [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(AppDrawer).props('title')).toBe('编辑费用项目 · 测试操作费')
expect(document.querySelector('.el-drawer')?.textContent).not.toContain(privateId)
wrapper.findComponent(ElInputNumber).vm.$emit('update:modelValue', null)
await check(wrapper, '提货环节应付', false)
expect(document.querySelector('.el-drawer [placeholder="请选择供应商类别"]')).toBeNull()
button('保存费用项目', '.el-drawer__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-drawer__footer button').click(); await flushPromises()
expect(wrapper.findComponent(ElDrawer).props()).toMatchObject({ showClose: false, closeOnPressEscape: false, closeOnClickModal: false })
expect(button('取消', '.el-drawer__footer button').disabled).toBe(true)
rejectSave(new Error('保存失败')); await flushPromises()
expect(wrapper.findComponent(AppDrawer).props('modelValue')).toBe(true)
expect(document.querySelector<HTMLInputElement>('.el-drawer [placeholder="请输入费用名称"]')!.value).toBe('修改后费用')
expect(useUnsavedWorkStore().entries[0]?.dirty).toBe(true)
vi.mocked(api.updatePlatformFeeItem).mockResolvedValue(record)
button('保存费用项目', '.el-drawer__footer button').click(); await flushPromises()
expect(api.updatePlatformFeeItem).toHaveBeenCalledTimes(2)
expect(wrapper.findComponent(AppDrawer).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(AppDrawer).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()
})
})