2026-08-04 21:01:39 +08:00
|
|
|
|
import ElementPlus from 'element-plus'
|
|
|
|
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
|
|
|
|
import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|
|
|
|
|
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
|
|
|
|
import { useAuthStore } from '@/core/auth/auth'
|
|
|
|
|
|
import {
|
2026-08-06 14:55:17 +08:00
|
|
|
|
createPlatformLocation,
|
2026-08-04 21:01:39 +08:00
|
|
|
|
getPlatformLocation,
|
|
|
|
|
|
listPlatformLocations,
|
|
|
|
|
|
type PlatformLocationResource,
|
|
|
|
|
|
} from '../api/platformLocations'
|
2026-08-06 14:55:17 +08:00
|
|
|
|
import { listAllPlatformWarehouses, type PlatformWarehouseResource } from '../../../warehouses/api/platformWarehouses'
|
2026-08-04 21:01:39 +08:00
|
|
|
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
|
|
|
|
|
import DataTable from '@/shared/components/DataTable.vue'
|
|
|
|
|
|
import LocationsView from './LocationsView.vue'
|
|
|
|
|
|
|
2026-08-06 14:55:17 +08:00
|
|
|
|
vi.mock('../../../warehouses/api/platformWarehouses', () => ({ listAllPlatformWarehouses: vi.fn() }))
|
2026-08-04 21:01:39 +08:00
|
|
|
|
vi.mock('../api/platformLocations', () => ({
|
|
|
|
|
|
listPlatformLocations: vi.fn(),
|
|
|
|
|
|
getPlatformLocation: vi.fn(),
|
|
|
|
|
|
createPlatformLocation: vi.fn(),
|
|
|
|
|
|
updatePlatformLocation: vi.fn(),
|
|
|
|
|
|
changePlatformLocationStatus: vi.fn(),
|
|
|
|
|
|
deletePlatformLocation: vi.fn(),
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
class ResizeObserverStub implements ResizeObserver {
|
|
|
|
|
|
observe() {}
|
|
|
|
|
|
unobserve() {}
|
|
|
|
|
|
disconnect() {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createMemoryStorage(): 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),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-06 13:56:04 +08:00
|
|
|
|
const tenantId = '0'
|
2026-08-04 21:01:39 +08:00
|
|
|
|
const warehouseId = '9223372036854775805'
|
|
|
|
|
|
const warehouse: PlatformWarehouseResource = {
|
|
|
|
|
|
id: warehouseId,
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
warehouseCode: 'CN-YW-01',
|
|
|
|
|
|
warehouseName: '义乌收货仓',
|
|
|
|
|
|
warehouseType: 'DOMESTIC',
|
|
|
|
|
|
countryCode: 'CN',
|
|
|
|
|
|
timezone: 'Asia/Shanghai',
|
|
|
|
|
|
contactName: null,
|
|
|
|
|
|
contactPhone: null,
|
|
|
|
|
|
contactEmail: null,
|
|
|
|
|
|
status: 'ACTIVE',
|
|
|
|
|
|
remark: null,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-06 14:55:17 +08:00
|
|
|
|
const fbaWarehouse: PlatformWarehouseResource = {
|
|
|
|
|
|
...warehouse,
|
|
|
|
|
|
id: '9223372036854775802',
|
|
|
|
|
|
warehouseCode: 'FBA-ONT8',
|
|
|
|
|
|
warehouseName: 'ONT8 FBA 仓',
|
|
|
|
|
|
warehouseType: 'FBA',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const overseasWarehouse: PlatformWarehouseResource = {
|
|
|
|
|
|
...warehouse,
|
|
|
|
|
|
id: '9223372036854775801',
|
|
|
|
|
|
warehouseCode: 'US-LAX-01',
|
|
|
|
|
|
warehouseName: '洛杉矶海外仓',
|
|
|
|
|
|
warehouseType: 'OVERSEAS',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-04 21:01:39 +08:00
|
|
|
|
const availableLocation: PlatformLocationResource = {
|
|
|
|
|
|
id: '9223372036854775804',
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
warehouseId,
|
|
|
|
|
|
zoneCode: 'A',
|
|
|
|
|
|
zoneName: '常规区',
|
|
|
|
|
|
locationCode: 'A1-01',
|
|
|
|
|
|
locationName: 'A1 区 01 位',
|
|
|
|
|
|
locationType: 'SHELF',
|
|
|
|
|
|
maxWeightKg: '500.000',
|
|
|
|
|
|
sharedOverflow: false,
|
|
|
|
|
|
status: 'AVAILABLE',
|
|
|
|
|
|
remark: '靠近入库口',
|
|
|
|
|
|
version: 3,
|
|
|
|
|
|
updatedAt: '2026-08-04T10:20:00',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const occupiedLocation: PlatformLocationResource = {
|
|
|
|
|
|
...availableLocation,
|
|
|
|
|
|
id: '9223372036854775803',
|
|
|
|
|
|
locationCode: 'A1-02',
|
|
|
|
|
|
locationName: 'A1 区 02 位',
|
|
|
|
|
|
status: 'OCCUPIED',
|
|
|
|
|
|
version: 5,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function mountView() {
|
|
|
|
|
|
vi.stubGlobal('localStorage', createMemoryStorage())
|
|
|
|
|
|
vi.stubGlobal('sessionStorage', createMemoryStorage())
|
|
|
|
|
|
const pinia = createPinia()
|
|
|
|
|
|
setActivePinia(pinia)
|
|
|
|
|
|
const auth = useAuthStore()
|
|
|
|
|
|
auth.token = 'token'
|
|
|
|
|
|
auth.permissions = [
|
|
|
|
|
|
'platform:location:list',
|
|
|
|
|
|
'platform:location:create',
|
|
|
|
|
|
'platform:location:edit',
|
|
|
|
|
|
'platform:location:status',
|
|
|
|
|
|
'platform:location:delete',
|
|
|
|
|
|
]
|
|
|
|
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
|
|
|
|
const wrapper = mount(LocationsView, {
|
|
|
|
|
|
attachTo: document.body,
|
|
|
|
|
|
global: { plugins: [pinia, ElementPlus] },
|
|
|
|
|
|
})
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function selectScope(wrapper: VueWrapper) {
|
2026-08-05 15:54:59 +08:00
|
|
|
|
const toolbar = wrapper.findComponent(QueryToolbar)
|
2026-08-04 21:01:39 +08:00
|
|
|
|
toolbar.vm.$emit('update:modelValue', { ...(toolbar.props('modelValue') as QueryModel), warehouseId })
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
const searchButton = toolbar.findAll('button').find((button) => button.text().trim() === '查询')
|
|
|
|
|
|
await searchButton!.trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enableAutoUnmount(afterEach)
|
|
|
|
|
|
afterAll(() => vi.unstubAllGlobals())
|
|
|
|
|
|
|
|
|
|
|
|
describe('LocationsView', () => {
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
|
|
|
|
|
document.body.innerHTML = ''
|
2026-08-06 14:55:17 +08:00
|
|
|
|
vi.mocked(listAllPlatformWarehouses).mockReset().mockResolvedValue([warehouse, fbaWarehouse, overseasWarehouse])
|
2026-08-04 21:01:39 +08:00
|
|
|
|
vi.mocked(listPlatformLocations).mockReset().mockResolvedValue({
|
|
|
|
|
|
records: [availableLocation, occupiedLocation], page: 1, size: 20, total: 2, pages: 1,
|
|
|
|
|
|
})
|
|
|
|
|
|
vi.mocked(getPlatformLocation).mockReset().mockResolvedValue({
|
|
|
|
|
|
location: availableLocation, warehouseCode: warehouse.warehouseCode, warehouseName: warehouse.warehouseName,
|
|
|
|
|
|
})
|
2026-08-06 14:55:17 +08:00
|
|
|
|
vi.mocked(createPlatformLocation).mockReset()
|
2026-08-04 21:01:39 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-06 15:32:02 +08:00
|
|
|
|
it('loads only domestic warehouse candidates for querying and writes', async () => {
|
2026-08-04 21:01:39 +08:00
|
|
|
|
const wrapper = await mountView()
|
|
|
|
|
|
const toolbar = wrapper.findComponent(QueryToolbar)
|
|
|
|
|
|
const fields = toolbar.props('fields') as QueryField[]
|
|
|
|
|
|
|
2026-08-05 15:54:59 +08:00
|
|
|
|
expect(wrapper.find('[aria-label="所属企业"]').exists()).toBe(false)
|
|
|
|
|
|
expect(fields[0]?.placeholder).toBe('全部仓库')
|
2026-08-06 14:55:17 +08:00
|
|
|
|
expect(fields[0]?.options).toEqual([
|
|
|
|
|
|
{ value: warehouseId, label: '义乌收货仓(CN-YW-01)' },
|
|
|
|
|
|
])
|
2026-08-04 21:01:39 +08:00
|
|
|
|
expect(fields.map(({ key, width }) => [key, width])).toEqual([
|
2026-08-05 15:54:59 +08:00
|
|
|
|
['warehouseId', 300], ['zoneCode', 210],
|
2026-08-04 21:01:39 +08:00
|
|
|
|
['locationCode', 220], ['locationType', 170], ['status', 160],
|
|
|
|
|
|
])
|
2026-08-06 15:32:02 +08:00
|
|
|
|
expect(listAllPlatformWarehouses).toHaveBeenCalledWith({ warehouseType: 'DOMESTIC' })
|
2026-08-04 21:01:39 +08:00
|
|
|
|
expect(listPlatformLocations).toHaveBeenCalledWith({
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
size: 20,
|
|
|
|
|
|
zoneCode: '',
|
|
|
|
|
|
locationCode: '',
|
|
|
|
|
|
locationType: '',
|
|
|
|
|
|
status: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
const searchButton = toolbar.findAll('button').find((button) => button.text().trim() === '查询')
|
|
|
|
|
|
await searchButton!.trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
expect(listPlatformLocations).toHaveBeenCalledTimes(2)
|
|
|
|
|
|
expect(wrapper.text()).not.toContain('请选择客户')
|
|
|
|
|
|
expect(wrapper.text()).not.toContain('请选择所属企业')
|
|
|
|
|
|
expect(wrapper.text()).not.toContain('租户')
|
|
|
|
|
|
expect(wrapper.text()).not.toContain('企业 ID')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-05 15:54:59 +08:00
|
|
|
|
it('filters by the selected warehouse without exposing or sending a customer id', async () => {
|
2026-08-04 21:01:39 +08:00
|
|
|
|
const wrapper = await mountView()
|
|
|
|
|
|
await selectScope(wrapper)
|
|
|
|
|
|
|
2026-08-06 20:14:56 +08:00
|
|
|
|
const tableToolbar = wrapper.find('.table-panel .data-table__toolbar')
|
|
|
|
|
|
expect(tableToolbar.exists()).toBe(true)
|
|
|
|
|
|
expect(tableToolbar.text()).toContain('库位列表')
|
|
|
|
|
|
expect(tableToolbar.text()).toContain('共 2 个库位')
|
|
|
|
|
|
expect(tableToolbar.text()).toContain('新增库位')
|
2026-08-05 17:10:10 +08:00
|
|
|
|
expect(wrapper.findComponent(QueryToolbar).text()).not.toContain('新增库位')
|
2026-08-06 15:32:02 +08:00
|
|
|
|
expect(listAllPlatformWarehouses).toHaveBeenCalledWith({ warehouseType: 'DOMESTIC' })
|
2026-08-04 21:01:39 +08:00
|
|
|
|
expect(listPlatformLocations).toHaveBeenLastCalledWith({
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
size: 20,
|
|
|
|
|
|
warehouseId,
|
|
|
|
|
|
zoneCode: '',
|
|
|
|
|
|
locationCode: '',
|
|
|
|
|
|
locationType: '',
|
|
|
|
|
|
status: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
expect(wrapper.text()).toContain('义乌收货仓')
|
|
|
|
|
|
expect(wrapper.text()).toContain('A1-01')
|
|
|
|
|
|
expect(wrapper.findAll('thead th').map((item) => item.text().trim())).toEqual([
|
|
|
|
|
|
'操作', '库位编码', '所属仓库', '库区', '库位类型', '最大承重(kg)', '状态', '备注',
|
|
|
|
|
|
])
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-05 15:54:59 +08:00
|
|
|
|
it('opens creation without a list filter and requires the warehouse inside the form', async () => {
|
|
|
|
|
|
const wrapper = await mountView()
|
2026-08-06 20:14:56 +08:00
|
|
|
|
const createButton = wrapper.find('.table-panel .data-table__toolbar').findAll('button').find((button) => button.text().trim() === '新增库位')
|
2026-08-05 15:54:59 +08:00
|
|
|
|
|
|
|
|
|
|
expect(createButton?.attributes('disabled')).toBeUndefined()
|
|
|
|
|
|
await createButton!.trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
expect(document.body.textContent).toContain('新增库位')
|
|
|
|
|
|
expect(document.body.textContent).toContain('所属仓库')
|
2026-08-06 14:55:17 +08:00
|
|
|
|
expect(document.body.textContent).toContain('请选择国内仓')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('does not use an FBA warehouse filter as the creation scope', async () => {
|
|
|
|
|
|
const wrapper = await mountView()
|
|
|
|
|
|
const toolbar = wrapper.findComponent(QueryToolbar)
|
|
|
|
|
|
toolbar.vm.$emit('update:modelValue', { ...(toolbar.props('modelValue') as QueryModel), warehouseId: fbaWarehouse.id })
|
|
|
|
|
|
await flushPromises()
|
2026-08-06 20:14:56 +08:00
|
|
|
|
const createButton = wrapper.find('.table-panel .data-table__toolbar').findAll('button').find((button) => button.text().trim() === '新增库位')
|
2026-08-06 14:55:17 +08:00
|
|
|
|
await createButton!.trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
const saveButton = [...document.body.querySelectorAll('button')].find((button) => button.textContent?.trim() === '保存库位')
|
|
|
|
|
|
saveButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
expect(createPlatformLocation).not.toHaveBeenCalled()
|
2026-08-05 15:54:59 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-04 21:01:39 +08:00
|
|
|
|
it('keeps the operation column first and hides edit actions for occupied resources', async () => {
|
|
|
|
|
|
const wrapper = await mountView()
|
|
|
|
|
|
await selectScope(wrapper)
|
|
|
|
|
|
const headings = wrapper.findAll('thead th').map((item) => item.text().trim())
|
|
|
|
|
|
expect(headings[0]).toBe('操作')
|
|
|
|
|
|
|
|
|
|
|
|
const bodyRows = wrapper.findAll('tbody tr')
|
|
|
|
|
|
expect(bodyRows[0]?.text()).toContain('编辑')
|
|
|
|
|
|
expect(bodyRows[1]?.text()).toContain('详情')
|
|
|
|
|
|
expect(bodyRows[1]?.text()).not.toContain('编辑')
|
|
|
|
|
|
expect(bodyRows[1]?.text()).not.toContain('更多')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-06 14:55:17 +08:00
|
|
|
|
it('does not offer editing for a location outside a domestic warehouse', async () => {
|
|
|
|
|
|
vi.mocked(listPlatformLocations).mockResolvedValue({
|
|
|
|
|
|
records: [{ ...availableLocation, warehouseId: fbaWarehouse.id }, availableLocation], page: 1, size: 20, total: 2, pages: 1,
|
|
|
|
|
|
})
|
|
|
|
|
|
const wrapper = await mountView()
|
|
|
|
|
|
|
|
|
|
|
|
const bodyRows = wrapper.findAll('tbody tr')
|
|
|
|
|
|
expect(bodyRows[0]?.text()).toContain('详情')
|
|
|
|
|
|
expect(bodyRows[0]?.text()).not.toContain('编辑')
|
|
|
|
|
|
expect(bodyRows[1]?.text()).toContain('编辑')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-04 21:01:39 +08:00
|
|
|
|
it('opens an authenticated detail view with warehouse context', async () => {
|
|
|
|
|
|
const wrapper = await mountView()
|
|
|
|
|
|
const detailButton = wrapper.find('.el-table__body-wrapper tbody tr button')
|
|
|
|
|
|
expect(detailButton.text()).toBe('详情')
|
|
|
|
|
|
await detailButton.trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
2026-08-06 15:32:02 +08:00
|
|
|
|
expect(getPlatformLocation).toHaveBeenCalledWith(warehouseId, availableLocation.id)
|
2026-08-04 21:01:39 +08:00
|
|
|
|
expect(document.body.textContent).toContain('库位详情')
|
|
|
|
|
|
expect(document.body.textContent).toContain('义乌收货仓(CN-YW-01)')
|
2026-08-06 13:56:04 +08:00
|
|
|
|
expect(document.body.textContent).toContain('资源作用域平台公共资源')
|
|
|
|
|
|
expect(document.body.textContent).not.toContain('所属企业')
|
2026-08-04 21:01:39 +08:00
|
|
|
|
expect(document.body.textContent).not.toContain('企业 ID')
|
|
|
|
|
|
expect(document.body.textContent).toContain('库位名称A1 区 01 位')
|
|
|
|
|
|
expect(document.body.textContent).toContain('v3')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('presents occupied locations as read-only business resources', async () => {
|
|
|
|
|
|
vi.mocked(getPlatformLocation).mockResolvedValueOnce({
|
|
|
|
|
|
location: occupiedLocation,
|
|
|
|
|
|
warehouseCode: warehouse.warehouseCode,
|
|
|
|
|
|
warehouseName: warehouse.warehouseName,
|
|
|
|
|
|
})
|
|
|
|
|
|
const wrapper = await mountView()
|
|
|
|
|
|
const occupiedRow = wrapper.findAll('tbody tr')[1]
|
|
|
|
|
|
await occupiedRow!.find('button').trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
expect(document.body.textContent).toContain('状态已占用')
|
|
|
|
|
|
expect(document.body.textContent).toContain('当前库位正在被业务使用,只允许查看。')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-05 15:54:59 +08:00
|
|
|
|
it('clears the warehouse on reset and immediately restores the full query', async () => {
|
2026-08-04 21:01:39 +08:00
|
|
|
|
const wrapper = await mountView()
|
|
|
|
|
|
await selectScope(wrapper)
|
|
|
|
|
|
const toolbar = wrapper.findComponent(QueryToolbar)
|
|
|
|
|
|
const resetButton = toolbar.findAll('button').find((button) => button.text().trim() === '重置')
|
|
|
|
|
|
|
|
|
|
|
|
await resetButton!.trigger('click')
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
const model = wrapper.findComponent(QueryToolbar).props('modelValue') as QueryModel
|
|
|
|
|
|
expect(model.warehouseId).toBe('')
|
|
|
|
|
|
expect(listPlatformLocations).toHaveBeenLastCalledWith({
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
size: 20,
|
|
|
|
|
|
zoneCode: '',
|
|
|
|
|
|
locationCode: '',
|
|
|
|
|
|
locationType: '',
|
|
|
|
|
|
status: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
expect(wrapper.text()).not.toContain('请选择客户')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('paginates the all-customer list without adding customer or warehouse IDs', async () => {
|
|
|
|
|
|
vi.mocked(listPlatformLocations).mockResolvedValue({
|
|
|
|
|
|
records: [availableLocation, occupiedLocation], page: 1, size: 20, total: 40, pages: 2,
|
|
|
|
|
|
})
|
|
|
|
|
|
const wrapper = await mountView()
|
|
|
|
|
|
wrapper.findComponent(DataTable).vm.$emit('update:page', 2)
|
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
|
|
expect(listPlatformLocations).toHaveBeenLastCalledWith({
|
|
|
|
|
|
page: 2,
|
|
|
|
|
|
size: 20,
|
|
|
|
|
|
zoneCode: '',
|
|
|
|
|
|
locationCode: '',
|
|
|
|
|
|
locationType: '',
|
|
|
|
|
|
status: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|