Files
admin-web/src/modules/warehouses/pages/WarehousesView.test.ts
T
2026-08-12 16:58:52 +08:00

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)
})
})