120 lines
11 KiB
TypeScript
120 lines
11 KiB
TypeScript
import { defineComponent, h } from 'vue'
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import dayjs from 'dayjs'
|
|
import OwnedBusinessView from './OwnedBusinessView.vue'
|
|
import OwnedBusinessDetailView from './OwnedBusinessDetailView.vue'
|
|
import { ownedBusinessFixture } from '../components/OwnedBusinessFixtures'
|
|
import { overseasMasterDataPermissions as permission } from '../permissions'
|
|
|
|
const calls = vi.hoisted(() => ({ list: vi.fn(), detail: vi.fn(), history: vi.fn(), content: vi.fn(), warehouses: vi.fn(), push: vi.fn(), allowed: new Set<string>() }))
|
|
vi.mock('../api/ownedBusiness', () => ({ listOwnedBusiness: calls.list, getOwnedBusiness: calls.detail, listOwnedBusinessHistory: calls.history, getOwnedBusinessPhotoContent: calls.content }))
|
|
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: calls.warehouses }))
|
|
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: (code: string) => calls.allowed.has(code) }) }))
|
|
vi.mock('vue-router', () => ({ useRouter: () => ({ push: calls.push }), useRoute: () => ({ params: { dispatchNo: 'DS-OWN-1' } }) }))
|
|
const Surface = defineComponent({ template: '<section><slot name="header" /><slot /></section>' })
|
|
const Button = defineComponent({ props: ['disabled'], template: '<button :disabled="disabled"><slot /></button>' })
|
|
const Tabs = defineComponent({ props: ['modelValue'], emits: ['update:modelValue'], template: '<div><slot /></div>' })
|
|
const Pane = defineComponent({ props: ['name','label'], template: '<article><slot /></article>' })
|
|
const Query = defineComponent({ props: ['modelValue','fields'], emits: ['update:modelValue','search'], template: '<div />' })
|
|
const Table = defineComponent({ props: ['data','columns','total','page','pageSize'], emits: ['update:page','update:pageSize'], setup(props,{slots}) { return () => h('div', [slots['toolbar-start']?.(), slots.filters?.(), slots.notices?.(), ...(props.data || []).map((row: object) => h('div', Object.entries(slots).filter(([name]) => !['toolbar-start','filters','notices'].includes(name)).map(([name,slot]) => h('section', { 'data-column': name }, slot?.({row})))))]) } })
|
|
const OperationsEditor = defineComponent({ name: 'OwnedBusinessOperationsEditorStub', props: ['modelValue','detail'], emits: ['update:modelValue','saved','busy'], template: '<div />' })
|
|
const PhotosEditor = defineComponent({ name: 'OwnedBusinessPhotosEditorStub', props: ['modelValue','detail'], emits: ['update:modelValue','saved','busy'], template: '<div />' })
|
|
const global = { stubs: { AdminPage: Surface, DetailPageLayout: Surface, DataTable: Table, QueryToolbar: Query, ElTabs: Tabs, ElTabPane: Pane, ElButton: Button, OwnedBusinessOperationsEditor: OperationsEditor, OwnedBusinessPhotosEditor: PhotosEditor, LoadErrorState: { props:['message'], template:'<div>{{message}}<slot name="details" /></div>' }, ApiErrorMeta: true }, directives: { loading: () => {} } }
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks(); calls.allowed = new Set(Object.values(permission)); calls.allowed.add('platform:warehouse:list')
|
|
const detail = ownedBusinessFixture()
|
|
calls.list.mockResolvedValue({ records:[detail.row],page:1,size:20,total:41,summary:{dispatchCount:41,afterLoadingCount:30,deliveringCount:10,deliveredCount:1} })
|
|
calls.detail.mockResolvedValue(detail); calls.history.mockResolvedValue({records:[],page:1,size:20,total:0}); calls.warehouses.mockResolvedValue([])
|
|
})
|
|
|
|
describe('owned business views', () => {
|
|
it('keeps split prices authoritative and arrival times associated with each destination', async () => {
|
|
const detail = ownedBusinessFixture()
|
|
detail.row.operations.money = { currency: 'USD', amount: '0.00' }; detail.row.unitPrice = '0.00'
|
|
detail.row.destinations[0]!.appointments[0]!.scheduledAt = '2026-10-21T09:00:00Z'
|
|
detail.row.destinations.push({ destinationKey: 'destination-2', warehouseCode: 'FBA-2', warehouseName: null, address: null, appointments: [], actualArrivedAt: '2026-10-21T10:00:00Z' })
|
|
calls.list.mockResolvedValue({ records: [detail.row], page: 1, size: 20, total: 1, summary: { dispatchCount: 1, afterLoadingCount: 1, deliveringCount: 0, deliveredCount: 0 } })
|
|
const wrapper = mount(OwnedBusinessView, { global }); await flushPromises()
|
|
const columns = wrapper.findComponent(Table).props('columns') as { key?: string; label: string; slotName?: string }[]
|
|
expect(columns.map(column => column.label)).toEqual(expect.arrayContaining(['目的仓', '派送仓库', '总价', '单价(/托)', '最晚装车完成时间', '预约到达时间', '实际到达时间', '状态']))
|
|
expect(columns.find(column => column.key === 'deliveryWarehouses')!.slotName).toBe('destinations')
|
|
expect(wrapper.get('[data-column="money"]').text()).toBe('USD 0.00')
|
|
expect(wrapper.get('[data-column="unitPrice"]').text()).toBe('USD 0.00')
|
|
expect(wrapper.get('[data-column="latestLoaded"]').text()).toBe('—')
|
|
const scheduled = wrapper.findAll('[data-column="scheduledArrival"] .owned-business-destination')
|
|
expect(scheduled.map(cell => cell.text())).toEqual(['FBA-1' + dayjs('2026-10-21T09:00:00Z').format('YYYY-MM-DD HH:mm'), 'FBA-2—'])
|
|
const actual = wrapper.findAll('[data-column="actualArrival"] .owned-business-destination')
|
|
expect(actual.map(cell => cell.text())).toEqual(['FBA-1—', 'FBA-2' + dayjs('2026-10-21T10:00:00Z').format('YYYY-MM-DD HH:mm')])
|
|
expect(calls.detail).not.toHaveBeenCalled()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('keeps a non-pool external vehicle returned by the server without filtering or requiring a vehicle id', async () => {
|
|
const detail = ownedBusinessFixture()
|
|
detail.row.vehicle = { vehicleId: null, plate: 'EXTERNAL-NO-POOL', ownership: 'DISPATCHED' }
|
|
calls.list.mockResolvedValue({ records: [detail.row], page: 1, size: 20, total: 1, summary: { dispatchCount: 1, afterLoadingCount: 1, deliveringCount: 0, deliveredCount: 0 } })
|
|
const wrapper = mount(OwnedBusinessView, { global }); await flushPromises()
|
|
expect(wrapper.findComponent(Table).props('data')[0].vehicle).toEqual(detail.row.vehicle)
|
|
expect(wrapper.findComponent(Table).props('total')).toBe(1)
|
|
expect(wrapper.text()).toContain('登记运营信息')
|
|
wrapper.unmount()
|
|
})
|
|
it('uses one list request for all statistics and never hydrates each row', async () => {
|
|
const wrapper = mount(OwnedBusinessView,{global}); await flushPromises()
|
|
expect(calls.list).toHaveBeenCalledTimes(1); expect(calls.detail).not.toHaveBeenCalled(); expect(calls.content).not.toHaveBeenCalled()
|
|
expect(wrapper.find('[aria-label="完整筛选范围自有业务统计"]').text()).toContain('排车数41')
|
|
expect(wrapper.text()).not.toContain('今日排车')
|
|
expect(wrapper.findComponent(Table).props('columns')[0].key).toBe('actions')
|
|
expect(wrapper.findComponent(Table).props('total')).toBe(41)
|
|
wrapper.unmount()
|
|
})
|
|
it('applies planned date half-open boundaries only on query and keeps all stage counts from the response', async () => {
|
|
const wrapper=mount(OwnedBusinessView,{global});await flushPromises();const query=wrapper.findComponent(Query)
|
|
query.vm.$emit('update:modelValue',{plannedRange:['2026-10-20','2026-10-21'],keyword:' FBA-1 ',stage:'AFTER_LOADING',warehouseId:'1'});await wrapper.vm.$nextTick()
|
|
expect(calls.list).toHaveBeenCalledTimes(1)
|
|
query.vm.$emit('search');await flushPromises()
|
|
expect(calls.list).toHaveBeenLastCalledWith({page:1,size:20,keyword:'FBA-1',stage:'AFTER_LOADING',warehouseId:'1',plannedDepartureFrom:dayjs('2026-10-20').startOf('day').toISOString(),plannedDepartureTo:dayjs('2026-10-21').add(1,'day').startOf('day').toISOString()})
|
|
expect(wrapper.find('[aria-label="完整筛选范围自有业务统计"]').text()).toContain('装车完成30')
|
|
wrapper.unmount()
|
|
})
|
|
it('opens the operations modal on demand with auxiliary zero version instead of the dispatch version', async () => {
|
|
const wrapper=mount(OwnedBusinessView,{global});await flushPromises()
|
|
expect(calls.detail).not.toHaveBeenCalled();expect(wrapper.findComponent(OperationsEditor).props('modelValue')).toBe(false)
|
|
await wrapper.findAll('button').find(button=>button.text()==='登记运营信息')!.trigger('click');await flushPromises()
|
|
expect(calls.detail).toHaveBeenCalledExactlyOnceWith('DS-OWN-1')
|
|
const editor=wrapper.findComponent(OperationsEditor)
|
|
expect(editor.props('modelValue')).toBe(true);expect(wrapper.findComponent(PhotosEditor).props('modelValue')).toBe(false)
|
|
expect(editor.props('detail').row.recordVersion).toBe('0');expect(editor.props('detail').dispatch.recordVersion).toBe('8')
|
|
wrapper.unmount()
|
|
})
|
|
it('keeps a list-only user read-only while retaining detail and authorized photo reads', async () => {
|
|
calls.allowed=new Set([permission.ownedBusinessList])
|
|
const wrapper=mount(OwnedBusinessView,{global});await flushPromises()
|
|
expect(wrapper.text()).not.toContain('登记运营信息');expect(wrapper.text()).toContain('查看照片');expect(calls.warehouses).not.toHaveBeenCalled()
|
|
wrapper.unmount()
|
|
})
|
|
it('loads aggregated history only on its tab, preserving null operations on dispatch events', async () => {
|
|
calls.history.mockResolvedValue({records:[{eventSource:'DISPATCH',recordVersion:null,dispatchRecordVersion:'8',action:'LOADING_COMPLETE',operatorName:null,occurredAt:'2026-10-20T09:00:00Z',reason:null,operations:null,photos:[]},{eventSource:'OPERATIONS',recordVersion:'1',dispatchRecordVersion:null,action:'SAVE_OPERATIONS',operatorName:'登记员',occurredAt:'2026-10-20T10:00:00Z',reason:null,operations:ownedBusinessFixture().row.operations,photos:[]}],page:1,size:20,total:2})
|
|
const wrapper=mount(OwnedBusinessDetailView,{global});await flushPromises()
|
|
expect(calls.detail).toHaveBeenCalledTimes(1);expect(calls.history).not.toHaveBeenCalled()
|
|
wrapper.findComponent(Tabs).vm.$emit('update:modelValue','history');await flushPromises()
|
|
expect(calls.history).toHaveBeenCalledExactlyOnceWith('DS-OWN-1',1,20)
|
|
expect(wrapper.text()).toContain('原排车');expect(wrapper.text()).toContain('运营登记')
|
|
const table=wrapper.findComponent(Table), records=table.props('data')
|
|
expect(records[0].eventKey).not.toBe(records[1].eventKey);expect(records[0].operations).toBeNull()
|
|
table.vm.$emit('update:page',2);await flushPromises();expect(calls.history).toHaveBeenLastCalledWith('DS-OWN-1',2,20)
|
|
expect(calls.detail).toHaveBeenCalledTimes(1);expect(calls.content).not.toHaveBeenCalled()
|
|
wrapper.unmount()
|
|
})
|
|
it('shows authoritative large money text and unit price without rounding or recomputing from two cargo rows', async () => {
|
|
const detail=ownedBusinessFixture();detail.row.operations.money={currency:'USD',amount:'9999999999999999.99'};detail.row.unitPrice='9999999999999999.99'
|
|
calls.detail.mockResolvedValue(detail)
|
|
const wrapper=mount(OwnedBusinessDetailView,{global});await flushPromises()
|
|
expect(wrapper.text()).toContain('USD 9999999999999999.99');expect(wrapper.text()).not.toContain('10000000000000000')
|
|
expect(wrapper.text()).not.toContain('4999999999999999')
|
|
wrapper.unmount()
|
|
})
|
|
})
|