import { readFileSync } from 'node:fs' import { join } from 'node:path' import ElementPlus from 'element-plus' import { createPinia, setActivePinia } from 'pinia' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getPlatformPartner, listPlatformPartners, type PlatformPartnerResource, } from '@/modules/master-data/counterparties/api/platformPartners' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useAuthStore } from '@/core/auth/auth' import QueryToolbar from '@/shared/components/QueryToolbar.vue' import PartnersView from './PartnersView.vue' const source = readFileSync(join(process.cwd(), 'src/modules/master-data/counterparties/pages/PartnersView.vue'), 'utf8') vi.mock('@/modules/master-data/counterparties/api/platformPartners', () => ({ listPlatformPartners: vi.fn(), createPlatformPartner: vi.fn(), getPlatformPartner: vi.fn(), downloadPlatformPartnerImportTemplate: vi.fn(), importPlatformPartners: vi.fn(), updatePlatformPartner: vi.fn(), changePlatformPartnerStatus: vi.fn(), })) class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} } function createMemoryStorage(): Storage { const values = new Map() 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 partner: PlatformPartnerResource = { id: '19001', tenantId: 0, dataScopeId: 0, partnerCode: 'SHP-001', partnerName: '深圳示例贸易有限公司', partnerType: 'SHIPPER', types: ['SHIPPER'], carrierCode: null, contactName: '张三', contactPhone: '13800138000', contactEmail: 'contact@example.com', countryCode: 'CN', province: '广东省', city: '深圳市', district: '宝安区', addressLine: '航城街道示例路 1 号', postalCode: '518000', status: 'ACTIVE', remark: '长期合作单位', updatedAt: '2026-07-30T22:45:00', } async function mountView() { vi.stubGlobal('localStorage', createMemoryStorage()) vi.stubGlobal('sessionStorage', createMemoryStorage()) const pinia = createPinia() setActivePinia(pinia) const auth = useAuthStore() auth.token = 'token' auth.permissions = [ 'platform:counterparty:list', 'platform:counterparty:create', 'platform:counterparty:edit', 'platform:counterparty:status', ] useAccessPreviewStore().initializeAfterLogin() const wrapper = mount(PartnersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] }, }) await flushPromises() return wrapper } enableAutoUnmount(afterEach) afterAll(() => vi.unstubAllGlobals()) describe('PartnersView real API integration', () => { it('clamps long unit names with the shared component', () => { expect(source).toContain("LineClampText from '@/shared/components/LineClampText.vue'") expect(source).toContain('') expect(source).not.toContain('.partner-cell strong') }) it('keeps operation first and separates the prototype contact columns', () => { const actionsIndex = source.indexOf("{ key: 'actions', label: '操作'") const nameIndex = source.indexOf("{ key: 'partner', label: '单位名称'") const typeIndex = source.indexOf("{ key: 'type', label: '类型'") const contactIndex = source.indexOf("{ key: 'contact', prop: 'contactName', label: '联系人'") const phoneIndex = source.indexOf("{ key: 'contactPhone', prop: 'contactPhone', label: '电话'") expect([actionsIndex, nameIndex, typeIndex, contactIndex, phoneIndex].every((index) => index >= 0)).toBe(true) expect(actionsIndex).toBeLessThan(nameIndex) expect(nameIndex).toBeLessThan(typeIndex) expect(typeIndex).toBeLessThan(contactIndex) expect(contactIndex).toBeLessThan(phoneIndex) expect(source).toContain("{ key: 'partnerCode', prop: 'partnerCode', label: '单位编码', minWidth: 130, defaultVisible: false }") expect(source).toContain("{ key: 'contactEmail', prop: 'contactEmail', label: '邮箱', minWidth: 190, defaultVisible: false") }) beforeEach(() => { vi.stubGlobal('ResizeObserver', ResizeObserverStub) document.body.innerHTML = '' vi.mocked(listPlatformPartners).mockReset().mockResolvedValue({ records: [partner], page: 1, size: 20, total: 1, pages: 1, }) vi.mocked(getPlatformPartner).mockReset().mockResolvedValue(partner) }) it('loads platform partner data and renders the real partner fields', async () => { const wrapper = await mountView() expect(listPlatformPartners).toHaveBeenCalledWith({ page: 1, size: 20, keyword: undefined, type: undefined, status: undefined, }) expect(wrapper.find('input[aria-label="所属企业"]').exists()).toBe(false) expect(wrapper.text()).toContain('平台公共数据 · 共 1 个往来单位') expect(wrapper.text()).toContain('深圳示例贸易有限公司') expect(wrapper.find('.data-table').text()).toContain('13800138000') expect(wrapper.find('.data-table').text()).not.toContain('CUS-001') expect(wrapper.find('.data-table').text()).not.toContain('contact@example.com') expect(wrapper.find('.data-table').text()).not.toContain('广东省 深圳市 宝安区 航城街道示例路 1 号') expect(wrapper.text()).not.toContain('往来单位总数') expect(wrapper.text()).not.toContain('往来单位档案') expect(wrapper.find('.query-toolbar').exists()).toBe(true) expect(wrapper.find('.data-table').exists()).toBe(true) expect(wrapper.find('.el-table').exists()).toBe(true) expect(wrapper.find('.query-toolbar__actions').text()).not.toContain('新增往来单位') expect(wrapper.find('.data-table__business-actions').text()).toContain('新增往来单位') expect(wrapper.get('.data-table__toolbar-start h1').text()).toBe('往来单位') expect(wrapper.find('.data-table__pagination-left').text()).toContain('平台公共数据 · 共 1 个往来单位') expect(wrapper.find('.data-table__notices').exists()).toBe(false) const detailButton = wrapper.findAll('button').find((button) => button.text().trim() === '详情') await detailButton?.trigger('click') await flushPromises() expect(document.body.textContent).toContain('广东省 深圳市 宝安区 航城街道示例路 1 号') }) it('opens the create form without selecting an enterprise', async () => { const wrapper = await mountView() const createButton = wrapper.findAll('button').find((button) => button.text().trim() === '新增往来单位') await createButton?.trigger('click') await flushPromises() expect(document.body.textContent).toContain('新增往来单位') expect(document.body.textContent).not.toContain('请先在筛选条件中选择所属企业') expect(document.body.textContent).toContain('船公司') expect(document.body.textContent).toContain('发货单位') expect(document.body.textContent).toContain('收货单位') expect(document.body.textContent).not.toContain('报关行') expect(document.body.textContent).not.toContain('供应商') }) it('uses the fixed carrier-code selector when the unit is a shipping company', async () => { const wrapper = await mountView() await wrapper.findAll('button').find((button) => button.text().trim() === '新增往来单位')?.trigger('click') await flushPromises() const typeSelect = wrapper.findAllComponents({ name: 'ElSelect' }).find((select) => select.props('multiple')) typeSelect!.vm.$emit('update:modelValue', ['CARRIER']) await flushPromises() expect(document.body.textContent).toContain('船司标准编码') const carrierValues = new Set(['MATSON', 'SML', 'ZIM', 'COSCO', 'EMC', 'HEDE', 'OTHER']) const carrierOptions = wrapper.findAllComponents({ name: 'ElOption' }).filter((option) => carrierValues.has(String(option.props('value')))) expect(carrierOptions.map((option) => option.props('value'))).toEqual(['MATSON', 'SML', 'ZIM', 'COSCO', 'EMC', 'HEDE', 'OTHER']) expect(carrierOptions.map((option) => option.props('label'))).toContain('美森 (MATSON)') expect(source).toContain('placeholder="请选择船司标准编码"') expect(source).toContain("MATSON: '美森'") expect(source).toContain("OTHER: '其他'") expect(source).toContain("carrierCode: types.includes('CARRIER') ? form.carrierCode : null") }) it('renders a padded detail layout without an empty data-scope cell', async () => { const wrapper = await mountView() const detailButton = wrapper.findAll('button').find((button) => button.text().trim() === '详情') await detailButton?.trigger('click') await flushPromises() const detailContent = document.querySelector('.detail-drawer .partner-detail') const sections = detailContent?.querySelectorAll('section') const otherInfo = sections?.item(sections.length - 1) expect(detailContent).not.toBeNull() expect(detailContent?.closest('.el-drawer')?.classList.contains('admin-density-drawer')).toBe(true) expect(detailContent?.querySelectorAll('.density-fields')).toHaveLength(3) expect(otherInfo?.querySelectorAll('.detail-grid > .detail-span')).toHaveLength(2) expect(otherInfo?.textContent).toContain('更新时间') expect(otherInfo?.textContent).toContain('长期合作单位') expect(document.body.textContent).not.toContain('数据范围') }) it('updates filters locally and only requests after search', async () => { const wrapper = await mountView() const keywordInput = wrapper.find('input[placeholder="请输入往来单位名称"]') await keywordInput.setValue('深圳') await flushPromises() expect(listPlatformPartners).toHaveBeenCalledTimes(1) const searchButton = wrapper.findAll('button').find((button) => button.text().trim() === '查询') expect(searchButton).toBeDefined() await searchButton?.trigger('click') await flushPromises() expect(listPlatformPartners).toHaveBeenCalledTimes(2) expect(listPlatformPartners).toHaveBeenLastCalledWith({ page: 1, size: 20, keyword: '深圳', type: undefined, status: undefined, }) }) it('shows useful placeholders and visible required markers in the create form', async () => { const wrapper = await mountView() const toolbar = wrapper.findComponent(QueryToolbar) const model = { keyword: '', type: '', status: '' } toolbar.vm.$emit('update:modelValue', model) toolbar.vm.$emit('search', model) await flushPromises() const createButton = wrapper.findAll('button').find((button) => button.text().trim() === '新增往来单位') expect(createButton).toBeDefined() await createButton?.trigger('click') await flushPromises() expect(document.querySelector('input[placeholder="请输入工商登记或常用单位名称"]')).not.toBeNull() expect(document.querySelector('input[placeholder="请输入主要联系人姓名"]')).not.toBeNull() expect(document.querySelector('textarea[placeholder="请输入街道、园区、楼栋及门牌信息"]')).not.toBeNull() expect(document.querySelectorAll('.el-form-item.is-required').length).toBe(9) }) it('shows a recoverable API failure without demo data', async () => { vi.mocked(listPlatformPartners).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('.data-table__notices').exists()).toBe(true) expect(wrapper.find('.el-table').exists()).toBe(false) }) it('keeps the latest detail when an older partner request finishes last', async () => { const secondPartner = { ...partner, id: '19002', partnerCode: 'CUS-002', partnerName: '上海示例贸易有限公司' } let resolveFirst!: (value: PlatformPartnerResource) => void let resolveSecond!: (value: PlatformPartnerResource) => void vi.mocked(getPlatformPartner).mockImplementation((partnerId) => new Promise((resolve) => { if (String(partnerId) === String(partner.id)) resolveFirst = resolve else resolveSecond = resolve })) const wrapper = await mountView() const vm = wrapper.vm as unknown as { detailOpen: boolean detail: PlatformPartnerResource | null openDetail(row: PlatformPartnerResource): Promise } void vm.openDetail(partner) vm.detailOpen = false await wrapper.vm.$nextTick() void vm.openDetail(secondPartner) resolveSecond(secondPartner) await flushPromises() resolveFirst(partner) await flushPromises() expect(vm.detail?.partnerCode).toBe('CUS-002') }) it('keeps all three row actions visible without a premature more menu', () => { const actions = source.slice(source.indexOf('