This commit is contained in:
2026-08-06 13:56:04 +08:00
parent 6736893128
commit a813426588
35 changed files with 2822 additions and 489 deletions
@@ -2,25 +2,32 @@ import ElementPlus from 'element-plus'
import { createPinia, setActivePinia } from 'pinia'
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import DataTable from '@/shared/components/DataTable.vue'
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
import {
createFbaWarehouse,
createPlatformWarehouse,
getPlatformWarehouse,
importWarehouses,
listPlatformWarehouses,
updatePlatformWarehouse,
type PlatformWarehouseDetailResource,
type PlatformWarehouseResource,
type PlatformWarehouseSummaryResource,
type WarehouseImportResult,
} from '@/modules/warehouses/api/platformWarehouses'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
import WarehousesView from './WarehousesView.vue'
vi.mock('@/modules/warehouses/api/platformWarehouses', () => ({
listPlatformWarehouses: vi.fn(),
getPlatformWarehouse: vi.fn(),
createPlatformWarehouse: vi.fn(),
createFbaWarehouse: vi.fn(),
updatePlatformWarehouse: vi.fn(),
changePlatformWarehouseStatus: vi.fn(),
downloadWarehouseImportTemplate: vi.fn(),
importWarehouses: vi.fn(),
}))
class ResizeObserverStub implements ResizeObserver {
@@ -41,43 +48,109 @@ function createMemoryStorage(initial: Record<string, string> = {}): Storage {
}
}
const warehouse: PlatformWarehouseResource = {
const domesticAddress = {
id: '19002',
tenantId: 0,
warehouseId: '19001',
addressType: 'OPERATING',
countryCode: 'CN',
stateProvince: '浙江省',
city: '义乌市',
district: '北苑街道',
postalCode: '322000',
addressLine1: '真实物流园 1 号库',
addressLine2: 'A 栋',
latitude: '29.3',
longitude: '120.0',
}
const domesticWarehouse: PlatformWarehouseSummaryResource = {
id: '19001',
tenantId: 10001,
warehouseCode: 'CN-TEST-01',
warehouseName: '测试环境收货仓',
tenantId: 0,
warehouseCode: 'CN-YW-01',
warehouseName: '义乌收货仓',
warehouseType: 'DOMESTIC',
countryCode: 'CN',
timezone: 'Asia/Shanghai',
contactName: '真实联系人',
timezone: 'UTC+8',
contactName: '仓库客服',
contactPhone: '0579-12345678',
contactEmail: 'warehouse@example.com',
status: 'ACTIVE',
remark: '测试环境数据',
updatedAt: '2026-07-31T10:20:00',
addressSummary: '浙江省义乌市北苑街道 1 号库',
primaryAddress: domesticAddress,
}
const fbaAddress = {
id: '19012',
tenantId: 0,
warehouseId: '19011',
addressType: 'OPERATING',
countryCode: 'US',
stateProvince: 'OH',
city: 'Canton',
district: null,
postalCode: '44705',
addressLine1: '4747 Rebar Ave NE',
addressLine2: null,
latitude: null,
longitude: null,
}
const fbaWarehouse: PlatformWarehouseSummaryResource = {
id: '19011',
tenantId: 0,
warehouseCode: 'AKR1',
warehouseName: 'AKR1',
warehouseType: 'FBA',
countryCode: 'US',
timezone: 'UTC-5',
contactName: null,
contactPhone: null,
contactEmail: null,
status: 'ACTIVE',
remark: null,
updatedAt: '2026-07-31T10:20:00',
addressSummary: 'OH · Canton',
primaryAddress: fbaAddress,
}
const fbaDetail: PlatformWarehouseDetailResource = {
warehouse: fbaWarehouse,
addresses: [fbaAddress],
capabilities: [],
}
const detail: PlatformWarehouseDetailResource = {
warehouse,
addresses: [{
id: '19002', tenantId: 10001, warehouseId: '19001', addressType: 'OPERATING', countryCode: 'CN',
stateProvince: '浙江省', city: '义乌市', district: '北苑街道', postalCode: '322000',
addressLine1: '真实物流园 1 号库', addressLine2: 'A 栋', latitude: '29.3', longitude: '120.0',
}],
warehouse: domesticWarehouse,
addresses: [domesticAddress],
capabilities: [
{ id: '19003', tenantId: 10001, warehouseId: '19001', capabilityCode: 'RECEIVING', capabilityName: '收货服务', enabled: true, cutoffTime: '17:00:00', description: '周一至周六 08:30-18:00' },
{ id: '19004', tenantId: 10001, warehouseId: '19001', capabilityCode: 'STORAGE', capabilityName: '仓储', enabled: true, cutoffTime: null, description: '托盘仓储' },
{ id: '19003', tenantId: 0, warehouseId: '19001', capabilityCode: 'RECEIVING', capabilityName: '收货服务', enabled: true, cutoffTime: '17:00:00', description: '周一至周六 08:30-18:00' },
{ id: '19004', tenantId: 0, warehouseId: '19001', capabilityCode: 'STORAGE', capabilityName: '仓储', enabled: true, cutoffTime: null, description: '托盘仓储' },
],
}
async function mountView() {
const importPreview: WarehouseImportResult = {
fileName: 'warehouses.xlsx',
totalRows: 4,
validRows: 3,
createCount: 2,
updateCount: 1,
unchangedCount: 0,
errorCount: 1,
committed: false,
errors: [{ row: 4, field: '仓库编码', code: 'DUPLICATE_CODE', message: '文件内编码重复' }],
}
async function mountView(permissions = ['platform:warehouse:list', 'platform:warehouse:create', 'platform:warehouse:edit', 'platform:warehouse:status']) {
vi.stubGlobal('localStorage', createMemoryStorage())
vi.stubGlobal('sessionStorage', createMemoryStorage())
const pinia = createPinia()
setActivePinia(pinia)
const auth = useAuthStore()
auth.token = 'token'
auth.permissions = ['platform:warehouse:list', 'platform:warehouse:create', 'platform:warehouse:edit', 'platform:warehouse:status']
auth.permissions = permissions
useAccessPreviewStore().initializeAfterLogin()
const wrapper = mount(WarehousesView, {
attachTo: document.body,
@@ -87,72 +160,80 @@ async function mountView() {
return wrapper
}
function setField(selector: string, value: string) {
const field = document.querySelector<HTMLInputElement | HTMLTextAreaElement>(selector)
expect(field).not.toBeNull()
field!.value = value
field!.dispatchEvent(new Event('input', { bubbles: true }))
}
enableAutoUnmount(afterEach)
afterAll(() => vi.unstubAllGlobals())
describe('WarehousesView real API integration', () => {
describe('WarehousesView', () => {
beforeEach(() => {
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
document.body.innerHTML = ''
vi.mocked(listPlatformWarehouses).mockReset().mockResolvedValue({ records: [warehouse], page: 1, size: 200, total: 1, pages: 1 })
vi.mocked(listPlatformWarehouses).mockReset().mockResolvedValue({ records: [domesticWarehouse], page: 1, size: 20, total: 1, pages: 1 })
vi.mocked(getPlatformWarehouse).mockReset().mockResolvedValue(detail)
vi.mocked(createPlatformWarehouse).mockReset().mockResolvedValue('19005')
vi.mocked(createFbaWarehouse).mockReset().mockResolvedValue('19006')
vi.mocked(updatePlatformWarehouse).mockReset().mockResolvedValue(null)
vi.mocked(importWarehouses).mockReset().mockResolvedValue(importPreview)
})
it('automatically loads the fixed tenant and renders address and receiving capability from details', async () => {
it('loads a server-paged warehouse list with backend address summaries and no row detail fan-out', async () => {
const wrapper = await mountView()
expect(listPlatformWarehouses).toHaveBeenCalledWith(10001, 1, 200)
expect(getPlatformWarehouse).toHaveBeenCalledWith(10001, '19001')
expect(wrapper.text()).toContain('测试环境收货仓')
expect(wrapper.text()).toContain('北苑街道 真实物流园 1 号库 A 栋')
expect(wrapper.text()).toContain('周一至周六 08:30-18:00')
expect(listPlatformWarehouses).toHaveBeenCalledWith({
page: 1,
size: 20,
keyword: undefined,
warehouseType: undefined,
status: undefined,
})
expect(getPlatformWarehouse).not.toHaveBeenCalled()
expect(wrapper.text()).toContain('义乌收货仓')
expect(wrapper.text()).toContain('浙江省义乌市北苑街道 1 号库')
expect(wrapper.text()).toContain('国内收货仓')
expect(wrapper.text()).toContain('Asia/Shanghai')
expect(wrapper.text()).not.toContain('全部收货仓')
expect(wrapper.find('.query-toolbar').exists()).toBe(true)
expect(wrapper.text()).toContain('中国 CN')
expect(wrapper.text()).toContain('UTC+8')
expect(wrapper.text()).toContain('共 1 个公共仓库')
expect(wrapper.find('.data-table').exists()).toBe(true)
})
it('updates filters locally and only reloads after search', async () => {
const wrapper = await mountView()
const keywordInput = wrapper.find('input[placeholder="仓库名称、编码、城市或地址"]')
await keywordInput.setValue('义乌')
await flushPromises()
expect(listPlatformWarehouses).toHaveBeenCalledTimes(1)
const searchButton = wrapper.findAll('button').find((button) => button.text().trim() === '查询')
expect(searchButton).toBeDefined()
await searchButton?.trigger('click')
await flushPromises()
expect(listPlatformWarehouses).toHaveBeenCalledTimes(2)
expect(wrapper.text()).toContain('测试环境收货仓')
})
it('uses the fixed public tenant without rendering a tenant selector', async () => {
const wrapper = await mountView()
expect(wrapper.find('[aria-label="目标企业"]').exists()).toBe(false)
expect(listPlatformWarehouses).toHaveBeenCalledWith(10001, 1, 200)
expect(wrapper.text()).toContain('公共仓库')
})
it('filters the public warehouse table by warehouse type', async () => {
it('submits filters and pagination to the API instead of filtering rows locally', async () => {
vi.mocked(listPlatformWarehouses).mockResolvedValue({ records: [domesticWarehouse], page: 1, size: 20, total: 45, pages: 3 })
const wrapper = await mountView()
wrapper.findComponent(QueryToolbar).vm.$emit('search', {
keyword: '', warehouseType: 'FBA', status: '',
keyword: 'FBA',
warehouseType: 'FBA',
status: 'ACTIVE',
})
await flushPromises()
expect(wrapper.text()).not.toContain('测试环境收货仓')
expect(wrapper.text()).toContain('暂无符合筛选条件的仓库')
expect(listPlatformWarehouses).toHaveBeenLastCalledWith({
page: 1,
size: 20,
keyword: 'FBA',
warehouseType: 'FBA',
status: 'ACTIVE',
})
wrapper.findComponent(DataTable).vm.$emit('update:page', 2)
await flushPromises()
expect(listPlatformWarehouses).toHaveBeenLastCalledWith({
page: 2,
size: 20,
keyword: 'FBA',
warehouseType: 'FBA',
status: 'ACTIVE',
})
})
it('shows a recoverable real API error without falling back to demo warehouses', async () => {
it('shows a recoverable API error without falling back to demo warehouses', async () => {
vi.mocked(listPlatformWarehouses).mockRejectedValueOnce(new Error('Gateway unavailable'))
const wrapper = await mountView()
@@ -162,66 +243,219 @@ describe('WarehousesView real API integration', () => {
expect(wrapper.text()).not.toContain('演示物流园')
})
it('preserves backend-only address, email, capability and cutoff fields when updating', async () => {
it('keeps create actions behind permissions and preserves a read-only table state', async () => {
const wrapper = await mountView(['platform:warehouse:list'])
expect(wrapper.text()).not.toContain('新增仓库')
expect(wrapper.text()).not.toContain('导入仓库')
expect(wrapper.text()).toContain('仅查看')
})
it('creates an FBA address from the fast path with selectable state and timezone values', async () => {
const wrapper = await mountView()
const editButton = wrapper.findAll('button').find((button) => button.text().trim() === '编辑')
expect(editButton).toBeDefined()
await editButton?.trigger('click')
wrapper.findComponent({ name: 'ElDropdown' }).vm.$emit('command', 'FBA')
await flushPromises()
const typeFormItem = wrapper.findAll('.el-form-item').find((item) => item.text().includes('仓库类型'))
expect(typeFormItem).toBeDefined()
typeFormItem?.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', 'FBA')
await wrapper.find('input[placeholder="例如 CN、US"]').setValue('US')
await wrapper.find('input[placeholder="例如 Asia/Shanghai"]').setValue('America/Los_Angeles')
const drawerText = document.querySelector('.el-drawer')?.textContent || ''
expect(drawerText).toContain('新增FBA 地址')
expect(drawerText).toContain('国家或地区')
expect(drawerText).not.toContain('时区覆盖')
expect(drawerText).not.toContain('联系人')
expect(drawerText).not.toContain('收货时间')
const countrySelect = wrapper.findAllComponents({ name: 'ElSelect' })
.find((select) => select.props('placeholder') === '美国 US')
expect(countrySelect).toBeDefined()
expect(countrySelect!.props('modelValue')).toBe('US')
expect(countrySelect!.props('disabled')).toBe(true)
const saveButton = wrapper.findAll('button').find((button) => button.text().trim() === '保存仓库')
const stateSelect = wrapper.findAllComponents({ name: 'ElSelect' })
.find((select) => select.props('placeholder') === '请选择州')
expect(stateSelect).toBeDefined()
setField('input[placeholder="例如 LAX9"]', 'lax9')
stateSelect!.vm.$emit('update:modelValue', 'CA')
stateSelect!.vm.$emit('change', 'CA')
await flushPromises()
setField('input[placeholder="例如 Long Beach"]', 'Long Beach')
setField('input[placeholder="例如 90802"]', '90802')
setField('textarea[placeholder="请输入街道和门牌信息"]', '2400 E Spring St')
const saveButton = [...document.querySelectorAll('button')].find((button) => button.textContent?.trim() === '确认新增')
expect(saveButton).toBeDefined()
await saveButton?.trigger('click')
saveButton!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await flushPromises()
expect(createFbaWarehouse).toHaveBeenCalledWith({
warehouseCode: 'LAX9',
stateProvince: 'CA',
city: 'Long Beach',
postalCode: '90802',
addressLine1: '2400 E Spring St',
timezone: 'UTC-8',
})
expect(createPlatformWarehouse).not.toHaveBeenCalled()
})
it('hides domestic country, timezone and type inputs while submitting fixed CN and UTC+8 defaults', async () => {
const wrapper = await mountView()
wrapper.findComponent({ name: 'ElDropdown' }).vm.$emit('command', 'DOMESTIC')
await flushPromises()
const drawerText = document.querySelector('.el-drawer')?.textContent || ''
expect(drawerText).not.toContain('国家或地区')
expect(drawerText).not.toContain('时区')
expect(drawerText).not.toContain('仓库类型')
setField('input[placeholder="请输入仓库名称"]', '义乌新仓')
setField('input[placeholder="例如 浙江省"]', '浙江省')
setField('input[placeholder="例如 义乌市"]', '义乌市')
setField('textarea[placeholder="请输入街道、园区、楼栋及门牌信息"]', '稠州北路 100 号')
const saveButton = [...document.querySelectorAll('button')].find((button) => button.textContent?.trim() === '确认新增')
expect(saveButton).toBeDefined()
saveButton!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await flushPromises()
expect(createPlatformWarehouse).toHaveBeenCalledOnce()
expect(vi.mocked(createPlatformWarehouse).mock.calls[0][0]).toMatchObject({
warehouseName: '义乌新仓',
warehouseType: 'DOMESTIC',
countryCode: 'CN',
timezone: 'UTC+8',
address: expect.objectContaining({ countryCode: 'CN', city: '义乌市' }),
})
})
it('uses country and state selectors for overseas warehouse creation and auto-fills timezone when safe', async () => {
const wrapper = await mountView()
wrapper.findComponent({ name: 'ElDropdown' }).vm.$emit('command', 'OVERSEAS')
await flushPromises()
const countrySelect = wrapper.findAllComponents({ name: 'ElSelect' })
.find((select) => select.props('placeholder') === '请选择国家或地区')
expect(countrySelect).toBeDefined()
expect(countrySelect!.props('modelValue')).toBe('US')
const stateSelect = wrapper.findAllComponents({ name: 'ElSelect' })
.find((select) => select.props('placeholder') === '请选择州')
expect(stateSelect).toBeDefined()
const timezoneSelect = wrapper.findAllComponents({ name: 'ElSelect' })
.find((select) => select.props('placeholder') === '请选择时区')
expect(timezoneSelect).toBeDefined()
expect(wrapper.findAllComponents({ name: 'ElOption' }).some((option) => option.props('value') === 'UTC-8')).toBe(true)
setField('input[placeholder="请输入仓库名称"]', '洛杉矶海外仓')
stateSelect!.vm.$emit('update:modelValue', 'CA')
stateSelect!.vm.$emit('change', 'CA')
setField('input[placeholder="例如 Long Beach"]', 'Long Beach')
setField('textarea[placeholder="请输入街道、园区、楼栋及门牌信息"]', '2400 E Spring St')
const saveButton = [...document.querySelectorAll('button')].find((button) => button.textContent?.trim() === '确认新增')
expect(saveButton).toBeDefined()
saveButton!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await flushPromises()
expect(createPlatformWarehouse).toHaveBeenCalledOnce()
expect(vi.mocked(createPlatformWarehouse).mock.calls[0][0]).toMatchObject({
warehouseName: '洛杉矶海外仓',
warehouseType: 'OVERSEAS',
countryCode: 'US',
timezone: 'UTC-8',
address: expect.objectContaining({ stateProvince: 'CA' }),
})
})
it('edits FBA warehouses with selectors for country, timezone and state', async () => {
vi.mocked(listPlatformWarehouses).mockResolvedValueOnce({ records: [fbaWarehouse, domesticWarehouse], page: 1, size: 20, total: 2, pages: 1 })
vi.mocked(getPlatformWarehouse).mockResolvedValueOnce(fbaDetail)
const wrapper = await mountView()
const editButton = wrapper.find('.el-table__body-wrapper .table-actions button')
expect(editButton).toBeDefined()
await editButton.trigger('click')
await flushPromises()
const drawerText = document.querySelector('.el-drawer')?.textContent || ''
expect(drawerText).toContain('编辑FBA 地址')
expect(drawerText).not.toContain('例如 US')
expect(drawerText).not.toContain('例如 UTC-8')
const countrySelect = wrapper.findAllComponents({ name: 'ElSelect' })
.find((select) => select.props('placeholder') === '请选择国家或地区')
const stateSelect = wrapper.findAllComponents({ name: 'ElSelect' })
.find((select) => select.props('placeholder') === '请选择州')
const timezoneSelect = wrapper.findAllComponents({ name: 'ElSelect' })
.find((select) => select.props('placeholder') === '请选择时区')
expect(countrySelect?.props('modelValue')).toBe('US')
expect(stateSelect?.props('modelValue')).toBe('OH')
expect(timezoneSelect?.props('modelValue')).toBe('UTC-5')
expect(countrySelect).toBeDefined()
const saveButton = [...document.querySelectorAll('button')].find((button) => button.textContent?.trim() === '保存修改')
expect(saveButton).toBeDefined()
saveButton!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await flushPromises()
expect(updatePlatformWarehouse).toHaveBeenCalledOnce()
const [, , payload] = vi.mocked(updatePlatformWarehouse).mock.calls[0]
expect(payload.contactEmail).toBe('warehouse@example.com')
const [, payload] = vi.mocked(updatePlatformWarehouse).mock.calls[0]
expect(payload).toMatchObject({
tenantId: 10001, warehouseType: 'FBA', countryCode: 'US', timezone: 'America/Los_Angeles',
})
expect(payload.address).toMatchObject({
countryCode: 'US', district: '北苑街道', postalCode: '322000', addressLine1: '真实物流园 1 号库', addressLine2: 'A 栋',
latitude: '29.3', longitude: '120.0',
warehouseType: 'FBA',
countryCode: 'US',
timezone: 'UTC-5',
address: expect.objectContaining({ stateProvince: 'OH' }),
})
})
it('edits existing records with full fields and preserves backend-only capabilities', async () => {
const wrapper = await mountView()
const editButton = wrapper.find('.el-table__body-wrapper .table-actions button')
expect(editButton).toBeDefined()
await editButton.trigger('click')
await flushPromises()
const saveButton = [...document.querySelectorAll('button')].find((button) => button.textContent?.trim() === '保存修改')
expect(saveButton).toBeDefined()
saveButton!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await flushPromises()
expect(getPlatformWarehouse).toHaveBeenCalledWith('19001')
expect(updatePlatformWarehouse).toHaveBeenCalledOnce()
const [, payload] = vi.mocked(updatePlatformWarehouse).mock.calls[0]
expect(payload.contactEmail).toBe('warehouse@example.com')
expect(payload.capabilities).toContainEqual(expect.objectContaining({
capabilityCode: 'RECEIVING', cutoffTime: '17:00:00', description: '周一至周六 08:30-18:00',
capabilityCode: 'RECEIVING',
cutoffTime: '17:00:00',
description: '周一至周六 08:30-18:00',
}))
expect(payload.capabilities).toContainEqual(expect.objectContaining({ capabilityCode: 'STORAGE', description: '托盘仓储' }))
})
it('shows field-level required errors and does not submit an empty warehouse', async () => {
it('renders FBA import precheck totals and row-level errors without committing by default', async () => {
const wrapper = await mountView()
const createButton = wrapper.findAll('button').find((button) => button.text().trim() === '新增仓库')
expect(createButton).toBeDefined()
await createButton?.trigger('click')
await wrapper.findAll('button').find((button) => button.text().trim() === '导入仓库')?.trigger('click')
await flushPromises()
expect(document.querySelector('input[placeholder="例如 广东省"]')).not.toBeNull()
expect(document.querySelector('input[placeholder="例如 CN、US"]')).not.toBeNull()
expect(document.querySelector('input[placeholder="例如 Asia/Shanghai"]')).not.toBeNull()
expect(document.querySelector('input[placeholder="请输入仓库联系人姓名"]')).not.toBeNull()
expect(document.querySelector('textarea[placeholder="请输入街道、园区、楼栋及库位信息"]')).not.toBeNull()
await wrapper.find('input[placeholder="例如 CN-NB-01"]').trigger('blur')
await wrapper.vm.$nextTick()
expect(wrapper.find('#warehouse-code-error').text()).toBe('请输入仓库编码')
const saveButton = wrapper.findAll('button').find((button) => button.text().trim() === '保存仓库')
expect(saveButton).toBeDefined()
await saveButton?.trigger('click')
const upload = wrapper.findComponent({ name: 'ElUpload' })
const file = new File(['content'], 'fba.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
upload.props('onChange')({ name: file.name, raw: file })
await flushPromises()
expect(createPlatformWarehouse).not.toHaveBeenCalled()
expect(wrapper.text()).toContain('请输入仓库编码')
expect(wrapper.text()).toContain('请输入仓库名称')
expect(wrapper.text()).toContain('请输入城市')
expect(wrapper.text()).toContain('请输入详细地址')
const precheckButton = [...document.querySelectorAll('button')].find((button) => button.textContent?.trim() === '预检文件')
expect(precheckButton).toBeDefined()
precheckButton!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await flushPromises()
expect(importWarehouses).toHaveBeenCalledWith(file, true, 'CREATE_ONLY')
expect(document.body.textContent).toContain('新增')
expect(document.body.textContent).toContain('更新')
expect(document.body.textContent).toContain('无变化')
expect(document.body.textContent).toContain('错误')
expect(document.body.textContent).toContain('文件内编码重复')
expect(importWarehouses).toHaveBeenCalledTimes(1)
})
})