Files
admin-web/src/modules/warehouse-resources/locations/pages/LocationsView.test.ts
T

342 lines
13 KiB
TypeScript
Raw Normal View History

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 14:55:17 +08:00
it('loads all warehouse candidates for querying while restricting write scope to domestic warehouses', 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)' },
{ value: fbaWarehouse.id, label: 'ONT8 FBA 仓(FBA-ONT8)' },
{ value: overseasWarehouse.id, label: '洛杉矶海外仓(US-LAX-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 14:55:17 +08:00
expect(listAllPlatformWarehouses).toHaveBeenCalledWith()
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-05 17:10:10 +08:00
const panelHeading = wrapper.find('.table-panel .panel-heading')
expect(panelHeading.exists()).toBe(true)
expect(panelHeading.text()).toContain('库位列表')
expect(panelHeading.text()).toContain('共 2 个库位')
expect(panelHeading.text()).toContain('新增库位')
expect(wrapper.findComponent(QueryToolbar).text()).not.toContain('新增库位')
2026-08-06 14:55:17 +08:00
expect(listAllPlatformWarehouses).toHaveBeenCalledWith()
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-05 17:10:10 +08:00
const createButton = wrapper.find('.table-panel .panel-heading').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()
const createButton = wrapper.find('.table-panel .panel-heading').findAll('button').find((button) => button.text().trim() === '新增库位')
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()
expect(getPlatformLocation).toHaveBeenCalledWith(tenantId, warehouseId, availableLocation.id)
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: '',
})
})
})