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

415 lines
16 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, 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 {
createPlatformLocation,
getPlatformLocation,
listPlatformLocations,
type PlatformLocationResource,
} from '../api/platformLocations'
import { listAllPlatformWarehouses, type PlatformWarehouseResource } from '../../../warehouses/api/platformWarehouses'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import DataTable from '@/shared/components/DataTable.vue'
import LocationsView from './LocationsView.vue'
vi.mock('../../../warehouses/api/platformWarehouses', () => ({ listAllPlatformWarehouses: vi.fn() }))
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),
}
}
const tenantId = '0'
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,
}
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',
}
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(permissions = [
'platform:location:list',
'platform:location:create',
'platform:location:edit',
'platform:location:status',
'platform:location:delete',
]) {
vi.stubGlobal('localStorage', createMemoryStorage())
vi.stubGlobal('sessionStorage', createMemoryStorage())
const pinia = createPinia()
setActivePinia(pinia)
const auth = useAuthStore()
auth.token = 'token'
auth.permissions = permissions
useAccessPreviewStore().initializeAfterLogin()
const wrapper = mount(LocationsView, {
attachTo: document.body,
global: { plugins: [pinia, ElementPlus] },
})
await flushPromises()
return wrapper
}
async function selectScope(wrapper: VueWrapper) {
const toolbar = wrapper.findComponent(QueryToolbar)
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 = ''
vi.mocked(listAllPlatformWarehouses).mockReset().mockResolvedValue([warehouse, fbaWarehouse, overseasWarehouse])
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,
})
vi.mocked(createPlatformLocation).mockReset()
})
it('loads only domestic warehouse candidates for querying and writes', async () => {
const wrapper = await mountView()
const toolbar = wrapper.findComponent(QueryToolbar)
const fields = toolbar.props('fields') as QueryField[]
expect(wrapper.find('[aria-label="所属企业"]').exists()).toBe(false)
expect(fields[0]?.placeholder).toBe('全部仓库')
expect(fields[0]?.options).toEqual([
{ value: warehouseId, label: '义乌收货仓(CN-YW-01)' },
])
expect(fields.map(({ key, width }) => [key, width])).toEqual([
['warehouseId', 300], ['zoneCode', 210],
['locationCode', 220], ['locationType', 170], ['status', 160],
])
expect(listAllPlatformWarehouses).toHaveBeenCalledWith({ warehouseType: 'DOMESTIC' })
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')
})
it('filters by the selected warehouse without exposing or sending a customer id', async () => {
const wrapper = await mountView()
await selectScope(wrapper)
const tableToolbar = wrapper.find('.table-panel .data-table__toolbar')
expect(tableToolbar.exists()).toBe(true)
expect(tableToolbar.text()).toContain('新增库位')
expect(tableToolbar.text()).not.toContain('库位列表')
expect(tableToolbar.text()).not.toContain('共 2 个库位')
expect(wrapper.find('.data-table__pagination-left').text()).toContain('共 2 个库位')
expect(wrapper.findComponent(QueryToolbar).text()).not.toContain('新增库位')
expect(listAllPlatformWarehouses).toHaveBeenCalledWith({ warehouseType: 'DOMESTIC' })
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)', '状态', '备注',
])
})
it('opens creation without a list filter and requires the warehouse inside the form', async () => {
const wrapper = await mountView()
const createButton = wrapper.find('.table-panel .data-table__toolbar').findAll('button').find((button) => button.text().trim() === '新增库位')
expect(createButton?.attributes('disabled')).toBeUndefined()
await createButton!.trigger('click')
await flushPromises()
expect(document.body.textContent).toContain('新增库位')
expect(document.body.textContent).toContain('所属仓库')
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 .data-table__toolbar').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()
})
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('更多')
})
it('shows all available actions directly when a row has no more than three', async () => {
const wrapper = await mountView(['platform:location:list', 'platform:location:status'])
const row = wrapper.find('tbody tr')
expect(row.text()).toContain('详情')
expect(row.text()).toContain('停用')
expect(row.text()).not.toContain('更多')
})
it('keeps the first three actions visible and moves only the fourth action into more', async () => {
vi.mocked(listPlatformLocations).mockResolvedValue({
records: [{ ...availableLocation, status: 'DISABLED' }], page: 1, size: 20, total: 1, pages: 1,
})
const wrapper = await mountView()
const row = wrapper.find('tbody tr')
expect(row.text()).toContain('详情')
expect(row.text()).toContain('编辑')
expect(row.text()).toContain('启用')
expect(row.text()).not.toContain('删除')
expect(row.find('button[aria-label="A1-01的更多操作"]').exists()).toBe(true)
})
it('uses unique fallback labels for more actions across rows', async () => {
vi.mocked(listPlatformLocations).mockResolvedValue({
records: [
{ ...availableLocation, id: 'location-1', locationCode: '', locationName: '东侧库位', status: 'DISABLED' },
{ ...availableLocation, id: 'location-2', locationCode: '', locationName: '', status: 'DISABLED' },
{ ...availableLocation, id: undefined, locationCode: '', locationName: '', status: 'DISABLED' } as unknown as PlatformLocationResource,
],
page: 1,
size: 20,
total: 3,
pages: 1,
})
const wrapper = await mountView()
const moreButtons = wrapper.findAll('button[aria-label$="的更多操作"]')
const labels = moreButtons.map((button) => button.attributes('aria-label'))
expect(labels).toEqual(['东侧库位的更多操作', 'location-2的更多操作', '当前库位的更多操作'])
expect(moreButtons.map((button) => button.attributes('title'))).toEqual(labels)
expect(new Set(labels).size).toBe(labels.length)
expect(labels.every((label) => Boolean(label) && !/undefined|null/.test(label ?? ''))).toBe(true)
})
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('编辑')
})
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(warehouseId, availableLocation.id)
expect(document.body.textContent).toContain('库位详情')
expect(document.body.textContent).toContain('义乌收货仓(CN-YW-01)')
expect(document.body.textContent).toContain('资源作用域平台公共资源')
expect(document.body.textContent).not.toContain('所属企业')
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('当前库位正在被业务使用,只允许查看。')
})
it('clears the warehouse on reset and immediately restores the full query', async () => {
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: '',
})
})
it('keeps the latest detail when an older location request finishes last', async () => {
const secondLocation = { ...availableLocation, id: '9223372036854775800', locationCode: 'A1-03' }
const firstDetail = { location: availableLocation, warehouseCode: warehouse.warehouseCode, warehouseName: warehouse.warehouseName }
const secondDetail = { location: secondLocation, warehouseCode: warehouse.warehouseCode, warehouseName: warehouse.warehouseName }
let resolveFirst!: (value: typeof firstDetail) => void
let resolveSecond!: (value: typeof secondDetail) => void
vi.mocked(getPlatformLocation).mockImplementation((_warehouseId, locationId) => new Promise((resolve) => {
if (String(locationId) === String(availableLocation.id)) resolveFirst = resolve
else resolveSecond = resolve
}))
const wrapper = await mountView()
const vm = wrapper.vm as unknown as {
drawerOpen: boolean
detail: typeof firstDetail | null
loadDetail(row: PlatformLocationResource, mode: 'view' | 'edit'): Promise<void>
}
void vm.loadDetail(availableLocation, 'view')
vm.drawerOpen = false
await wrapper.vm.$nextTick()
void vm.loadDetail(secondLocation, 'view')
resolveSecond(secondDetail)
await flushPromises()
resolveFirst(firstDetail)
await flushPromises()
expect(vm.detail?.location.locationCode).toBe('A1-03')
})
})