Files
workspace/code/fms/fms-vue/tests/unit/fms-export-dialog.spec.js
T
2026-09-21 23:13:14 +08:00

265 lines
9.2 KiB
JavaScript

/**
* 自定义导出弹窗:选列、三种导出方式、保存导出列。
*
* 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'])
})
})