import { createRequire } from 'node:module' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import QueryToolbar from '@/shared/components/QueryToolbar.vue' import * as api from '../api/storageRent' import type { StorageRentLot } from '../api/storageRent' import StorageRentPanel from './StorageRentPanel.vue' const { default: ElementPlus } = createRequire(import.meta.url)('element-plus') as typeof import('element-plus') vi.mock('../api/storageRent', () => ({ getStorageRentLot: vi.fn(), listStorageRentLots: vi.fn(), listStorageRentWarehouses: vi.fn() })) const lot: StorageRentLot = { id: '9223372036854775001', customerId: '1', customerName: '测试客户', businessType: 'LASTLEG_ORDER', businessNo: 'FL1', warehouseId: '2', warehouseName: '测试仓', batchNo: 'B1', inboundAt: '2026-10-22T00:00:00Z', storageDays: 1, originalQuantity: 10, remainingQuantity: 8, quantityUnit: 'PACKAGE', billingQuantity: 1, billingUnit: 'CBM', ruleCode: 'TRANSIT_V1', status: 'IN_STOCK', generatedAmount: 0, generatedCurrency: null, generatedAmounts: [], } enableAutoUnmount(afterEach) beforeEach(() => { vi.resetAllMocks() vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }) vi.mocked(api.listStorageRentLots).mockResolvedValue({ records: [lot], total: 1, page: 1, size: 20, pages: 1 }) vi.mocked(api.listStorageRentWarehouses).mockResolvedValue([{ id: '2', warehouseName: '测试仓' }]) vi.mocked(api.getStorageRentLot).mockResolvedValue({ lot, charges: [{ id: '3', outboundNo: 'OUT1', businessNo: 'FL1', quantity: 2, storageDays: 1, originalAmount: 0, currency: 'USD', settlementAmount: 0, currencyRate: 7, status: 'SETTLED', createdAt: '2026-10-22T01:00:00Z', outboundAt: '2026-10-22T01:00:00Z', }] }) }) afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.innerHTML = '' }) describe('仓租列表服务端契约', () => { it('shows storage age and remaining quantity and traces each half pallet without financial actions', async () => { const returned = { ...lot, businessType: 'RETURN_ORDER', businessNo: 'TH1', quantityUnit: 'PIECE', originalQuantity: 6, remainingQuantity: 2, billingQuantity: 1.5, billingUnit: 'PALLET', status: 'IN_STOCK', generatedAmounts: [{ currency: 'USD', amount: 1.5 }], settledAmount: 10, chargeCount: 2, settledChargeCount: 2 } vi.mocked(api.listStorageRentLots).mockResolvedValue({ records: [returned], total: 1, page: 1, size: 20, pages: 1 }) vi.mocked(api.getStorageRentLot).mockResolvedValue({ lot: returned, charges: ['TH1', 'CD1'].map((businessNo, index) => ({ id: String(index), outboundNo: `OUT${index}`, businessNo, quantity: 2, storageDays: 1, originalAmount: 0.75, currency: 'USD', settlementAmount: 5, currencyRate: 6.67, status: 'SETTLED', createdAt: '', outboundAt: '' })) }) const wrapper = mount(StorageRentPanel, { attachTo: document.body, props: { customers: [] }, global: { plugins: [ElementPlus] } }) await flushPromises() const body = wrapper.get('.el-table__body-wrapper') expect(body.text()).toContain('2 件') expect(wrapper.text()).toContain('库龄(天)') expect(wrapper.text()).toContain('来源单号') expect(body.text()).toContain('TH1') expect(wrapper.text()).not.toContain('仓租 / 扣款') await body.findAll('button').find(button => button.text().trim() === '详情')!.trigger('click'); await flushPromises() const dialog = document.body.querySelector('[role="dialog"]')! expect(dialog.textContent).toContain('0.5 托') expect(dialog.textContent).toContain('OUT0') expect(dialog.textContent).toContain('OUT1') expect(dialog.textContent).not.toContain('USD') expect(dialog.textContent).not.toContain('审核') expect(dialog.textContent).toContain('CD1') expect(dialog.textContent).toContain('来源单号') expect(dialog.textContent).toContain('出库业务单号') }) it('displays and filters first-leg lots using LASTLEG_ORDER and retains inbound and outbound evidence', async () => { const wrapper = mount(StorageRentPanel, { attachTo: document.body, props: { customers: [] }, global: { plugins: [ElementPlus] } }) await flushPromises() const toolbar = wrapper.getComponent(QueryToolbar) expect(api.listStorageRentLots).toHaveBeenLastCalledWith(expect.objectContaining({ status: 'IN_STOCK', page: 1 })) expect(toolbar.props('fields').some(field => field.key === 'status')).toBe(false) const options = toolbar.props('fields').find(field => field.key === 'businessType')?.options expect(options).toContainEqual({ value: 'LASTLEG_ORDER', label: '头程入仓' }) expect(options).not.toContainEqual({ value: 'FIRST_LEG', label: '头程入仓' }) expect(wrapper.get('.el-table__body-wrapper').text()).toContain('头程入仓') toolbar.vm.$emit('search', { businessType: 'LASTLEG_ORDER' }) await flushPromises() expect(api.listStorageRentLots).toHaveBeenLastCalledWith(expect.objectContaining({ businessType: 'LASTLEG_ORDER', status: 'IN_STOCK', page: 1 })) toolbar.vm.$emit('search', {}) await flushPromises() expect(api.listStorageRentLots).toHaveBeenLastCalledWith(expect.objectContaining({ status: 'IN_STOCK', page: 1 })) const detailButton = wrapper.findAll('.el-table__body-wrapper button').find(button => button.text().trim() === '详情') await detailButton!.trigger('click'); await flushPromises() expect(api.getStorageRentLot).toHaveBeenCalledWith(lot.id) const dialog = document.body.querySelector('[role="dialog"]')! expect(dialog.textContent).toContain('2026-10-22') expect(dialog.textContent).toContain('8 箱') expect(dialog.textContent).toContain('OUT1') expect(dialog.textContent).not.toContain('实扣金额') }) it('keeps inventory focused on age and outbound history even when charges use different currencies', async () => { const mixedLot = { ...lot, generatedAmount: null, generatedCurrency: null, generatedAmounts: [{ currency: 'USD', amount: 12 }, { currency: 'CNY', amount: 23 }] } vi.mocked(api.listStorageRentLots).mockResolvedValue({ records: [mixedLot], total: 1, page: 1, size: 20, pages: 1 }) vi.mocked(api.getStorageRentLot).mockResolvedValue({ lot: mixedLot, charges: [ { id: '3', outboundNo: 'OUT1', businessNo: 'FL1', quantity: 1, storageDays: 8, originalAmount: 12, currency: 'USD', settlementAmount: 84, currencyRate: 7, status: 'SETTLED', createdAt: '2026-10-22T01:00:00Z', outboundAt: '2026-10-22T01:00:00Z' }, { id: '4', outboundNo: 'OUT2', businessNo: 'FL1', quantity: 1, storageDays: 9, originalAmount: 23, currency: 'CNY', settlementAmount: 23, currencyRate: 1, status: 'SETTLED', createdAt: '2026-10-22T02:00:00Z', outboundAt: '2026-10-22T02:00:00Z' }, ] }) const wrapper = mount(StorageRentPanel, { attachTo: document.body, props: { customers: [] }, global: { plugins: [ElementPlus] } }) await flushPromises() const body = wrapper.get('.el-table__body-wrapper') expect(body.text()).toContain('8 箱') expect(body.text()).not.toContain('USD') expect(body.text()).not.toContain('CNY') await body.findAll('button').find(button => button.text().trim() === '详情')!.trigger('click'); await flushPromises() const dialog = document.body.querySelector('[role="dialog"]')! expect(dialog.textContent).toContain('OUT1') expect(dialog.textContent).toContain('OUT2') expect(dialog.textContent).toContain('计租天数') const rows = Array.from(dialog.querySelectorAll('.el-table__body-wrapper tbody tr')) expect(rows[0]?.textContent).toContain('8') expect(rows[1]?.textContent).toContain('9') expect(dialog.textContent).not.toContain('USD') expect(dialog.textContent).not.toContain('CNY') }) })