Files
admin-web/src/modules/warehouse-resources/pallets/pages/PalletsView.test.ts
T
2026-09-30 14:01:58 +08:00

384 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import ElementPlus from 'element-plus'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
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 { listAllPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses'
import DataTable from '@/shared/components/DataTable.vue'
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
import {
createPlatformPallet,
getPlatformPallet,
getPlatformPalletSummary,
listPlatformPallets,
type PlatformPalletResource,
} from '../api/platformPallets'
import PalletsView from './PalletsView.vue'
const palletsSource = readFileSync(resolve(process.cwd(), 'src/modules/warehouse-resources/pallets/pages/PalletsView.vue'), 'utf8')
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn() }))
vi.mock('../api/platformPallets', () => ({
listPlatformPallets: vi.fn(),
getPlatformPalletSummary: vi.fn(),
getPlatformPallet: vi.fn(),
createPlatformPallet: vi.fn(),
updatePlatformPallet: vi.fn(),
deletePlatformPallet: 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 warehouse: PlatformWarehouseResource = {
id: '19001', tenantId: 0, warehouseCode: 'CN-YW-01', internalCode: null, warehouseName: '义乌收货仓',
warehouseType: 'DOMESTIC', countryCode: 'CN', timezone: 'Asia/Shanghai', contactName: null,
contactPhone: null, contactEmail: null, status: 'ACTIVE', remark: null,
}
const fbaWarehouse: PlatformWarehouseResource = {
...warehouse,
id: '19002',
warehouseCode: 'FBA-ONT8',
internalCode: '154',
warehouseName: 'ONT8 FBA 仓',
warehouseType: 'FBA',
}
const overseasWarehouse: PlatformWarehouseResource = {
...warehouse,
id: '19003',
warehouseCode: 'US-LAX-01',
internalCode: null,
warehouseName: '洛杉矶海外仓',
warehouseType: 'OVERSEAS',
}
const pallet: PlatformPalletResource = {
id: '20001', tenantId: 0, warehouseId: '19001', palletCode: 'TP-001', palletType: '木托盘',
capacityCbm: '1.200000', weightKg: '8.000', color: '黄色', remark: '标准川字托盘', createdAt: '2026-08-04T09:00:00', updatedAt: '2026-08-04T10:00:00',
}
async function mountView(permissions = [
'platform:pallet:list', 'platform:pallet:create', 'platform:pallet:edit', 'platform:pallet: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(PalletsView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
return wrapper
}
async function setWarehouseFilter(wrapper: VueWrapper, warehouseId = '') {
wrapper.findComponent(QueryToolbar).vm.$emit('update:modelValue', {
warehouseId,
palletCode: '',
palletType: '',
})
await flushPromises()
}
async function loadRows(wrapper: VueWrapper, warehouseId = '') {
await setWarehouseFilter(wrapper, warehouseId)
wrapper.findComponent(QueryToolbar).vm.$emit('search', {
warehouseId,
palletCode: '',
palletType: '',
})
await flushPromises()
}
enableAutoUnmount(afterEach)
afterAll(() => vi.unstubAllGlobals())
const densitySource = readFileSync(resolve(process.cwd(), 'src/styles/adminDensity.css'), 'utf8')
describe('PalletsView', () => {
it('uses bordered striped tables and explicit semantic row actions', () => {
expect(palletsSource).toContain('border')
expect(palletsSource).toContain('stripe')
expect(palletsSource).toContain('class="density-action" size="small" title="编辑托盘"')
expect(palletsSource).toContain('class="density-action" data-tone="danger"')
expect(palletsSource).not.toContain('<Eye :size="15" />')
expect(palletsSource).not.toContain('<Pencil :size="15" />')
expect(palletsSource).not.toContain('<Trash2 :size="15" />')
expect(palletsSource).toContain('@media (max-width: 720px)')
})
it('uses the prototype-sized centered editor without an extra detail drawer', () => {
expect(palletsSource).not.toContain('<AppDrawer')
expect(palletsSource).toContain('<AppDialog v-model="editorOpen" :title="editorTitle" width="min(560px, calc(100vw - 28px))"')
expect(palletsSource).not.toContain('<el-drawer v-model="editorOpen"')
expect(densitySource).toContain('.admin-density-dialog.el-dialog .el-dialog__body { min-height: 0; overflow-y: auto;')
expect(palletsSource).toContain('.editor-field-grid { grid-template-columns: minmax(0, 1fr); }')
})
beforeEach(() => {
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
document.body.innerHTML = ''
vi.mocked(listAllPlatformWarehouses).mockReset().mockResolvedValue([warehouse, fbaWarehouse, overseasWarehouse])
vi.mocked(listPlatformPallets).mockReset().mockResolvedValue({ records: [pallet], page: 1, size: 20, total: 1, pages: 1 })
vi.mocked(getPlatformPalletSummary).mockReset().mockResolvedValue({ totalCount: 1, idleCount: 1, occupiedCount: 0 })
vi.mocked(getPlatformPallet).mockReset().mockResolvedValue(pallet)
vi.mocked(createPlatformPallet).mockReset().mockResolvedValue('20002')
})
it('starts with enabled domestic warehouse candidates for querying using compact filters', async () => {
const wrapper = await mountView()
const toolbar = wrapper.findComponent(QueryToolbar)
const fields = toolbar.props('fields')
expect(toolbar.props('modelValue')).toMatchObject({ warehouseId: '' })
expect(fields.map((field) => [field.key, field.width])).toEqual([
['warehouseId', 300],
['palletCode', 240],
['palletType', 180],
])
expect(wrapper.find('.pallet-filter-toolbar').exists()).toBe(true)
expect(fields.find((field) => field.key === 'warehouseId')?.placeholder).toBe('全部仓库')
expect(fields.find((field) => field.key === 'warehouseId')?.options).toEqual([
{ label: '义乌收货仓(CN-YW-01)', value: '19001' },
])
expect(listAllPlatformWarehouses).toHaveBeenCalledWith()
expect(listPlatformPallets).toHaveBeenCalledWith({
page: 1, size: 20,
})
})
it('loads all warehouses independently and omits an empty warehouse id when searching', async () => {
const wrapper = await mountView()
await loadRows(wrapper)
expect(listAllPlatformWarehouses).toHaveBeenCalledTimes(1)
expect(listAllPlatformWarehouses).toHaveBeenCalledWith()
expect(listPlatformPallets).toHaveBeenCalledWith({
page: 1, size: 20,
})
expect(wrapper.text()).toContain('TP-001')
expect(wrapper.text()).toContain('义乌收货仓')
expect(wrapper.text()).toContain('CN-YW-01')
expect(wrapper.text()).toContain('木托盘')
expect(wrapper.text()).toContain('1.2 m³')
expect(wrapper.text()).toContain('8 kg')
expect(wrapper.find('.data-table').text()).not.toContain('黄色')
expect(wrapper.find('.data-table').text()).not.toContain('标准川字托盘')
expect(wrapper.text()).not.toContain('更新时间')
const tableToolbar = wrapper.find('.data-table--integrated .data-table__toolbar')
expect(tableToolbar.exists()).toBe(true)
expect(tableToolbar.text()).toContain('新增托盘')
expect(tableToolbar.text()).not.toContain('托盘列表')
expect(wrapper.find('.data-table__pagination-left').text()).toContain('共 1 个托盘')
expect(wrapper.text()).not.toContain('启停托盘')
expect(wrapper.text()).not.toContain('全部状态')
expect(wrapper.text()).toContain('全部托盘类型')
expect(wrapper.find('input[aria-label="客户"]').exists()).toBe(false)
expect(wrapper.find('input[aria-label="所属仓库"]').exists()).toBe(true)
expect(wrapper.find('.platform-tenant-selector').exists()).toBe(false)
expect(wrapper.text()).not.toContain('租户')
expect(wrapper.find('.data-table').exists()).toBe(true)
expect(wrapper.find('.density-actions.pallet-row-actions').exists()).toBe(true)
expect(wrapper.findAll('thead th').map((item) => item.text().trim())).toEqual([
'操作', '托盘编码', '所属仓库', '托盘类型', '托盘容积(m³)', '托盘重量(kg)',
])
})
it('queries all customers explicitly without adding a customer id', async () => {
vi.mocked(listPlatformPallets).mockResolvedValue({ records: [pallet], page: 1, size: 20, total: 41, pages: 3 })
const wrapper = await mountView()
const toolbar = wrapper.findComponent(QueryToolbar)
toolbar.vm.$emit('search', { warehouseId: '', palletCode: '', palletType: '' })
await flushPromises()
expect(listPlatformPallets).toHaveBeenLastCalledWith({
page: 1, size: 20,
})
})
it('resets the warehouse filter without sending an empty id', async () => {
const wrapper = await mountView()
await setWarehouseFilter(wrapper, '19001')
const toolbar = wrapper.findComponent(QueryToolbar)
toolbar.vm.reset()
await flushPromises()
expect(toolbar.props('modelValue')).toMatchObject({ warehouseId: '' })
expect(listPlatformPallets).toHaveBeenCalledTimes(2)
expect(listPlatformPallets).toHaveBeenLastCalledWith({
page: 1, size: 20,
})
expect(toolbar.props('fields').find((field) => field.key === 'warehouseId')?.placeholder).toBe('全部仓库')
})
it('does not expose customer ownership in warehouse choices', async () => {
const wrapper = await mountView()
const warehouseField = wrapper.findComponent(QueryToolbar).props('fields').find((field) => field.key === 'warehouseId')
expect(listAllPlatformWarehouses).toHaveBeenCalledTimes(1)
expect(warehouseField?.options).toEqual([
{ label: '义乌收货仓(CN-YW-01)', value: '19001' },
])
})
it('does not offer editing for a pallet outside a domestic warehouse', async () => {
vi.mocked(listPlatformPallets).mockResolvedValue({
records: [{ ...pallet, id: '20002', warehouseId: fbaWarehouse.id }, pallet], page: 1, size: 20, total: 2, pages: 1,
})
const wrapper = await mountView()
const bodyRows = wrapper.findAll('.el-table__body-wrapper tbody tr')
expect(bodyRows[0]?.text()).not.toContain('编辑')
expect(bodyRows[1]?.text()).toContain('编辑')
})
it('applies pallet filters only after search', async () => {
const wrapper = await mountView()
await setWarehouseFilter(wrapper, '19001')
await wrapper.find('input[placeholder="托盘编码(模糊查询)"]').setValue('TP-00')
expect(listPlatformPallets).toHaveBeenCalledTimes(1)
wrapper.findComponent(QueryToolbar).vm.$emit('search', {
warehouseId: '19001', palletCode: 'TP-00', palletType: '木托盘',
})
await flushPromises()
expect(listPlatformPallets).toHaveBeenLastCalledWith({
page: 1, size: 20, warehouseId: 19001, palletCode: 'TP-00', palletType: '木托盘',
})
})
it('creates a pallet in the selected warehouse with the real request fields', async () => {
const wrapper = await mountView()
await setWarehouseFilter(wrapper, '19001')
const createButton = wrapper.findAll('button').find((button) => button.text().trim() === '新增托盘')
await createButton?.trigger('click')
await flushPromises()
const palletTypeItem = wrapper.findAll('.el-form-item').find((item) => item.text().includes('托盘类型'))
palletTypeItem?.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '塑料托盘')
const inputNumbers = wrapper.findAllComponents({ name: 'ElInputNumber' })
inputNumbers[0]?.vm.$emit('update:modelValue', 1.5)
inputNumbers[1]?.vm.$emit('update:modelValue', 10)
await wrapper.find('input[placeholder="例如 黄色"]').setValue('黑色')
await wrapper.find('input[placeholder="选填"]').setValue('标准托盘')
const saveButton = wrapper.findAll('button').find((button) => button.text().trim() === '保存托盘')
await saveButton?.trigger('click')
await flushPromises()
expect(createPlatformPallet).toHaveBeenCalledWith({
warehouseId: 19001, palletType: '塑料托盘', capacityCbm: 1.5, weightKg: 10, color: '黑色', remark: '标准托盘',
})
})
it('uses a compact three-row two-column editor', async () => {
const wrapper = await mountView()
const createButton = wrapper.findAll('button').find((button) => button.text().trim() === '新增托盘')
await createButton?.trigger('click')
await flushPromises()
const editorDialog = document.body.querySelector('.pallet-editor-dialog')
expect(editorDialog?.textContent).toContain('新增托盘')
expect(editorDialog?.querySelector('.el-dialog__title')?.textContent).toBe('新增托盘')
expect(document.body.querySelectorAll('.editor-field-grid')).toHaveLength(3)
expect(document.body.querySelector('.editor-section')).toBeNull()
expect(document.body.querySelector('.editor-context')).toBeNull()
expect(document.body.querySelector('.pallet-editor-footer')?.textContent).toContain('为必填项')
})
it('hides all write actions without write permissions', async () => {
const wrapper = await mountView(['platform:pallet:list'])
expect(wrapper.find('.panel-heading button').exists()).toBe(false)
const actions = wrapper.find('.pallet-row-actions').text()
expect(actions).not.toContain('编辑')
expect(actions).not.toContain('删除')
expect(actions).not.toContain('查看')
})
it('shows a recoverable list failure instead of demo data', async () => {
vi.mocked(listPlatformPallets).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('TP-001')
})
it('keeps platform warehouse and pallet queries independent from customer names', async () => {
const wrapper = await mountView()
expect(wrapper.text()).not.toContain('客户列表暂时无法加载')
expect(wrapper.text()).not.toContain('所属企业')
expect(listAllPlatformWarehouses).toHaveBeenCalledWith()
expect(listPlatformPallets).toHaveBeenCalledWith({
page: 1, size: 20,
})
})
it('keeps the list limited to fields exposed by the backend contract', async () => {
const publicPallet = { ...pallet, tenantId: '0' }
vi.mocked(listPlatformPallets).mockResolvedValue({ records: [publicPallet], page: 1, size: 20, total: 1, pages: 1 })
vi.mocked(getPlatformPallet).mockResolvedValue(publicPallet)
const wrapper = await mountView()
await loadRows(wrapper)
expect(wrapper.findComponent(DataTable).props('data')).toEqual([publicPallet])
const tableText = wrapper.find('.data-table').text()
expect(tableText).toContain('托盘编码')
expect(tableText).toContain('托盘类型')
expect(tableText).toContain('托盘容积')
expect(tableText).not.toContain('所属企业')
expect(tableText).not.toContain('企业 ID')
expect(tableText).not.toContain('租户')
expect(tableText).not.toContain('托盘状态')
expect(tableText).not.toContain('最大承重')
})
it('keeps the latest pallet selected for editing', async () => {
const secondPallet = { ...pallet, id: '20002', palletCode: 'TP-002', color: '蓝色' }
let resolveFirst!: (value: PlatformPalletResource) => void
let resolveSecond!: (value: PlatformPalletResource) => void
vi.mocked(getPlatformPallet).mockImplementation((_warehouseId, palletId) => new Promise((resolve) => {
if (String(palletId) === String(pallet.id)) resolveFirst = resolve
else resolveSecond = resolve
}))
const wrapper = await mountView()
const vm = wrapper.vm as unknown as {
form: { palletCode: string; color: string }
openEdit(row: PlatformPalletResource): Promise<void>
}
void vm.openEdit(pallet)
void vm.openEdit(secondPallet)
resolveSecond(secondPallet)
await flushPromises()
resolveFirst(pallet)
await flushPromises()
expect(vm.form).toMatchObject({ palletCode: 'TP-002', color: '蓝色' })
})
})