Files
admin-web/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts
T
2026-08-28 13:58:52 +08:00

282 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import ElementPlus from 'element-plus'
import { createPinia } from 'pinia'
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ApiResponseError } from '@/shared/api/http'
import DataTable from '@/shared/components/DataTable.vue'
import { listAllPlatformWarehouses } from '@/modules/warehouses'
import {
convertLastMileContainerToInboundPlans,
convertLastMileOrderToInboundPlan,
getContainerPlanProgress,
listContainerPlanProgress,
listLastMileOrders,
transferLastMileOrders,
} from '../api/lastMileOrders'
import LastMileOrdersView from './LastMileOrdersView.vue'
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) }))
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn() }))
vi.mock('../api/lastMileOrders', () => ({
listLastMileOrders: vi.fn(), listContainerPlanProgress: vi.fn(), getContainerPlanProgress: vi.fn(),
convertLastMileOrderToInboundPlan: vi.fn(), convertLastMileContainerToInboundPlans: vi.fn(), transferLastMileOrders: vi.fn(),
}))
class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} }
function memoryStorage(): Storage {
const values = new Map<string, string>()
return {
get length() { return values.size }, clear: () => values.clear(), getItem: (key) => values.get(key) ?? null,
key: (index) => [...values.keys()][index] ?? null, removeItem: (key) => values.delete(key), setItem: (key, value) => values.set(key, value),
}
}
const bookingId = '2092873463568597001'
const customerId = '2092618717861048321'
const summary = {
sourceContainerBookingId: bookingId, containerNo: 'CONT-15', totalOrderCount: 15,
inboundCodeCount: 15, missingInboundCodeCount: 0, planCount: 10, plannedInboundCodeCount: 10,
pendingPlanCount: 5, plannedArrivalAt: '2026-09-01T08:00:00', allPlansReady: false,
blockingReason: '存在5票尚未生成有效入库计划',
}
const fbaWarehouse = {
id: '9007199254740993', tenantId: '0', warehouseCode: 'ONT8', warehouseName: 'Ontario FBA',
warehouseType: 'FBA', countryCode: 'US', timezone: 'America/Los_Angeles', contactName: null,
contactPhone: null, contactEmail: null, status: 'ACTIVE', remark: null,
primaryAddress: {
id: '9007199254740994', tenantId: '0', warehouseId: '9007199254740993', addressType: 'OPERATING',
countryCode: 'US', stateProvince: 'CA', city: 'Ontario', district: null, postalCode: '91761',
addressLine1: 'FBA address', addressLine2: null, latitude: null, longitude: null,
},
addressSummary: 'CA Ontario',
}
function selectedRow(overrides: Record<string, unknown> = {}) {
return { id: '2092873463568596994', customerId, sourceContainerBookingId: bookingId, orderNo: 'LMO-260827-0001', ...overrides }
}
enableAutoUnmount(afterEach)
afterAll(() => vi.unstubAllGlobals())
async function mountView() {
const wrapper = mount(LastMileOrdersView, { attachTo: document.body, global: { plugins: [createPinia(), ElementPlus] } })
await flushPromises()
return wrapper
}
async function openPending() {
const wrapper = await mountView()
const pendingTab = wrapper.findAll('.el-radio-button').find((item) => item.text() === '待分配')
await pendingTab!.trigger('click')
await flushPromises()
return wrapper
}
async function selectRows(wrapper: Awaited<ReturnType<typeof mountView>>, rows: object[]) {
wrapper.findComponent(DataTable).vm.$emit('selection-change', rows)
await wrapper.vm.$nextTick()
}
function button(wrapper: Awaited<ReturnType<typeof mountView>>, text: string) {
return wrapper.findAll('button').find((item) => item.text().includes(text))
}
async function openContainerDialog(wrapper: Awaited<ReturnType<typeof mountView>>) {
await button(wrapper, '按柜批量生成')!.trigger('click')
await flushPromises()
}
async function chooseContainer(wrapper: Awaited<ReturnType<typeof mountView>>) {
const selects = wrapper.findAllComponents({ name: 'ElSelect' })
const containerSelect = selects[selects.length - 1]
expect(containerSelect).toBeDefined()
containerSelect.vm.$emit('update:modelValue', bookingId)
await wrapper.vm.$nextTick()
}
describe('LastMileOrdersView pending order and container conversion', () => {
beforeEach(() => {
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
vi.stubGlobal('localStorage', memoryStorage())
vi.mocked(listLastMileOrders).mockReset().mockResolvedValue({ records: [], page: 1, size: 10, total: 0, pages: 0 })
vi.mocked(listContainerPlanProgress).mockReset().mockResolvedValue({ records: [summary], page: 1, size: 50, total: 1, pages: 1 })
vi.mocked(getContainerPlanProgress).mockReset().mockResolvedValue({
summary,
orders: [{ orderId: '2092873463568596994', customerId, orderNo: 'LMO-1', inboundCode: 'IN-1', orderStatus: 'INBOUND_PLANNED', planId: '2092873463568597010', planNo: 'LIP-1', planStatus: 'PENDING_RECEIVING', planValid: true, plannedArrivalAt: '2026-09-01T08:00:00' }],
})
vi.mocked(convertLastMileOrderToInboundPlan).mockReset().mockResolvedValue({})
vi.mocked(convertLastMileContainerToInboundPlans).mockReset().mockResolvedValue([{ planNo: 'LIP-1' }, { planNo: 'LIP-2' }] as never)
vi.mocked(listAllPlatformWarehouses).mockReset().mockResolvedValue([fbaWarehouse] as never)
vi.mocked(transferLastMileOrders).mockReset().mockResolvedValue(2)
})
it('keeps the original pending order table and does not render a view switch or container table', async () => {
const wrapper = await openPending()
expect(wrapper.text()).toContain('尾程单号')
expect(wrapper.text()).toContain('海外仓')
expect(wrapper.text()).toContain('转仓')
expect(wrapper.text()).toContain('生成入库计划')
expect(wrapper.text()).toContain('按柜批量生成')
expect(wrapper.text()).not.toContain('按订单')
expect(wrapper.text()).not.toContain('待分配整柜')
expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, size: 10, currentStatus: 'PENDING_ASSIGNMENT' }))
expect(listContainerPlanProgress).not.toHaveBeenCalled()
})
it('keeps single-order conversion isolated with long order and customer ids', async () => {
const wrapper = await openPending()
await selectRows(wrapper, [selectedRow()])
await button(wrapper, '生成入库计划')!.trigger('click')
await flushPromises()
expect(convertLastMileOrderToInboundPlan).toHaveBeenCalledTimes(1)
expect(convertLastMileOrderToInboundPlan).toHaveBeenCalledWith({ orderId: '2092873463568596994', customerId })
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
expect(listContainerPlanProgress).not.toHaveBeenCalled()
})
it('opens a cabinet selector without requiring order selection and loads server summaries only then', async () => {
const wrapper = await openPending()
await openContainerDialog(wrapper)
expect(listContainerPlanProgress).toHaveBeenCalledTimes(1)
expect(listContainerPlanProgress).toHaveBeenCalledWith({ page: 1, size: 50, containerNo: undefined })
expect(document.body.textContent).toContain('选择待分配柜')
expect(document.body.textContent).toContain('将为所选柜内所有待分配尾程订单分别生成独立入库计划')
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
})
it('selects one cabinet and sends one long-id convert-container request without using current-page row counts', async () => {
const wrapper = await openPending()
await openContainerDialog(wrapper)
await chooseContainer(wrapper)
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认按柜生成')) as HTMLButtonElement
expect(confirm.disabled).toBe(false)
confirm.click()
await flushPromises()
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(1)
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledWith({ sourceContainerBookingId: bookingId })
expect(convertLastMileOrderToInboundPlan).not.toHaveBeenCalled()
expect(document.body.textContent).toContain('已按柜生成 2 个独立入库计划')
expect(listLastMileOrders).toHaveBeenCalledTimes(3)
})
it.each(['A0404', 'A0409'])('keeps %s request evidence visible after cabinet conversion failure', async (code) => {
vi.mocked(convertLastMileContainerToInboundPlans).mockRejectedValueOnce(new ApiResponseError(
'柜级生成失败', code, 200, undefined, `request-${code}`, `trace-${code}`,
))
const wrapper = await openPending()
await openContainerDialog(wrapper)
await chooseContainer(wrapper)
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认按柜生成')) as HTMLButtonElement
confirm.click()
await flushPromises()
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(1)
expect(wrapper.text()).toContain(code)
expect(wrapper.text()).toContain(`request-${code}`)
expect(wrapper.text()).toContain(`trace-${code}`)
})
it('loads the cabinet tab from server progress, shows the total and preserves server arrival ordering', async () => {
const nullArrival = { ...summary, sourceContainerBookingId: '2092873463568597002', containerNo: 'CONT-NULL', plannedArrivalAt: null, planCount: 0, pendingPlanCount: 15 }
vi.mocked(listContainerPlanProgress).mockResolvedValueOnce({ records: [summary, nullArrival], page: 1, size: 10, total: 2, pages: 1 })
const wrapper = await mountView()
const containerTab = wrapper.findAll('.el-radio-button').find((item) => item.text().startsWith('柜子'))
await containerTab!.trigger('click')
await flushPromises()
expect(listContainerPlanProgress).toHaveBeenLastCalledWith({ page: 1, size: 10, containerNo: undefined, createdFrom: undefined, createdTo: undefined })
expect(wrapper.text()).toContain('柜子(2)')
expect(wrapper.text()).toContain('计划到仓时间')
expect(wrapper.text()).toContain('已有 15 / 缺 0')
expect(wrapper.text()).toContain('已生成10个计划,剩余5个未操作')
expect(wrapper.text()).toContain('未就绪')
expect(wrapper.text()).toContain('存在5票尚未生成有效入库计划')
expect(wrapper.text().indexOf('CONT-15')).toBeLessThan(wrapper.text().indexOf('CONT-NULL'))
})
it('uses independent cabinet pagination and loads a compact long-id detail drawer', async () => {
vi.mocked(listContainerPlanProgress).mockResolvedValue({ records: [summary], page: 1, size: 10, total: 21, pages: 3 })
const wrapper = await mountView()
const containerTab = wrapper.findAll('.el-radio-button').find((item) => item.text().startsWith('柜子'))
await containerTab!.trigger('click')
await flushPromises()
const pageTwo = wrapper.find('button[aria-label="第 2 页"]')
await pageTwo.trigger('click')
await flushPromises()
expect(listContainerPlanProgress).toHaveBeenLastCalledWith(expect.objectContaining({ page: 2, size: 10 }))
const viewButtons = wrapper.findAll('button').filter((item) => item.text() === '查看')
const viewButton = viewButtons[viewButtons.length - 1]
await viewButton!.trigger('click')
await flushPromises()
expect(getContainerPlanProgress).toHaveBeenCalledWith(bookingId)
expect(document.body.textContent).toContain('柜级入库计划详情')
expect(document.body.textContent).toContain('LMO-1')
expect(document.body.textContent).toContain('IN-1')
expect(document.body.textContent).toContain('LIP-1')
})
it('loads only active FBA master data and submits exact string fields for a cross-customer transfer', async () => {
const wrapper = await openPending()
await selectRows(wrapper, [selectedRow(), selectedRow({ id: '2092873463568596995', customerId: '2092618717861048322' })])
await button(wrapper, '转仓')!.trigger('click')
await flushPromises()
expect(listAllPlatformWarehouses).toHaveBeenCalledWith({ warehouseType: 'FBA', status: 'ACTIVE' })
const warehouseSelect = wrapper.findAllComponents({ name: 'ElSelect' }).find((item) => item.props('placeholder') === '请选择目的 FBA 仓')!
expect(warehouseSelect).toBeDefined()
const option = wrapper.findAllComponents({ name: 'ElOption' }).find((item) => item.props('value') === '9007199254740993')!
expect(option.props('label')).toBe('ONT8 · Ontario FBA · CA / Ontario')
warehouseSelect.vm.$emit('update:modelValue', '9007199254740993')
await wrapper.vm.$nextTick()
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认转仓')) as HTMLButtonElement
expect(confirm.disabled).toBe(false)
confirm.click()
await flushPromises()
expect(transferLastMileOrders).toHaveBeenCalledWith({
orderIds: ['2092873463568596994', '2092873463568596995'],
warehouseId: '9007199254740993', warehouseCode: 'ONT8', warehouseName: 'Ontario FBA', remark: undefined,
})
expect(vi.mocked(transferLastMileOrders).mock.calls[0]?.[0].warehouseId).toBeTypeOf('string')
expect(wrapper.html()).not.toContain('请输入目的仓 ID')
})
it('clears all selected warehouse facts when the FBA candidate is cleared', async () => {
const wrapper = await openPending()
await selectRows(wrapper, [selectedRow()])
await button(wrapper, '转仓')!.trigger('click')
await flushPromises()
const warehouseSelect = wrapper.findAllComponents({ name: 'ElSelect' }).find((item) => item.props('placeholder') === '请选择目的 FBA 仓')!
warehouseSelect.vm.$emit('update:modelValue', '9007199254740993')
await wrapper.vm.$nextTick()
warehouseSelect.vm.$emit('update:modelValue', '')
await wrapper.vm.$nextTick()
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认转仓')) as HTMLButtonElement
expect(confirm.disabled).toBe(true)
expect(transferLastMileOrders).not.toHaveBeenCalled()
})
it('renders an explicit empty state when there are no active FBA candidates', async () => {
vi.mocked(listAllPlatformWarehouses).mockResolvedValueOnce([])
const wrapper = await openPending()
await selectRows(wrapper, [selectedRow()])
await button(wrapper, '转仓')!.trigger('click')
await flushPromises()
expect(document.body.textContent).toContain('暂无可用 FBA 仓候选')
expect(transferLastMileOrders).not.toHaveBeenCalled()
})
it('keeps candidate load request metadata visible and prevents free-form fallback', async () => {
vi.mocked(listAllPlatformWarehouses).mockRejectedValueOnce(new ApiResponseError(
'FBA candidate failed', 'A0503', 200, undefined, 'request-fba', 'trace-fba',
))
const wrapper = await openPending()
await selectRows(wrapper, [selectedRow()])
await button(wrapper, '转仓')!.trigger('click')
await flushPromises()
expect(document.body.textContent).toContain('A0503')
expect(document.body.textContent).toContain('request-fba')
expect(document.body.textContent).toContain('trace-fba')
expect(wrapper.html()).not.toContain('请输入目的仓 ID')
expect(wrapper.html()).not.toContain('请输入目的仓代码')
expect(wrapper.html()).not.toContain('请输入目的仓名称')
expect(transferLastMileOrders).not.toHaveBeenCalled()
})
})