/** * 自定义导出弹窗:选列、三种导出方式、保存导出列。 * * Modal 用 Teleport 挂到 body,所以断言都从 document 上找节点(与 modal-confirm.spec.js 一致)。 * 这里守住的是「弹窗参数拼得对不对」—— 导出本身由后端测试覆盖,前端错在这里最容易漏。 */ import { flushPromises, mount } from '@vue/test-utils' import { afterEach, describe, expect, it, vi } from 'vitest' import { nextTick } from 'vue' import FmsExportDialog from '@/components/fms-module-list/FmsExportDialog.vue' import { countExportApi, exportListApi } from '@/services/exportService' vi.mock('@/services/exportService', () => ({ countExportApi: vi.fn(), exportListApi: vi.fn(), })) const columns = [ { field: 'b_name', caption: '名称' }, { field: 'b_bz', caption: '备注' }, { field: 'b_amount', caption: '金额' }, ] const panel = () => document.querySelector('.fms-modal') const checkboxes = () => [...document.querySelectorAll('.export-dialog__field input')] const footerButtons = () => [...document.querySelectorAll('.fms-modal-footer button')] const radios = () => [...document.querySelectorAll('.export-dialog__modes input')] function buttonByText(text) { return [...document.querySelectorAll('.fms-modal button')].find( (button) => button.textContent.trim() === text, ) } async function openDialog(props = {}) { const wrapper = mount(FmsExportDialog, { props: { open: false, moduleId: 'v_test', moduleName: '测试模块', columns, ...props, }, }) await wrapper.setProps({ open: true }) await flushPromises() await nextTick() return wrapper } /** 下载按钮(footer 第二个),点击后返回给 exportListApi 的参数 */ async function download() { const button = footerButtons()[1] button.click() await flushPromises() await nextTick() } describe('FmsExportDialog 自定义导出', () => { afterEach(() => { vi.clearAllMocks() document.body.innerHTML = '' }) it('全部列已勾选但顺序不同仍可恢复列表顺序', async () => { countExportApi.mockResolvedValue({ data: { total: 8 } }) const wrapper = await openDialog({ savedColumns: ['b_amount', 'b_name', 'b_bz'] }) expect(buttonByText('全选').disabled).toBe(false) buttonByText('全选').click() await nextTick() await download() expect(exportListApi.mock.calls.at(-1)[0].columns).toEqual(['b_name', 'b_bz', 'b_amount']) wrapper.unmount() }) it('重新打开后忽略上次未完成的计数响应', async () => { let finishFirst countExportApi.mockImplementationOnce( () => new Promise((resolve) => { finishFirst = resolve }), ) const wrapper = await openDialog() await wrapper.setProps({ open: false }) countExportApi.mockResolvedValueOnce({ data: { total: 7 } }) await wrapper.setProps({ open: true, searchCondition: 'b_canuse = 1' }) await flushPromises() finishFirst({ data: { total: 100 } }) await flushPromises() expect(document.querySelector('.export-dialog__total').textContent).toContain('共 7 条数据') wrapper.unmount() }) it('打开后渲染全部候选列且默认全选,并显示后端统计的条数', async () => { countExportApi.mockResolvedValue({ data: { total: 8 } }) exportListApi.mockResolvedValue('x.xlsx') await openDialog() expect(panel()).not.toBeNull() expect(checkboxes()).toHaveLength(3) expect(checkboxes().every((input) => input.checked)).toBe(true) expect(countExportApi).toHaveBeenCalledWith({ moduleId: 'v_test', searchCondition: '', }) expect(panel().textContent).toContain('8') }) it('取消勾选后下载只带剩下的列', async () => { countExportApi.mockResolvedValue({ data: { total: 8 } }) exportListApi.mockResolvedValue('x.xlsx') await openDialog() const remark = checkboxes()[1] remark.checked = false remark.dispatchEvent(new Event('change')) await nextTick() await download() expect(exportListApi).toHaveBeenCalledTimes(1) const params = exportListApi.mock.calls[0][0] expect(params.moduleId).toBe('v_test') expect(params.columns).toEqual(['b_name', 'b_amount']) // 默认「全部导出」不带区间与分卷 expect(params.rangeStart).toBeUndefined() expect(params.volumeSize).toBeUndefined() }) it('沿用列表已保存的导出列,且保存的顺序优先(其余列补在后面)', async () => { countExportApi.mockResolvedValue({ data: { total: 8 } }) exportListApi.mockResolvedValue('x.xlsx') await openDialog({ savedColumns: ['b_amount', 'b_name'] }) // 显示顺序 = 已保存顺序 + 未保存的列(未勾选) const names = [...document.querySelectorAll('.export-dialog__field-name')].map( (n) => n.textContent, ) expect(names).toEqual(['金额', '名称', '备注']) expect(checkboxes().filter((input) => input.checked)).toHaveLength(2) await download() // 导出列顺序跟随弹窗里的显示顺序 expect(exportListApi.mock.calls[0][0].columns).toEqual(['b_amount', 'b_name']) }) it('按条数区间导出带 rangeStart / rangeEnd', async () => { countExportApi.mockResolvedValue({ data: { total: 800 } }) exportListApi.mockResolvedValue('x.xlsx') await openDialog() radios()[1].checked = true radios()[1].dispatchEvent(new Event('change')) await nextTick() const numbers = [...document.querySelectorAll('.export-dialog__params input')] expect(numbers).toHaveLength(2) numbers[0].value = '101' numbers[0].dispatchEvent(new Event('input')) numbers[1].value = '200' numbers[1].dispatchEvent(new Event('input')) await nextTick() await download() const params = exportListApi.mock.calls[0][0] expect(params.rangeStart).toBe(101) expect(params.rangeEnd).toBe(200) expect(params.volumeSize).toBeUndefined() }) it('分卷导出带 volumeSize 并沿用默认合计', async () => { countExportApi.mockResolvedValue({ data: { total: 800 } }) exportListApi.mockResolvedValue('x.zip') await openDialog() radios()[2].checked = true radios()[2].dispatchEvent(new Event('change')) await nextTick() const sizeInput = document.querySelector('.export-dialog__params input') sizeInput.value = '300' sizeInput.dispatchEvent(new Event('input')) await nextTick() await download() const params = exportListApi.mock.calls[0][0] expect(params.volumeSize).toBe(300) expect(params.rangeStart).toBeUndefined() }) it('区间不合法时不发起请求', async () => { countExportApi.mockResolvedValue({ data: { total: 800 } }) await openDialog() radios()[1].checked = true radios()[1].dispatchEvent(new Event('change')) await nextTick() const numbers = [...document.querySelectorAll('.export-dialog__params input')] numbers[0].value = '500' numbers[0].dispatchEvent(new Event('input')) numbers[1].value = '100' numbers[1].dispatchEvent(new Event('input')) await nextTick() await download() expect(exportListApi).not.toHaveBeenCalled() }) it('一列都不选时不发起请求', async () => { countExportApi.mockResolvedValue({ data: { total: 8 } }) await openDialog() for (const input of checkboxes()) { input.checked = false input.dispatchEvent(new Event('change')) } await nextTick() await download() expect(exportListApi).not.toHaveBeenCalled() }) it('导出方式:标题与 Segmented 同行,参数行是它下面的独立一行', async () => { countExportApi.mockResolvedValue({ data: { total: 800 } }) await openDialog() const mode = document.querySelector('.export-dialog__mode') // 直接子元素顺序:标题、Segmented、(选中区间/分卷时的)参数行 const children = [...mode.children].map((node) => node.className) expect(children[0]).toContain('export-dialog__title') expect(children[1]).toContain('export-dialog__modes') // 「全部」模式没有参数行 expect(mode.querySelector('.export-dialog__params')).toBeNull() radios()[1].checked = true radios()[1].dispatchEvent(new Event('change')) await nextTick() // 参数行是 mode 的直接子元素(grid 第二行第二列),不是包在另一层容器里 const params = document.querySelector('.export-dialog__mode > .export-dialog__params') expect(params).not.toBeNull() expect([...mode.children].indexOf(params)).toBe(2) }) it('保存配置上抛字段;全选等于回到列表列并清掉个人覆盖', async () => { countExportApi.mockResolvedValue({ data: { total: 8 } }) const wrapper = await openDialog({ savedColumns: ['b_amount'] }) buttonByText('保存配置').click() await nextTick() expect(wrapper.emitted('save-columns')?.at(-1)).toEqual([['b_amount']]) buttonByText('全选').click() await nextTick() // 全选 = 回到列表列,顺手清掉个人覆盖(空数组) expect(wrapper.emitted('save-columns')?.at(-1)).toEqual([[]]) expect(checkboxes().every((input) => input.checked)).toBe(true) await download() expect(exportListApi.mock.calls.at(-1)[0].columns).toEqual(['b_name', 'b_bz', 'b_amount']) }) })