Files
admin-web/src/modules/overseas-warehouse/components/SmartDispatchDialog.layout.test.ts
T

228 lines
21 KiB
TypeScript
Raw Normal View History

import ElementPlus from 'element-plus'
import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import SmartDispatchDialog from './SmartDispatchDialog.vue'
import DispatchResourceFields from './DispatchResourceFields.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { ApiResponseError } from '@/shared/api/http'
import { listAllPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses'
import { listDrivers, listVehicles, type DriverDetail, type VehicleDetail } from '@/modules/overseas-master-data'
import * as api from '../api/dispatches'
import { candidate, command, detail, sharedPalletCandidates } from '../dispatchTestFixtures'
2026-10-21 13:22:42 +08:00
import type { ConvertDraft, DispatchDraft } from '../dispatchContracts'
const identity = vi.hoisted(() => ({ id: '' }))
const can = vi.hoisted(() => vi.fn((_code: string) => true))
const push = vi.hoisted(() => vi.fn())
vi.mock('@/core/auth/auth', () => ({ useAuthStore: () => ({ user: identity }) }))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) }))
vi.mock('vue-router', () => ({ useRouter: () => ({ push }) }))
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn() }))
vi.mock('@/modules/overseas-master-data', () => ({ listVehicles: vi.fn(), listDrivers: vi.fn(), overseasMasterDataPermissions: { vehicleList: 'vehicle:list', driverList: 'driver:list' } }))
vi.mock('../api/dispatches', () => ({ listDispatchDrafts: vi.fn(), listDispatchCandidates: vi.fn(), createDispatchDraft: vi.fn(), createDispatchDraftContainer: vi.fn(), lockDispatchDraftPallet: vi.fn(), unlockDispatchDraftPallet: vi.fn(), deleteDispatchDraftContainer: vi.fn(), releaseDispatchDraft: vi.fn(), convertDispatchDraft: vi.fn(), createDispatch: vi.fn(), getDispatch: vi.fn(), getDispatchCommand: vi.fn() }))
enableAutoUnmount(afterEach)
afterEach(() => vi.unstubAllGlobals())
const warehouse: PlatformWarehouseResource = { id: '321', tenantId: '0', warehouseCode: 'LAX', internalCode: null, warehouseName: '洛杉矶仓', warehouseType: 'OVERSEAS', countryCode: 'US', timezone: 'America/Los_Angeles', contactName: null, contactPhone: null, contactEmail: null, status: 'ACTIVE', remark: null }
const vehicleRecord: VehicleDetail = { id: '9223372036854775001', plateNumber: 'CA-MASTER', model: '档案车型', maxPalletSpaces: 17, maxLoadKg: 15000, maxVolumeCbm: 62, fuelConsumptionPer100Km: 10, mileageKm: 100, driverOrDispatchCompany: null, phone: null, region: 'OVERSEAS', ownership: 'OWNED', status: 'IN_SERVICE', remark: null, recordVersion: 0, updatedBy: 'operator', updatedAt: '' }
const driverRecord: DriverDetail = { id: '9223372036854775002', driverNo: 'DR-1', active: true, reviewStatus: 'APPROVED', licenseFileId: 'LICENSE-FILE', recordVersion: 0, updatedBy: 'operator', updatedAt: '', licenseState: 'VALID', warehouseCode: 'LAX', warehouseName: '洛杉矶仓', qualification: { canDispatch: true, expiringSoon: false, reason: 'ELIGIBLE', licenseExpiresOn: '2030-01-01' }, details: { name: '档案司机', phone: '555-0101', ownership: 'OWNED', region: 'OVERSEAS', licenseNumber: 'LICENSE-1', licenseType: null, licenseIssuedOn: null, licenseExpiresOn: '2030-01-01', licenseZone: 'America/Los_Angeles', remark: null, profile: null } }
function draft(): DispatchDraft {
const first = candidate(), second = candidate({ sourceKey: 'source-2', customerId: 'C2', customerName: '客户乙', selection: { ...first.selection, planPalletId: '12', warehousePalletId: '22' }, palletNo: 'PALLET-2' })
return { draftToken: 'server-token', recordVersion: 0, warehouseId: '321', status: 'ACTIVE', expiresAt: '2026-12-01T00:00:00Z', containers: [{ draftContainerId: 'container-1', sequenceNo: 1, capacity: 30, sourceKeys: [first.sourceKey] }, { draftContainerId: 'container-2', sequenceNo: 2, capacity: 15, sourceKeys: [second.sourceKey] }], sources: [first, second] }
}
beforeEach(() => {
vi.resetAllMocks(); sessionStorage.clear(); identity.id = crypto.randomUUID(); can.mockReturnValue(true)
vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} })
vi.mocked(listAllPlatformWarehouses).mockResolvedValue([warehouse])
vi.mocked(api.listDispatchDrafts).mockResolvedValue([draft()])
vi.mocked(api.listDispatchCandidates).mockResolvedValue({ total: 1, page: 1, size: 20, pages: 1, candidates: draft().sources, complete: true, snapshotAt: '' })
vi.mocked(api.createDispatch).mockImplementation(async body => command('CREATE', body.idempotencyKey))
vi.mocked(api.convertDispatchDraft).mockImplementation(async body => ({ draftToken: body.draftToken, replayed: false, vehicles: body.groups.map((group, index) => ({ draftContainerId: group.draftContainerId, dispatch: command('CREATE', group.dispatch.idempotencyKey, `PC/${index + 1}`) })) }))
vi.mocked(api.getDispatch).mockImplementation(async no => detail({ dispatchNo: no, recordVersion: 1 }))
vi.mocked(listVehicles).mockResolvedValue({ records: [vehicleRecord], page: 1, size: 100, total: 1, pages: 1 })
vi.mocked(listDrivers).mockResolvedValue({ records: [driverRecord], page: 1, size: 100, total: 1, pages: 1, summary: { total: 1, owned: 1, dispatched: 0, expiringSoon: 0, expired: 0 } })
})
async function render() { const wrapper = mount(SmartDispatchDialog, { attachTo: document.body, props: { modelValue: true, orders: [] }, global: { plugins: [ElementPlus] } }); await flushPromises(); return { wrapper, dialog: wrapper.findComponent(AppDialog) } }
2026-10-20 23:34:11 +08:00
function view(dialog: VueWrapper) { return dialog.getComponent({ name: 'ElDialogContent' }) }
function field(dialog: VueWrapper, label: string) { return dialog.findAllComponents({ name: 'ElFormItem' }).find(item => item.props('label') === label)! }
async function selectWarehouse(dialog: VueWrapper) { const select = field(dialog, '起运仓').getComponent({ name: 'ElSelect' }); select.vm.$emit('update:modelValue', '321'); select.vm.$emit('change', '321'); await flushPromises() }
async function emptyMode(dialog: VueWrapper) { field(dialog, '建单方式').getComponent({ name: 'ElRadioGroup' }).vm.$emit('update:modelValue', 'empty'); await flushPromises() }
2026-10-21 13:22:42 +08:00
async function restore(dialog: VueWrapper) { dialog.findAllComponents({ name: 'ElSelect' }).find(select => select.props('placeholder') === '继续待完成排车')!.vm.$emit('change', 'server-token'); await flushPromises() }
async function selectFirstOrder(dialog: VueWrapper) { await view(dialog).get('[aria-label="待排订单"] tbody tr button').trigger('click'); await flushPromises() }
describe('new dispatch native layout', () => {
2026-10-21 13:22:42 +08:00
it('adds cargo directly, preparing the first vehicle internally and enabling submission after assignment', async () => {
2026-10-20 23:34:11 +08:00
const empty: DispatchDraft = { ...draft(), containers: [], sources: [] }
const grouped: DispatchDraft = { ...empty, recordVersion: 1, containers: [{ draftContainerId: 'first-group', sequenceNo: 1, capacity: 30, sourceKeys: [] }] }
const planned = candidate({ selection: { ...candidate().selection, warehousePalletId: null } })
vi.mocked(api.createDispatchDraft).mockResolvedValue(empty)
vi.mocked(api.createDispatchDraftContainer).mockResolvedValue(grouped)
vi.mocked(api.listDispatchCandidates).mockResolvedValue({ total: 1, page: 1, size: 20, pages: 1, candidates: [planned], complete: true, snapshotAt: '' })
2026-10-20 23:34:11 +08:00
vi.mocked(api.lockDispatchDraftPallet).mockResolvedValue({ ...grouped, recordVersion: 2, sources: [planned], containers: [{ ...grouped.containers[0]!, sourceKeys: [planned.sourceKey] }] })
const { dialog } = await render()
2026-10-21 13:22:42 +08:00
expect(view(dialog).text()).not.toContain('开始安排第一辆车')
expect(view(dialog).find('[aria-label="第 1 辆车"]').exists()).toBe(true)
2026-10-20 23:34:11 +08:00
expect(api.createDispatchDraft).not.toHaveBeenCalled()
await selectWarehouse(dialog)
expect(view(dialog).text()).toContain('在途计划')
const candidateCalls = vi.mocked(api.listDispatchCandidates).mock.calls.length
2026-10-21 13:22:42 +08:00
expect(dialog.props('confirmDisabled')).toBe(true)
await selectFirstOrder(dialog)
await view(dialog).findAll('button').find(button => button.text() === '加入这辆车')!.trigger('click'); await flushPromises()
2026-10-20 23:34:11 +08:00
expect(api.createDispatchDraft).toHaveBeenCalledExactlyOnceWith({ warehouseId: '321', idempotencyKey: expect.any(String) })
expect(api.createDispatchDraftContainer).toHaveBeenCalledExactlyOnceWith({ draftToken: 'server-token', recordVersion: 0, capacity: 30, idempotencyKey: expect.any(String) })
2026-10-21 13:22:42 +08:00
expect(api.listDispatchCandidates).toHaveBeenCalledTimes(candidateCalls + 1)
expect(dialog.findComponent(DispatchResourceFields).exists()).toBe(false)
2026-10-20 23:34:11 +08:00
expect(api.lockDispatchDraftPallet).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ draftContainerId: 'first-group', source: planned.selection, recordVersion: 1 }))
expect(dialog.props('confirmDisabled')).toBe(false)
2026-10-21 13:22:42 +08:00
expect(view(dialog).get('[aria-label="第 1 辆车"]').text()).toContain('托数 1/30')
expect(view(dialog).findAll('button').find(button => button.text() === '已加入第 1 辆车')!.attributes('disabled')).toBeDefined()
2026-10-20 23:34:11 +08:00
})
it('retains the exact first-group command after an unknown result without creating a second draft', async () => {
const empty: DispatchDraft = { ...draft(), containers: [], sources: [] }
vi.mocked(api.createDispatchDraft).mockResolvedValue(empty)
vi.mocked(api.createDispatchDraftContainer).mockRejectedValueOnce(new Error('network unknown')).mockResolvedValueOnce({ ...empty, recordVersion: 1, containers: [{ draftContainerId: 'first-group', sequenceNo: 1, capacity: 30, sourceKeys: [] }] })
const { dialog } = await render(); await selectWarehouse(dialog)
2026-10-21 13:22:42 +08:00
await selectFirstOrder(dialog)
await view(dialog).findAll('button').find(button => button.text() === '加入这辆车')!.trigger('click'); await flushPromises()
2026-10-20 23:34:11 +08:00
const original = vi.mocked(api.createDispatchDraftContainer).mock.calls[0]![0]
expect(dialog.props('confirmDisabled')).toBe(false)
dialog.vm.$emit('confirm'); await flushPromises()
expect(api.createDispatchDraft).toHaveBeenCalledTimes(1)
expect(api.createDispatchDraftContainer).toHaveBeenNthCalledWith(2, original)
2026-10-21 13:22:42 +08:00
expect(api.lockDispatchDraftPallet).not.toHaveBeenCalled()
expect(dialog.findComponent(DispatchResourceFields).exists()).toBe(false)
2026-10-20 23:34:11 +08:00
expect(api.convertDispatchDraft).not.toHaveBeenCalled()
})
it('requires an active cargo group before accepting per-group settings while keeping setup controls available', async () => {
const { dialog } = await render()
2026-10-20 23:34:11 +08:00
expect(view(dialog).find('[aria-label="排车设置"] table').exists()).toBe(false)
expect(view(dialog).find('[aria-label="排车设置"] .density-dialog-note').exists()).toBe(false)
expect(view(dialog).classes()).toContain('admin-density-workspace')
expect(dialog.getComponent({ name: 'ElDialog' }).props()).toMatchObject({ appendToBody: true, lockScroll: true })
expect(view(dialog).text()).not.toContain('点击“开始选货”建立第一组')
expect(view(dialog).text()).not.toContain('选择起运仓后点击')
expect(dialog.findComponent(DispatchResourceFields).exists()).toBe(false)
expect(dialog.props('confirmDisabled')).toBe(true)
expect(field(dialog, '建单方式').getComponent({ name: 'ElRadioGroup' }).props('disabled')).toBe(false)
await restore(dialog)
2026-10-21 13:22:42 +08:00
expect(dialog.findComponent(DispatchResourceFields).exists()).toBe(false)
expect(field(dialog, '备注').get('input').attributes('disabled')).toBeUndefined()
})
2026-10-21 13:22:42 +08:00
it('creates an unassigned empty dispatch while preserving optional dates and remark through fullscreen', async () => {
const { dialog } = await render(); await emptyMode(dialog); await selectWarehouse(dialog)
expect(dialog.findComponent(DispatchResourceFields).exists()).toBe(false)
field(dialog, '计划发车时间').getComponent({ name: 'ElDatePicker' }).vm.$emit('update:modelValue', '2026-11-02T10:00:00Z')
field(dialog, '预计到达时间').getComponent({ name: 'ElDatePicker' }).vm.$emit('update:modelValue', '2026-11-02T18:00:00Z')
await field(dialog, '备注').get('input').setValue('本趟备注')
2026-10-20 23:34:11 +08:00
await view(dialog).get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises()
expect(dialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true)
2026-10-20 23:34:11 +08:00
expect(view(dialog).text()).toContain('空单暂不能核平、装车或发车')
2026-10-21 13:22:42 +08:00
expect(view(dialog).find('[aria-label="智能排车三栏选货"]').exists()).toBe(false)
2026-10-20 23:34:11 +08:00
await view(dialog).findAll('button').find(button => button.text() === '保存空排车单')!.trigger('click'); await flushPromises()
2026-10-21 13:22:42 +08:00
expect(api.createDispatch).toHaveBeenCalledExactlyOnceWith({ idempotencyKey: expect.any(String), warehouseId: '321', cargo: [], batchAllocations: [], plannedDepartureAt: '2026-11-02T10:00:00Z', estimatedArrivalAt: '2026-11-02T18:00:00Z', remark: '本趟备注', vehicle: null, driver: null })
})
2026-10-21 13:22:42 +08:00
it('creates with only the warehouse and never requests vehicle or driver directories', async () => {
const { dialog } = await render(); await emptyMode(dialog); await selectWarehouse(dialog)
2026-10-21 13:22:42 +08:00
expect(dialog.findComponent(DispatchResourceFields).exists()).toBe(false)
dialog.vm.$emit('confirm'); await flushPromises()
2026-10-21 13:22:42 +08:00
expect(api.createDispatch).toHaveBeenCalledWith(expect.objectContaining({ vehicle: null, driver: null, plannedDepartureAt: null, estimatedArrivalAt: null, remark: null }))
expect(listVehicles).not.toHaveBeenCalled()
expect(listDrivers).not.toHaveBeenCalled()
})
it('keeps each group configuration independent and verifies proven conversions without creating them twice', async () => {
vi.mocked(api.getDispatch).mockRejectedValueOnce(new Error('details not ready')).mockImplementation(async no => detail({ dispatchNo: no, recordVersion: 1 }))
const { wrapper, dialog } = await render(); await restore(dialog)
2026-10-21 13:22:42 +08:00
await field(dialog, '备注').get('input').setValue('第一组')
const radios = view(dialog).get('[aria-label="车辆装载"]').findAll('input[type="radio"]')
await radios[1]!.setValue(true); await flushPromises()
await field(dialog, '备注').get('input').setValue('第二组')
await radios[0]!.setValue(true); await flushPromises()
expect((field(dialog, '备注').get('input').element as HTMLInputElement).value).toBe('第一组')
2026-10-20 23:34:11 +08:00
await view(dialog).get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises()
dialog.vm.$emit('confirm'); await flushPromises()
expect(api.convertDispatchDraft).toHaveBeenCalledTimes(1)
const request = vi.mocked(api.convertDispatchDraft).mock.calls[0]![0]
expect(request.recordVersion).toBe(0)
2026-10-21 13:22:42 +08:00
expect(request.groups.map(group => [group.dispatch.vehicle, group.dispatch.driver, group.dispatch.remark])).toEqual([[null, null, '第一组'], [null, null, '第二组']])
expect(wrapper.emitted('created')).toBeUndefined()
2026-10-20 23:34:11 +08:00
expect(view(dialog).text()).toContain('草稿操作结果未确认,原请求已保留')
dialog.findComponent(LoadErrorState).vm.$emit('retry'); await flushPromises()
expect(api.convertDispatchDraft).toHaveBeenCalledTimes(1)
expect(wrapper.emitted('created')).toHaveLength(1)
})
it('locks the real frame and all resource edits while pending and reuses the original conversion after failure', async () => {
let reject!: (reason: unknown) => void
vi.mocked(api.convertDispatchDraft).mockReturnValueOnce(new Promise((_, no) => { reject = no })).mockImplementationOnce(async body => ({ draftToken: body.draftToken, replayed: true, vehicles: body.groups.map((group, index) => ({ draftContainerId: group.draftContainerId, dispatch: command('CREATE', group.dispatch.idempotencyKey, `PC/${index + 1}`) })) }))
2026-10-21 13:22:42 +08:00
const { wrapper, dialog } = await render(); await restore(dialog)
dialog.vm.$emit('confirm'); dialog.vm.$emit('confirm'); await flushPromises()
expect(api.convertDispatchDraft).toHaveBeenCalledTimes(1)
const original = JSON.parse(JSON.stringify(vi.mocked(api.convertDispatchDraft).mock.calls[0]![0])) as ConvertDraft
expect(dialog.findComponent({ name: 'ElDialog' }).props()).toMatchObject({ showClose: false, closeOnPressEscape: false })
2026-10-21 13:22:42 +08:00
expect(field(dialog, '备注').get('input').attributes('disabled')).toBeDefined()
reject(new ApiResponseError('转换结果尚未确认', 'A0503', 503, null, 'new-dispatch-request')); await flushPromises()
2026-10-20 23:34:11 +08:00
expect(view(dialog).text()).toContain('new-dispatch-request')
2026-10-21 13:22:42 +08:00
expect(dialog.findComponent(DispatchResourceFields).exists()).toBe(false)
await wrapper.setProps({ modelValue: false }); await wrapper.setProps({ modelValue: true }); await flushPromises()
dialog.vm.$emit('confirm'); await flushPromises()
expect(vi.mocked(api.convertDispatchDraft).mock.calls[1]![0]).toEqual(original)
})
2026-10-21 13:22:42 +08:00
it('keeps all current-page orders and reveals complete pallet source details only after selecting an order', async () => {
const rows = Array.from({ length: 20 }, (_, index) => candidate({ sourceKey: `SOURCE-${index}`, palletNo: `PALLET-${index}`, orderNo: `ORDER-${index}`, customerName: '很长的客户名称'.repeat(8) }))
rows[0] = sharedPalletCandidates()[0]!
vi.mocked(api.listDispatchCandidates).mockResolvedValue({ total: 20, page: 1, size: 20, pages: 1, candidates: rows, complete: true, snapshotAt: '' })
const { dialog } = await render(); await selectWarehouse(dialog)
2026-10-21 13:22:42 +08:00
const table = view(dialog).get('[aria-label="待排订单"]')
expect(table.findAll('tbody tr')).toHaveLength(20)
expect(table.text()).toContain('ORDER-19')
expect(view(dialog).get('[aria-label="托盘明细"]').text()).toContain('请选择左侧订单')
await selectFirstOrder(dialog)
const sourceGroup = view(dialog).get('[aria-label="托盘明细"] details')
expect(sourceGroup.findAll('li')).toHaveLength(2)
expect(sourceGroup.text()).toContain('客户A')
expect(sourceGroup.text()).toContain('客户B')
expect(api.lockDispatchDraftPallet).not.toHaveBeenCalled()
})
2026-10-20 23:34:11 +08:00
it.each(['PLAN', 'RELABEL'])('keeps %s query conditions and regional tabs on the original candidate transport', async sourceStage => {
const { dialog } = await render(); await selectWarehouse(dialog)
2026-10-20 23:34:11 +08:00
await view(dialog).get('input[placeholder="订单号/入仓码"]').setValue('IN-QUERY')
await view(dialog).get('input[placeholder="目的仓码"]').setValue('ONT8')
dialog.findAllComponents({ name: 'ElSelect' }).find(select => select.props('placeholder') === '全部来源')!.vm.$emit('update:modelValue', sourceStage); await flushPromises()
await view(dialog).findAll('button').find(button => button.text() === '查询')!.trigger('click'); await flushPromises()
expect(api.listDispatchCandidates).toHaveBeenLastCalledWith({ page: 1, size: 20, warehouseTab: 'ALL', warehouseId: '321', orderNo: 'IN-QUERY', destinationWarehouseCode: 'ONT8', sourceStage, draftToken: undefined })
const calls = vi.mocked(api.listDispatchCandidates).mock.calls.length
2026-10-20 23:34:11 +08:00
await view(dialog).get('[aria-label="候选仓库展示"]').findAll('button').find(button => button.text() === '纽约')!.trigger('click'); await flushPromises()
expect(view(dialog).get('[aria-label="候选仓库展示"]').findAll('button').find(button => button.text() === '纽约')!.attributes('aria-selected')).toBe('true')
expect(api.listDispatchCandidates).toHaveBeenCalledTimes(calls + 1)
expect(api.listDispatchCandidates).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, warehouseTab: 'NYC', orderNo: 'IN-QUERY', sourceStage }))
})
it('shows loading and initialization diagnostics without issuing create commands', async () => {
vi.mocked(api.listDispatchDrafts).mockRejectedValueOnce(new ApiResponseError('草稿加载失败', 'A0503', 503, null, 'draft-load-request')).mockResolvedValueOnce([draft()])
const { dialog } = await render()
2026-10-20 23:34:11 +08:00
expect(view(dialog).text()).toContain('draft-load-request')
dialog.findComponent(LoadErrorState).vm.$emit('retry'); await flushPromises()
expect(api.listDispatchDrafts).toHaveBeenCalledTimes(2)
expect(api.createDispatch).not.toHaveBeenCalled(); expect(api.createDispatchDraft).not.toHaveBeenCalled()
})
it('requires the explicit recheck action after a definite rejection without automatically resubmitting', async () => {
vi.mocked(api.createDispatchDraft).mockRejectedValue(new ApiResponseError('草稿条件已变化', 'A0409', 409, null, 'draft-rejected-request'))
const { dialog } = await render(); await selectWarehouse(dialog)
2026-10-21 13:22:42 +08:00
await selectFirstOrder(dialog)
await view(dialog).findAll('button').find(button => button.text() === '加入这辆车')!.trigger('click'); await flushPromises()
2026-10-20 23:34:11 +08:00
expect(view(dialog).text()).toContain('draft-rejected-request')
expect(dialog.findComponent(DispatchResourceFields).exists()).toBe(false)
await view(dialog).findAll('button').find(button => button.text() === '按明确驳回结果重新核对')!.trigger('click'); await flushPromises()
expect(api.createDispatchDraft).toHaveBeenCalledTimes(1)
expect(field(dialog, '建单方式').getComponent({ name: 'ElRadioGroup' }).props('disabled')).toBe(false)
2026-10-20 23:34:11 +08:00
expect(dialog.findComponent(DispatchResourceFields).exists()).toBe(false)
expect(api.listDispatchDrafts).toHaveBeenCalledTimes(2)
})
it('keeps creation unavailable without its write permission', async () => {
can.mockReturnValue(false)
const { dialog } = await render()
expect(dialog.props('confirmDisabled')).toBe(true)
dialog.vm.$emit('confirm'); await flushPromises()
expect(api.createDispatch).not.toHaveBeenCalled(); expect(api.convertDispatchDraft).not.toHaveBeenCalled()
})
})