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 = {}): 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(selector) expect(field).not.toBeNull() field!.value = value field!.dispatchEvent(new Event('input', { bubbles: true })) } enableAutoUnmount(afterEach) afterAll(() => vi.unstubAllGlobals()) describe('WarehousesView', () => { it('uses bordered striped tables and semantic operation buttons', () => { expect(warehousesSource).toContain('border') expect(warehousesSource).toContain('stripe') expect(warehousesSource).toContain('size="small" plain type="primary"') expect(warehousesSource).toMatch(/size="small"\s+plain\s+:type=/) expect(warehousesSource).toContain('') expect(warehousesSource).toContain('') expect(warehousesSource).toContain('@media (max-width: 720px)') }) it('uses the prototype-sized centered warehouse editor with a fixed footer', () => { expect(warehousesSource).toContain(' { 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('submits the current keyword once when Enter is pressed in the text filter', async () => { const wrapper = await mountView() const keywordInput = wrapper.find('input[placeholder="仓库名称、编码、城市或地址"]') await keywordInput.setValue('AKR1') await keywordInput.trigger('keyup', { key: 'Enter' }) await flushPromises() expect(listPlatformWarehouses).toHaveBeenCalledTimes(2) expect(listPlatformWarehouses).toHaveBeenLastCalledWith({ page: 1, size: 20, keyword: 'AKR1', warehouseType: undefined, status: undefined, }) expect(wrapper.findComponent(QueryToolbar).props('loading')).toBe(false) }) 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 () => { expect(warehousesSource).toContain('') expect(warehousesSource).not.toContain('importResult.errors" max-height="260" size="small" border stripe') 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) }) })