478 lines
19 KiB
TypeScript
478 lines
19 KiB
TypeScript
import ElementPlus from 'element-plus'
|
|
import { readFileSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
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 PlatformWarehouseSummaryResource,
|
|
type WarehouseImportResult,
|
|
} from '@/modules/warehouses/api/platformWarehouses'
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
import { useAuthStore } from '@/core/auth/auth'
|
|
import WarehousesView from './WarehousesView.vue'
|
|
|
|
const warehousesSource = readFileSync(resolve(process.cwd(), 'src/modules/warehouses/pages/WarehousesView.vue'), 'utf8')
|
|
|
|
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 {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
}
|
|
|
|
function createMemoryStorage(initial: Record<string, string> = {}): Storage {
|
|
const values = new Map(Object.entries(initial))
|
|
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 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: 0,
|
|
warehouseCode: 'CN-YW-01',
|
|
warehouseName: '义乌收货仓',
|
|
warehouseType: 'DOMESTIC',
|
|
countryCode: 'CN',
|
|
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: domesticWarehouse,
|
|
addresses: [domesticAddress],
|
|
capabilities: [
|
|
{ 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: '托盘仓储' },
|
|
],
|
|
}
|
|
|
|
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 = permissions
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
const wrapper = mount(WarehousesView, {
|
|
attachTo: document.body,
|
|
global: { plugins: [pinia, ElementPlus] },
|
|
})
|
|
await flushPromises()
|
|
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', () => {
|
|
it('uses the prototype-sized centered warehouse editor with a fixed footer', () => {
|
|
expect(warehousesSource).toContain('<AppDialog v-model="editorOpen" :title="editorLabel" width="min(650px, calc(100vw - 28px))"')
|
|
expect(warehousesSource).not.toContain('<AppDrawer v-model="editorOpen"')
|
|
expect(warehousesSource).toContain(':global(.warehouse-editor-dialog .el-dialog__body) { min-height: 0; overflow-y: auto; }')
|
|
expect(warehousesSource).toContain('.drawer-form :deep(.form-grid.two-cols) { grid-template-columns: minmax(0, 1fr); }')
|
|
})
|
|
beforeEach(() => {
|
|
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
|
document.body.innerHTML = ''
|
|
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('loads a server-paged warehouse list with backend address summaries and no row detail fan-out', async () => {
|
|
const wrapper = await mountView()
|
|
|
|
expect(listPlatformWarehouses).toHaveBeenCalledWith({
|
|
page: 1,
|
|
size: 20,
|
|
keyword: undefined,
|
|
warehouseType: undefined,
|
|
status: undefined,
|
|
})
|
|
expect(getPlatformWarehouse).not.toHaveBeenCalled()
|
|
expect(wrapper.text()).toContain('义乌收货仓')
|
|
expect(wrapper.find('.data-table').text()).not.toContain('浙江省义乌市北苑街道 1 号库')
|
|
expect(wrapper.text()).toContain('国内收货仓')
|
|
expect(wrapper.text()).toContain('中国 CN')
|
|
expect(wrapper.find('.data-table').text()).not.toContain('UTC+8')
|
|
expect(wrapper.text()).toContain('共 1 个公共仓库')
|
|
expect(wrapper.find('.data-table').exists()).toBe(true)
|
|
const editButton = wrapper.findAll('.table-actions button').find((button) => button.text().trim() === '编辑')
|
|
await editButton?.trigger('click')
|
|
await flushPromises()
|
|
expect((wrapper.find('textarea[placeholder*="街道"]').element as HTMLTextAreaElement).value).toBe('真实物流园 1 号库')
|
|
expect(document.body.textContent).toContain('UTC+8')
|
|
})
|
|
|
|
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: 'FBA',
|
|
warehouseType: 'FBA',
|
|
status: 'ACTIVE',
|
|
})
|
|
await flushPromises()
|
|
|
|
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 API error without falling back to demo warehouses', async () => {
|
|
vi.mocked(listPlatformWarehouses).mockRejectedValueOnce(new Error('Gateway unavailable'))
|
|
const wrapper = await mountView()
|
|
|
|
expect(wrapper.text()).toContain('仓库数据加载失败')
|
|
expect(wrapper.text()).not.toContain('Gateway unavailable')
|
|
expect(wrapper.text()).toContain('重新查询')
|
|
expect(wrapper.text()).not.toContain('演示物流园')
|
|
expect(wrapper.find('.table-panel .data-table').exists()).toBe(false)
|
|
})
|
|
|
|
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()
|
|
|
|
wrapper.findComponent({ name: 'ElDropdown' }).vm.$emit('command', 'FBA')
|
|
await flushPromises()
|
|
|
|
const dialogText = document.querySelector('.warehouse-editor-dialog')?.textContent || ''
|
|
expect(dialogText).toContain('新增FBA 地址')
|
|
expect(dialogText).toContain('国家或地区')
|
|
expect(dialogText).not.toContain('时区覆盖')
|
|
expect(dialogText).not.toContain('联系人')
|
|
expect(dialogText).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 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()
|
|
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 dialogText = document.querySelector('.warehouse-editor-dialog')?.textContent || ''
|
|
expect(dialogText).not.toContain('国家或地区')
|
|
expect(dialogText).not.toContain('时区')
|
|
expect(dialogText).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 dialogText = document.querySelector('.warehouse-editor-dialog')?.textContent || ''
|
|
expect(dialogText).toContain('编辑FBA 地址')
|
|
expect(dialogText).not.toContain('例如 US')
|
|
expect(dialogText).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).toMatchObject({
|
|
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',
|
|
}))
|
|
expect(payload.capabilities).toContainEqual(expect.objectContaining({ capabilityCode: 'STORAGE', description: '托盘仓储' }))
|
|
})
|
|
|
|
it('renders FBA import precheck totals and row-level errors without committing by default', async () => {
|
|
const wrapper = await mountView()
|
|
|
|
await wrapper.findAll('button').find((button) => button.text().trim() === '导入仓库')?.trigger('click')
|
|
await flushPromises()
|
|
|
|
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()
|
|
|
|
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)
|
|
})
|
|
})
|