415 lines
16 KiB
TypeScript
415 lines
16 KiB
TypeScript
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')
|
||
})
|
||
})
|