156 lines
12 KiB
TypeScript
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()
|
|
})
|
|
})
|