2026-09-01 14:23:53 +08:00
|
|
|
import ElementPlus from 'element-plus'
|
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
|
import { resolve } from 'node:path'
|
|
|
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
|
import { listOutbounds } from '../api/outbounds'
|
|
|
|
|
import OutboundsView from './OutboundsView.vue'
|
2026-09-15 18:17:17 +08:00
|
|
|
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
2026-09-01 14:23:53 +08:00
|
|
|
|
|
|
|
|
const canPermission = vi.hoisted(() => vi.fn((_code: string) => true))
|
|
|
|
|
const routerPush = vi.hoisted(() => vi.fn())
|
|
|
|
|
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canPermission }) }))
|
|
|
|
|
vi.mock('vue-router', () => ({ useRouter: () => ({ push: routerPush }) }))
|
|
|
|
|
vi.mock('../api/outbounds', () => ({ listOutbounds: vi.fn(), cancelOutbound: vi.fn(), createOneClickDropshipOutbound: vi.fn() }))
|
|
|
|
|
vi.mock('../api/inventory', () => ({ listInventoryBalances: vi.fn(), listInventoryPositions: vi.fn() }))
|
|
|
|
|
|
|
|
|
|
class ResizeObserverStub { observe() {} unobserve() {} disconnect() {} }
|
|
|
|
|
|
|
|
|
|
describe('OutboundsView permissions and navigation', () => {
|
2026-09-15 18:17:17 +08:00
|
|
|
it('distinguishes outbound stages and does not present unknown states as picking', async () => {
|
|
|
|
|
const statuses = [
|
|
|
|
|
['WAIT_OUTBOUND', '待出库', 'warning'], ['PICKING', '拣货中', 'info'],
|
|
|
|
|
['PICKED', '已拣货', 'warning'], ['OUTBOUNDED', '已出库', 'success'],
|
|
|
|
|
['CANCELLED', '已取消', 'danger'], ['FUTURE_STATE', '未知状态', 'neutral'],
|
|
|
|
|
]
|
|
|
|
|
vi.mocked(listOutbounds).mockResolvedValueOnce({ records: statuses.map(([status], index) => ({ id: String(index), outboundNo: `OUT-${index}`, status })), page: 1, size: 20, total: statuses.length, pages: 1 })
|
|
|
|
|
const wrapper = mount(OutboundsView, { global: { plugins: [ElementPlus] } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
const badges = wrapper.findAllComponents(StatusBadge).filter(badge => badge.element.closest('tbody'))
|
|
|
|
|
expect(badges).toHaveLength(statuses.length)
|
|
|
|
|
statuses.forEach(([, label, tone], index) => {
|
|
|
|
|
expect(badges[index].text()).toBe(label)
|
|
|
|
|
expect(badges[index].classes()).toContain(`is-${tone}`)
|
|
|
|
|
})
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-01 14:23:53 +08:00
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
|
|
|
|
vi.stubGlobal('localStorage', { getItem: () => null, setItem: () => undefined, removeItem: () => undefined })
|
|
|
|
|
vi.mocked(listOutbounds).mockReset().mockResolvedValue({ records: [{ id: '1', outboundNo: 'OUT-1', dispatchNo: 'PC-1', status: 'PICKED', palletCount: 1, packageCount: 2 }], page: 1, size: 20, total: 1, pages: 1 })
|
|
|
|
|
canPermission.mockReset().mockReturnValue(true)
|
|
|
|
|
routerPush.mockReset()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('uses list access for detail and independent create/cancel permissions', async () => {
|
|
|
|
|
canPermission.mockImplementation((code: string) => code.endsWith(':list'))
|
|
|
|
|
const wrapper = mount(OutboundsView, { global: { plugins: [ElementPlus] } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(wrapper.text()).toContain('详情')
|
2026-09-09 17:17:05 +08:00
|
|
|
expect(wrapper.text()).not.toContain('新建出库单')
|
2026-09-01 14:23:53 +08:00
|
|
|
expect(wrapper.text()).not.toContain('取消')
|
|
|
|
|
expect(OutboundsView.__file).toContain('OutboundsView.vue')
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-09 17:17:05 +08:00
|
|
|
it('shows outbound creation and cancellation only with their exact permissions', async () => {
|
2026-09-01 14:23:53 +08:00
|
|
|
canPermission.mockImplementation((code: string) => ['platform:overseas-warehouse:outbound:create', 'platform:overseas-warehouse:outbound:cancel'].includes(code))
|
|
|
|
|
const wrapper = mount(OutboundsView, { global: { plugins: [ElementPlus] } })
|
|
|
|
|
await flushPromises()
|
2026-09-10 15:33:54 +08:00
|
|
|
expect(wrapper.text()).toContain('新建出库单')
|
2026-09-01 14:23:53 +08:00
|
|
|
expect(wrapper.text()).toContain('取消')
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-10 15:33:54 +08:00
|
|
|
it('loads one-click candidates across customers and loads return inventory after customer selection', async () => {
|
2026-09-01 14:23:53 +08:00
|
|
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/pages/OutboundsView.vue'), 'utf8')
|
|
|
|
|
expect(source).toContain('v-model="selectedCustomerId"')
|
2026-09-10 15:33:54 +08:00
|
|
|
expect(source).toContain('listOneClickOutboundCandidates(selectedCustomerId.value || undefined')
|
|
|
|
|
expect(source).toContain('if (selectedCustomerId.value && selectedWarehouseId.value) await loadReturnCandidates()')
|
|
|
|
|
expect(source).toContain("createOneClickDropshipOutbound([...customerIds][0] || ''")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('applies batched return filters only through explicit search and exposes page sizes', () => {
|
|
|
|
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/pages/OutboundsView.vue'), 'utf8')
|
|
|
|
|
expect(source).toContain('returnSkuQueryInput')
|
|
|
|
|
expect(source).toContain('returnCartonQueryInput')
|
|
|
|
|
expect(source).toContain("item.replace(/\\s+/g, '')")
|
|
|
|
|
expect(source).toContain('@click="searchReturnSkus"')
|
|
|
|
|
expect(source).toContain('@click="searchReturnCartons"')
|
|
|
|
|
expect(source).toContain(':page-sizes="[5, 10, 20, 50]"')
|
|
|
|
|
expect(source).toContain('width="min(1320px, calc(100vw - 32px))"')
|
2026-09-01 14:23:53 +08:00
|
|
|
})
|
|
|
|
|
})
|