Files
admin-web/src/modules/warehouse-resources/pallets/pages/PalletsView.test.ts
T

384 lines
17 KiB
TypeScript
Raw Normal View History

2026-08-04 21:01:39 +08:00
import ElementPlus from 'element-plus'
2026-08-12 16:58:52 +08:00
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
2026-08-04 21:01:39 +08:00
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'
2026-08-06 14:55:17 +08:00
import { listAllPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses'
2026-08-04 21:01:39 +08:00
import DataTable from '@/shared/components/DataTable.vue'
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
import {
createPlatformPallet,
getPlatformPallet,
getPlatformPalletSummary,
2026-08-04 21:01:39 +08:00
listPlatformPallets,
type PlatformPalletResource,
} from '../api/platformPallets'
import PalletsView from './PalletsView.vue'
2026-08-12 16:58:52 +08:00
const palletsSource = readFileSync(resolve(process.cwd(), 'src/modules/warehouse-resources/pallets/pages/PalletsView.vue'), 'utf8')
2026-08-06 14:55:17 +08:00
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn() }))
2026-08-04 21:01:39 +08:00
vi.mock('../api/platformPallets', () => ({
listPlatformPallets: vi.fn(),
getPlatformPalletSummary: vi.fn(),
2026-08-04 21:01:39 +08:00
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 = {
2026-09-09 17:42:19 +08:00
id: '19001', tenantId: 0, warehouseCode: 'CN-YW-01', internalCode: null, warehouseName: '义乌收货仓',
2026-08-04 21:01:39 +08:00
warehouseType: 'DOMESTIC', countryCode: 'CN', timezone: 'Asia/Shanghai', contactName: null,
contactPhone: null, contactEmail: null, status: 'ACTIVE', remark: null,
}
2026-08-06 14:55:17 +08:00
const fbaWarehouse: PlatformWarehouseResource = {
...warehouse,
id: '19002',
warehouseCode: 'FBA-ONT8',
2026-09-09 17:42:19 +08:00
internalCode: '154',
2026-08-06 14:55:17 +08:00
warehouseName: 'ONT8 FBA 仓',
warehouseType: 'FBA',
}
const overseasWarehouse: PlatformWarehouseResource = {
...warehouse,
id: '19003',
warehouseCode: 'US-LAX-01',
2026-09-09 17:42:19 +08:00
internalCode: null,
2026-08-06 14:55:17 +08:00
warehouseName: '洛杉矶海外仓',
warehouseType: 'OVERSEAS',
}
2026-08-04 21:01:39 +08:00
const pallet: PlatformPalletResource = {
2026-08-06 13:56:04 +08:00
id: '20001', tenantId: 0, warehouseId: '19001', palletCode: 'TP-001', palletType: '木托盘',
2026-08-06 14:55:17 +08:00
capacityCbm: '1.200000', weightKg: '8.000', color: '黄色', remark: '标准川字托盘', createdAt: '2026-08-04T09:00:00', updatedAt: '2026-08-04T10:00:00',
2026-08-04 21:01:39 +08: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
}
2026-08-05 15:54:59 +08:00
async function setWarehouseFilter(wrapper: VueWrapper, warehouseId = '') {
2026-08-04 21:01:39 +08:00
wrapper.findComponent(QueryToolbar).vm.$emit('update:modelValue', {
warehouseId,
palletCode: '',
palletType: '',
})
await flushPromises()
}
async function loadRows(wrapper: VueWrapper, warehouseId = '') {
2026-08-05 15:54:59 +08:00
await setWarehouseFilter(wrapper, warehouseId)
2026-08-04 21:01:39 +08:00
wrapper.findComponent(QueryToolbar).vm.$emit('search', {
warehouseId,
palletCode: '',
palletType: '',
})
await flushPromises()
}
enableAutoUnmount(afterEach)
afterAll(() => vi.unstubAllGlobals())
2026-09-14 17:54:21 +08:00
const densitySource = readFileSync(resolve(process.cwd(), 'src/styles/adminDensity.css'), 'utf8')
2026-08-04 21:01:39 +08:00
describe('PalletsView', () => {
2026-08-12 18:50:03 +08:00
it('uses bordered striped tables and explicit semantic row actions', () => {
expect(palletsSource).toContain('border')
expect(palletsSource).toContain('stripe')
2026-09-14 17:54:21 +08:00
expect(palletsSource).toContain('class="density-action" size="small" title="编辑托盘"')
expect(palletsSource).toContain('class="density-action" data-tone="danger"')
2026-08-13 19:19:34 +08:00
expect(palletsSource).not.toContain('<Eye :size="15" />')
2026-09-14 17:54:21 +08:00
expect(palletsSource).not.toContain('<Pencil :size="15" />')
expect(palletsSource).not.toContain('<Trash2 :size="15" />')
2026-08-12 18:50:03 +08:00
expect(palletsSource).toContain('@media (max-width: 720px)')
})
2026-08-13 19:19:34 +08:00
it('uses the prototype-sized centered editor without an extra detail drawer', () => {
expect(palletsSource).not.toContain('<AppDrawer')
2026-08-12 16:58:52 +08:00
expect(palletsSource).toContain('<AppDialog v-model="editorOpen" :title="editorTitle" width="min(560px, calc(100vw - 28px))"')
expect(palletsSource).not.toContain('<el-drawer v-model="editorOpen"')
2026-09-14 17:54:21 +08:00
expect(densitySource).toContain('.admin-density-dialog.el-dialog .el-dialog__body { min-height: 0; overflow-y: auto;')
2026-08-12 16:58:52 +08:00
expect(palletsSource).toContain('.editor-field-grid { grid-template-columns: minmax(0, 1fr); }')
})
2026-08-04 21:01:39 +08:00
beforeEach(() => {
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
document.body.innerHTML = ''
2026-08-06 14:55:17 +08:00
vi.mocked(listAllPlatformWarehouses).mockReset().mockResolvedValue([warehouse, fbaWarehouse, overseasWarehouse])
2026-08-04 21:01:39 +08:00
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 })
2026-08-04 21:01:39 +08:00
vi.mocked(getPlatformPallet).mockReset().mockResolvedValue(pallet)
vi.mocked(createPlatformPallet).mockReset().mockResolvedValue('20002')
})
2026-08-06 15:32:02 +08:00
it('starts with enabled domestic warehouse candidates for querying using compact filters', async () => {
2026-08-04 21:01:39 +08:00
const wrapper = await mountView()
const toolbar = wrapper.findComponent(QueryToolbar)
const fields = toolbar.props('fields')
2026-08-05 15:54:59 +08:00
expect(toolbar.props('modelValue')).toMatchObject({ warehouseId: '' })
2026-08-04 21:01:39 +08:00
expect(fields.map((field) => [field.key, field.width])).toEqual([
2026-08-05 15:54:59 +08:00
['warehouseId', 300],
2026-08-04 21:01:39 +08:00
['palletCode', 240],
['palletType', 180],
])
expect(wrapper.find('.pallet-filter-toolbar').exists()).toBe(true)
2026-08-05 15:54:59 +08:00
expect(fields.find((field) => field.key === 'warehouseId')?.placeholder).toBe('全部仓库')
2026-08-06 14:55:17 +08:00
expect(fields.find((field) => field.key === 'warehouseId')?.options).toEqual([
{ label: '义乌收货仓(CN-YW-01)', value: '19001' },
])
expect(listAllPlatformWarehouses).toHaveBeenCalledWith()
2026-08-04 21:01:39 +08:00
expect(listPlatformPallets).toHaveBeenCalledWith({
page: 1, size: 20,
})
})
2026-08-05 15:54:59 +08:00
it('loads all warehouses independently and omits an empty warehouse id when searching', async () => {
2026-08-04 21:01:39 +08:00
const wrapper = await mountView()
await loadRows(wrapper)
2026-08-06 14:55:17 +08:00
expect(listAllPlatformWarehouses).toHaveBeenCalledTimes(1)
expect(listAllPlatformWarehouses).toHaveBeenCalledWith()
2026-08-04 21:01:39 +08:00
expect(listPlatformPallets).toHaveBeenCalledWith({
2026-08-05 15:54:59 +08:00
page: 1, size: 20,
2026-08-04 21:01:39 +08:00
})
expect(wrapper.text()).toContain('TP-001')
2026-08-12 12:19:02 +08:00
expect(wrapper.text()).toContain('义乌收货仓')
expect(wrapper.text()).toContain('CN-YW-01')
2026-08-04 21:01:39 +08:00
expect(wrapper.text()).toContain('木托盘')
expect(wrapper.text()).toContain('1.2 m³')
2026-08-06 14:55:33 +08:00
expect(wrapper.text()).toContain('8 kg')
2026-08-12 12:19:02 +08:00
expect(wrapper.find('.data-table').text()).not.toContain('黄色')
expect(wrapper.find('.data-table').text()).not.toContain('标准川字托盘')
2026-08-06 14:55:33 +08:00
expect(wrapper.text()).not.toContain('更新时间')
2026-09-30 14:01:58 +08:00
const tableToolbar = wrapper.find('.data-table--integrated .data-table__toolbar')
2026-08-10 16:24:54 +08:00
expect(tableToolbar.exists()).toBe(true)
expect(tableToolbar.text()).toContain('新增托盘')
expect(tableToolbar.text()).not.toContain('托盘列表')
expect(wrapper.find('.data-table__pagination-left').text()).toContain('共 1 个托盘')
2026-08-04 21:01:39 +08:00
expect(wrapper.text()).not.toContain('启停托盘')
expect(wrapper.text()).not.toContain('全部状态')
expect(wrapper.text()).toContain('全部托盘类型')
2026-08-05 15:54:59 +08:00
expect(wrapper.find('input[aria-label="客户"]').exists()).toBe(false)
2026-08-04 21:01:39 +08:00
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)
2026-09-14 18:34:34 +08:00
expect(wrapper.find('.density-actions.pallet-row-actions').exists()).toBe(true)
2026-08-12 12:19:02 +08:00
expect(wrapper.findAll('thead th').map((item) => item.text().trim())).toEqual([
'操作', '托盘编码', '所属仓库', '托盘类型', '托盘容积(m³)', '托盘重量(kg)',
])
2026-08-04 21:01:39 +08:00
})
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)
2026-08-05 15:54:59 +08:00
toolbar.vm.$emit('search', { warehouseId: '', palletCode: '', palletType: '' })
2026-08-04 21:01:39 +08:00
await flushPromises()
expect(listPlatformPallets).toHaveBeenLastCalledWith({
page: 1, size: 20,
})
})
2026-08-05 15:54:59 +08:00
it('resets the warehouse filter without sending an empty id', async () => {
2026-08-04 21:01:39 +08:00
const wrapper = await mountView()
2026-08-05 15:54:59 +08:00
await setWarehouseFilter(wrapper, '19001')
2026-08-04 21:01:39 +08:00
const toolbar = wrapper.findComponent(QueryToolbar)
toolbar.vm.reset()
await flushPromises()
2026-08-05 15:54:59 +08:00
expect(toolbar.props('modelValue')).toMatchObject({ warehouseId: '' })
2026-08-04 21:01:39 +08:00
expect(listPlatformPallets).toHaveBeenCalledTimes(2)
expect(listPlatformPallets).toHaveBeenLastCalledWith({
page: 1, size: 20,
})
2026-08-05 15:54:59 +08:00
expect(toolbar.props('fields').find((field) => field.key === 'warehouseId')?.placeholder).toBe('全部仓库')
2026-08-04 21:01:39 +08:00
})
2026-08-05 15:54:59 +08:00
it('does not expose customer ownership in warehouse choices', async () => {
2026-08-04 21:01:39 +08:00
const wrapper = await mountView()
2026-08-05 15:54:59 +08:00
const warehouseField = wrapper.findComponent(QueryToolbar).props('fields').find((field) => field.key === 'warehouseId')
2026-08-04 21:01:39 +08:00
2026-08-06 14:55:17 +08:00
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('编辑')
2026-08-04 21:01:39 +08:00
})
it('applies pallet filters only after search', async () => {
const wrapper = await mountView()
2026-08-05 15:54:59 +08:00
await setWarehouseFilter(wrapper, '19001')
2026-08-04 21:01:39 +08:00
await wrapper.find('input[placeholder="托盘编码(模糊查询)"]').setValue('TP-00')
expect(listPlatformPallets).toHaveBeenCalledTimes(1)
wrapper.findComponent(QueryToolbar).vm.$emit('search', {
2026-08-05 15:54:59 +08:00
warehouseId: '19001', palletCode: 'TP-00', palletType: '木托盘',
2026-08-04 21:01:39 +08:00
})
await flushPromises()
expect(listPlatformPallets).toHaveBeenLastCalledWith({
2026-08-05 15:54:59 +08:00
page: 1, size: 20, warehouseId: 19001, palletCode: 'TP-00', palletType: '木托盘',
2026-08-04 21:01:39 +08:00
})
})
it('creates a pallet in the selected warehouse with the real request fields', async () => {
const wrapper = await mountView()
2026-08-05 15:54:59 +08:00
await setWarehouseFilter(wrapper, '19001')
2026-08-04 21:01:39 +08:00
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', '塑料托盘')
2026-08-06 14:55:17 +08:00
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('黑色')
2026-08-13 19:19:34 +08:00
await wrapper.find('input[placeholder="选填"]').setValue('标准托盘')
2026-08-04 21:01:39 +08:00
const saveButton = wrapper.findAll('button').find((button) => button.text().trim() === '保存托盘')
await saveButton?.trigger('click')
await flushPromises()
2026-08-06 15:32:02 +08:00
expect(createPlatformPallet).toHaveBeenCalledWith({
2026-08-06 14:55:17 +08:00
warehouseId: 19001, palletType: '塑料托盘', capacityCbm: 1.5, weightKg: 10, color: '黑色', remark: '标准托盘',
2026-08-04 21:01:39 +08:00
})
})
2026-08-13 19:19:34 +08:00
it('uses a compact three-row two-column editor', async () => {
2026-08-05 17:10:10 +08:00
const wrapper = await mountView()
const createButton = wrapper.findAll('button').find((button) => button.text().trim() === '新增托盘')
await createButton?.trigger('click')
await flushPromises()
2026-08-12 16:58:52 +08:00
const editorDialog = document.body.querySelector('.pallet-editor-dialog')
expect(editorDialog?.textContent).toContain('新增托盘')
expect(editorDialog?.querySelector('.el-dialog__title')?.textContent).toBe('新增托盘')
2026-08-13 19:19:34 +08:00
expect(document.body.querySelectorAll('.editor-field-grid')).toHaveLength(3)
expect(document.body.querySelector('.editor-section')).toBeNull()
expect(document.body.querySelector('.editor-context')).toBeNull()
2026-08-05 17:10:10 +08:00
expect(document.body.querySelector('.pallet-editor-footer')?.textContent).toContain('为必填项')
})
2026-08-04 21:01:39 +08:00
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('删除')
2026-08-13 19:19:34 +08:00
expect(actions).not.toContain('查看')
2026-08-04 21:01:39 +08:00
})
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('托盘列表暂时无法加载')
2026-08-12 12:35:32 +08:00
expect(wrapper.text()).not.toContain('Gateway unavailable')
2026-08-04 21:01:39 +08:00
expect(wrapper.text()).toContain('重新加载')
expect(wrapper.text()).not.toContain('TP-001')
})
2026-08-06 13:56:04 +08:00
it('keeps platform warehouse and pallet queries independent from customer names', async () => {
2026-08-04 21:01:39 +08:00
const wrapper = await mountView()
2026-08-06 13:56:04 +08:00
expect(wrapper.text()).not.toContain('客户列表暂时无法加载')
expect(wrapper.text()).not.toContain('所属企业')
2026-08-06 14:55:17 +08:00
expect(listAllPlatformWarehouses).toHaveBeenCalledWith()
2026-08-04 21:01:39 +08:00
expect(listPlatformPallets).toHaveBeenCalledWith({
page: 1, size: 20,
})
})
2026-08-13 19:19:34 +08:00
it('keeps the list limited to fields exposed by the backend contract', async () => {
2026-08-06 13:56:04 +08:00
const publicPallet = { ...pallet, tenantId: '0' }
vi.mocked(listPlatformPallets).mockResolvedValue({ records: [publicPallet], page: 1, size: 20, total: 1, pages: 1 })
vi.mocked(getPlatformPallet).mockResolvedValue(publicPallet)
2026-08-04 21:01:39 +08:00
const wrapper = await mountView()
await loadRows(wrapper)
2026-08-06 13:56:04 +08:00
expect(wrapper.findComponent(DataTable).props('data')).toEqual([publicPallet])
2026-08-13 19:19:34 +08:00
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('最大承重')
2026-08-07 20:07:37 +08:00
})
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: '蓝色' })
})
2026-08-04 21:01:39 +08:00
})