import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { defineComponent, reactive, nextTick, ref } from 'vue' import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import ElementPlus from 'element-plus' import type { Component } from 'vue' import type { Supplier } from '@/modules/suppliers/types' const mocks = vi.hoisted(() => ({ route: null as null | { params: { id: string } }, push: vi.fn(), getSupplier: vi.fn(), updateSupplier: vi.fn(), createSupplier: vi.fn(), listSuppliers: vi.fn(), getSupplierStats: vi.fn(), changeSupplierStatus: vi.fn(), can: vi.fn((permission: string) => permission === 'platform:supplier:list' || permission === 'platform:supplier:edit' || permission === 'platform:supplier:create'), })) const routeState = reactive({ params: { id: 'A' } }) mocks.route = routeState let SupplierFormView: Component let SupplierDetailView: Component let SuppliersView: Component vi.mock('vue-router', () => ({ useRoute: () => mocks.route, useRouter: () => ({ push: mocks.push }) })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: mocks.can }) })) vi.mock('@/core/unsaved-work/useUnsavedWorkRegistration', () => ({ useUnsavedWorkRegistration: () => ({ captureBaseline: vi.fn() }) })) vi.mock('@/modules/suppliers/api/platformSuppliers', () => ({ getSupplier: mocks.getSupplier, updateSupplier: mocks.updateSupplier, createSupplier: mocks.createSupplier, listSuppliers: mocks.listSuppliers, getSupplierStats: mocks.getSupplierStats, changeSupplierStatus: mocks.changeSupplierStatus, })) function supplier(id: string, name: string): Supplier { return { id, tenantId: '0', name, category: 'BOOKING', truckingScope: null, carrierCodes: ['MATSON', 'SML'], region: '宁波', legalRepresentative: '法人', contactName: '联系人', phone: '13800000000', email: null, address: null, businessLicenseNo: null, licenseFileId: null, status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: null, createdBy: null, createdAt: '', updatedBy: null, updatedAt: '', deleted: false, version: 1 } } function deferred() { let resolve!: (value: T) => void const promise = new Promise((next) => { resolve = next }) return { promise, resolve } } const stubs = { AdminPage: { template: '
' }, DetailPageLayout: { template: '
' }, FormSection: { template: '
' }, ApiErrorMeta: { template: '' }, LoadErrorState: { template: '
' }, SupplierLicenseDialog: { template: '
' }, ListPageLayout: { template: '
' }, QueryToolbar: { template: '
' }, DataTable: { template: '
' }, StatusBadge: { template: '' }, } enableAutoUnmount(afterEach) describe('supplier route reuse', () => { beforeAll(async () => { SupplierFormView = (await import('@/modules/suppliers/pages/SupplierFormView.vue')).default SupplierDetailView = (await import('@/modules/suppliers/pages/SupplierDetailView.vue')).default SuppliersView = (await import('@/modules/suppliers/pages/SuppliersView.vue')).default }) beforeEach(() => { routeState.params.id = 'A' mocks.push.mockReset(); mocks.getSupplier.mockReset(); mocks.updateSupplier.mockReset().mockResolvedValue(null); mocks.createSupplier.mockReset().mockResolvedValue('99') mocks.listSuppliers.mockReset().mockResolvedValue({ records: [], page: 1, size: 20, total: 0, pages: 0 }) mocks.getSupplierStats.mockReset().mockResolvedValue({ booking: 0, customs: 0, trucking: 0, certificate: 0 }) mocks.changeSupplierStatus.mockReset().mockResolvedValue(null) mocks.can.mockClear() }) it('retains all supplier facts with long values and the original back target', async () => { const name = '供应商完整长名称'.repeat(6) const address = '浙江省宁波市完整详细地址'.repeat(8) mocks.getSupplier.mockResolvedValue({ ...supplier('A', name), address, remark: '备注'.repeat(50) }) const wrapper = mount(SupplierDetailView, { global: { plugins: [ElementPlus], stubs } }) await flushPromises() expect(wrapper.get('.density-detail-title').text()).toBe(name) expect(wrapper.get('.density-fields').findAll('dt')).toHaveLength(12) expect(wrapper.get('.density-fields').text()).toContain(address) expect(wrapper.findAll('.wide-field')).toHaveLength(3) await wrapper.get('.density-back').trigger('click') expect(mocks.push).toHaveBeenCalledWith({ name: 'supplier-list' }) }) it('reloads form A to B, ignores late A, and submits B', async () => { const requestA = deferred(); const requestB = deferred() mocks.getSupplier.mockImplementation((id: string) => id === 'A' ? requestA.promise : requestB.promise) const wrapper = mount(SupplierFormView, { global: { plugins: [ElementPlus], stubs } }) expect(mocks.getSupplier).toHaveBeenCalledWith('A') routeState.params.id = 'B' await nextTick() expect(mocks.getSupplier).toHaveBeenLastCalledWith('B') requestB.resolve(supplier('B', '供应商B')); await flushPromises() expect((wrapper.find('input').element as HTMLInputElement).value).toBe('供应商B') requestA.resolve(supplier('A', '供应商A')); await flushPromises() expect((wrapper.find('input').element as HTMLInputElement).value).toBe('供应商B') expect((wrapper.find('input').element as HTMLInputElement).value).not.toBe('供应商A') const saveButton = wrapper.findAll('button').find((button) => button.text().includes('保存')) expect(saveButton).toBeDefined() await saveButton!.trigger('click') await flushPromises() expect(mocks.updateSupplier).toHaveBeenCalledWith('B', expect.objectContaining({ name: '供应商B', carrierCodes: ['MATSON', 'SML'] })) expect(mocks.push).toHaveBeenCalledWith({ name: 'supplier-list' }) }) it('reloads detail A to B when the route is reused', async () => { const requestA = deferred(); const requestB = deferred() mocks.getSupplier.mockImplementation((id: string) => id === 'A' ? requestA.promise : requestB.promise) const wrapper = mount(SupplierDetailView, { global: { plugins: [ElementPlus], stubs } }) routeState.params.id = 'B'; await nextTick() requestB.resolve(supplier('B', '供应商B')); await flushPromises() expect(wrapper.text()).toContain('供应商B') requestA.resolve(supplier('A', '供应商A')); await flushPromises() expect(wrapper.text()).toContain('供应商B') expect(wrapper.text()).not.toContain('供应商A') }) it('resets a cached create form every time it is activated', async () => { routeState.params.id = '' const active = ref(true) const Host = defineComponent({ components: { SupplierFormView }, setup: () => ({ active }), template: '', }) const wrapper = mount(Host, { global: { plugins: [ElementPlus], stubs } }) await nextTick() const name = wrapper.find('input[placeholder="请输入供应商名称"]') await name.setValue('上一次新增的数据') expect((name.element as HTMLInputElement).value).toBe('上一次新增的数据') active.value = false; await nextTick() active.value = true; await nextTick() expect((wrapper.find('input[placeholder="请输入供应商名称"]').element as HTMLInputElement).value).toBe('') expect(mocks.getSupplier).not.toHaveBeenCalled() }) it('reloads the cached supplier list every time it is activated', async () => { routeState.params.id = '' const active = ref(true) const Host = defineComponent({ components: { SuppliersView }, setup: () => ({ active }), template: '', }) mount(Host, { global: { plugins: [ElementPlus], stubs } }) await flushPromises() expect(mocks.listSuppliers).toHaveBeenCalledTimes(1) active.value = false; await nextTick() active.value = true; await flushPromises() expect(mocks.listSuppliers).toHaveBeenCalledTimes(2) expect(mocks.getSupplierStats).toHaveBeenCalledTimes(2) }) it('requires one trucking type, saves overseas and clears the type after changing category', async () => { mocks.getSupplier.mockResolvedValue({ ...supplier('A', '车队'), category: 'TRUCKING', carrierCodes: [], truckingScope: null }) const wrapper = mount(SupplierFormView, { global: { plugins: [ElementPlus], stubs } }) await flushPromises() const save = wrapper.findAll('button').find((button) => button.text().includes('保存'))! await save.trigger('click'); await flushPromises() expect(mocks.updateSupplier).not.toHaveBeenCalled() expect(wrapper.findAllComponents({ name: 'ElFormItem' }).find(item => item.props('prop') === 'truckingScope')?.props('error')).toBe('请选择国内拖车或海外车队') const radios = wrapper.findComponent({ name: 'ElRadioGroup' }) expect(radios.text()).toContain('国内拖车') expect(radios.text()).toContain('海外车队') radios.vm.$emit('update:modelValue', 'OVERSEAS') await flushPromises(); await save.trigger('click'); await flushPromises() expect(mocks.updateSupplier).toHaveBeenLastCalledWith('A', expect.objectContaining({ category: 'TRUCKING', truckingScope: 'OVERSEAS' })) wrapper.findAllComponents({ name: 'ElSelect' })[0]!.vm.$emit('update:modelValue', 'CUSTOMS') await flushPromises(); await save.trigger('click'); await flushPromises() expect(wrapper.findComponent({ name: 'ElRadioGroup' }).exists()).toBe(false) expect(mocks.updateSupplier).toHaveBeenLastCalledWith('A', expect.objectContaining({ category: 'CUSTOMS', truckingScope: null })) }) })