Merge remote-tracking branch 'origin/release/test' into dev

# Conflicts:
#	src/modules/container-loading/pages/ContainerDetailView.emptyReturn.behavior.test.ts
#	src/modules/container-loading/pages/ContainersView.test.ts
This commit is contained in:
2026-09-09 13:02:53 +08:00
7 changed files with 395 additions and 25 deletions
@@ -1,6 +1,6 @@
import ElementPlus from 'element-plus'
import { createPinia, setActivePinia } from 'pinia'
import { createMemoryHistory, createRouter } from 'vue-router'
import { createMemoryHistory, createRouter, RouterView } from 'vue-router'
import { defineComponent } from 'vue'
import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@@ -14,6 +14,7 @@ import { getOceanBooking, listOceanBookingLinkCandidates, listOceanBookings } fr
import { getContainer, getContainerOverview, linkContainerOceanBooking, listContainers, updateContainer } from '../api/platformContainerLoading'
import { getTerminalDrayage, getTerminalDrayageHistory, recordTerminalDrayageEmptyReturn, selectTerminalDrayageFleet } from '../api/terminalDrayages'
import AppDialog from '@/shared/components/AppDialog.vue'
import { containerLoadingRoutes } from '../routes'
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn() }))
vi.mock('@/modules/master-data', () => ({ listPlatformPartners: vi.fn() }))
@@ -43,6 +44,18 @@ function createTestRouter() {
] })
}
async function mountOverseasRoute() {
const router = createRouter({
history: createMemoryHistory(),
routes: containerLoadingRoutes.map((route) => ({ ...route, path: `/${route.path}` })),
})
await router.push('/container-loading/overseas')
await router.isReady()
const wrapper = mount(RouterView, { attachTo: document.body, global: { plugins: [ElementPlus, router] } })
await flushPromises()
return wrapper
}
describe('ContainersView', () => {
beforeEach(() => {
setActivePinia(createPinia())
@@ -77,10 +90,8 @@ describe('ContainersView', () => {
})
it('shows one direct empty-return action only when permission and server actions allow it', async () => {
const router = createTestRouter()
await router.push('/'); await router.isReady()
const wrapper = mount(ContainersView, { props: { workbench: 'OVERSEAS' }, attachTo: document.body, global: { plugins: [ElementPlus, router] } })
await flushPromises()
const wrapper = await mountOverseasRoute()
expect(listContainers).toHaveBeenCalledWith(expect.objectContaining({ scope: 'ACTIVE' }), 'OVERSEAS')
const buttons = wrapper.findAll('button').filter((item) => item.text().includes('确认还柜'))
expect(buttons).toHaveLength(1)
await buttons[0].trigger('click')
@@ -102,6 +113,48 @@ describe('ContainersView', () => {
wrapper.unmount()
})
it.each(['permission', 'row action', 'nested action', 'nested object', 'already returned', 'historical unloaded snapshot'] as const)(
'keeps empty return unavailable on the real overseas route: %s',
async (missing) => {
const page = await listContainers({ page: 1, size: 20 }, 'OVERSEAS')
vi.mocked(listContainers).mockClear()
const nested = missing === 'nested object' ? null : {
...drayage,
allowedActions: missing === 'nested action' || missing === 'already returned' ? [] : [...drayage.allowedActions],
...(missing === 'already returned' ? { status: 'EMPTY_RETURNED' as const, actualEmptyReturnAt: '2026-09-11T13:00:00' } : {}),
...(missing === 'historical unloaded snapshot' ? { status: 'ARRIVED' as const, version: 0, actualPickupAt: null, fleetSupplierId: null, fleetSupplierName: null, allowedActions: ['SELECT_FLEET' as const] } : {}),
}
vi.mocked(listContainers).mockResolvedValue({ ...page, records: page.records.map((row) => ({
...row,
allowedActions: missing === 'row action' || missing === 'already returned' ? [] : ['RECORD_EMPTY_RETURN'],
terminalDrayage: nested,
...(missing === 'historical unloaded snapshot' ? { workbenchStage: 'UNLOADED', allowedActions: ['MANAGE_DESTINATION_DOCUMENTS' as const, 'SELECT_FLEET' as const] } : {}),
})) })
if (missing === 'permission') {
accessCan.mockImplementation((code) => code !== 'platform:firstleg:terminal-drayage:empty-return:confirm')
}
const wrapper = await mountOverseasRoute()
try {
expect(listContainers).toHaveBeenCalledTimes(1)
expect(listContainers).toHaveBeenCalledWith(expect.objectContaining({ scope: 'ACTIVE' }), 'OVERSEAS')
expect(wrapper.text()).toContain('MSCU-1')
expect(wrapper.findAll('.row-actions button').some((button) => button.text() === '确认还柜')).toBe(false)
expect(wrapper.findAllComponents(AppDialog).some((dialog) => dialog.props('title') === '确认还柜' && dialog.props('modelValue'))).toBe(false)
expect(recordTerminalDrayageEmptyReturn).not.toHaveBeenCalled()
expect(getTerminalDrayage).not.toHaveBeenCalled()
if (missing === 'historical unloaded snapshot') {
const fleet = wrapper.findAllComponents({ name: 'ElSelect' }).find((select) =>
select.props('placeholder') === '请选择车队' && select.element.closest('.el-table__body'),
)
expect(fleet).toBeDefined()
expect(fleet!.props('disabled')).toBe(false)
}
} finally {
wrapper.unmount()
}
},
)
it('cancels empty return without writing and preserves input on failure while preventing duplicate requests', async () => {
const router = createTestRouter()
await router.push('/'); await router.isReady()