Files
workspace/code/fms/fms-vue/tests/fms-table-virtual.spec.js
T
2026-09-29 17:22:12 +08:00

268 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { nextTick } from 'vue'
import { registerFeature, useAreaSelection, StkTable } from 'stk-table-vue'
import FmsTable from '@/components/fms-table/FmsTable.vue'
// 与 main.js 一致:注册 stk-table 的按需 feature(行编辑依赖 area-selection)
registerFeature(useAreaSelection)
const COLS = 20
function makeColumns() {
const cols = []
for (let i = 0; i < COLS; i++) {
cols.push({ title: `列 ${i}`, dataIndex: `c${i}`, width: 120, editor: { type: 'input' } })
}
return cols
}
function makeRows(n) {
return Array.from({ length: n }, (_, i) => {
const row = { id: i + 1 }
for (let j = 0; j < COLS; j++) row[`c${j}`] = `${i + 1}-${j}`
return row
})
}
describe('fms-table 虚拟滚动验证', () => {
beforeEach(() => setActivePinia(createPinia()))
it('5000 行时仅渲染可视区行数(虚拟滚动切片生效)', async () => {
const wrapper = mount(FmsTable, {
props: { columns: makeColumns(), dataSource: makeRows(5000), rowKey: 'id', seq: false },
attachTo: document.body,
})
await nextTick()
await new Promise((r) => setTimeout(r, 100))
const trCount = wrapper.findAll('.stk-tbody-main tr').length
console.log('=== 5000 行时 DOM 实际渲染的 <tr> 数量:', trCount)
expect(trCount).toBeGreaterThan(0)
expect(trCount).toBeLessThan(100)
wrapper.unmount()
})
it('20 行时全量渲染(数据量小于阈值关闭虚拟滚动,不再吞掉最后一行)', async () => {
const wrapper = mount(FmsTable, {
props: { columns: makeColumns(), dataSource: makeRows(20), rowKey: 'id', seq: false },
attachTo: document.body,
})
await nextTick()
await new Promise((r) => setTimeout(r, 100))
const trCount = wrapper.findAll('.stk-tbody-main tr').length
console.log('=== 20 行时 DOM 实际渲染的 <tr> 数量:', trCount)
expect(trCount).toBe(20)
wrapper.unmount()
})
it('数据量从 20 行增长到 5000 行后,DOM 行数保持为可视区行数', async () => {
const wrapper = mount(FmsTable, {
props: { columns: makeColumns(), dataSource: makeRows(20), rowKey: 'id', seq: false },
attachTo: document.body,
})
await nextTick()
await new Promise((r) => setTimeout(r, 100))
const before = wrapper.findAll('.stk-tbody-main tr').length
await wrapper.setProps({ dataSource: makeRows(5000) })
await nextTick()
await new Promise((r) => setTimeout(r, 100))
const after = wrapper.findAll('.stk-tbody-main tr').length
console.log(`=== 20 行时渲染 ${before} 行,改为 5000 行后渲染 ${after} 行`)
expect(before).toBeGreaterThan(0)
expect(after).toBeGreaterThan(0)
// 核心断言:5000 行数据时 DOM 只保留可视区行数,不会渲染全部 5000 行
expect(after).toBeLessThan(100)
wrapper.unmount()
})
// 性能优化回归(item #1):非编辑态下可编辑列/纯展示列走 StkTable 原生 td 渲染,
// 不注入 customCell 函数式组件;只有进入编辑后才挂载编辑器。
it('非编辑态:全部单元格走原生 table-cell-wrapper 渲染,零编辑器组件', async () => {
const wrapper = mount(FmsTable, {
props: { columns: makeColumns(), dataSource: makeRows(20), rowKey: 'id', seq: false },
attachTo: document.body,
})
await nextTick()
await new Promise((r) => setTimeout(r, 100))
const tds = wrapper.findAll('.stk-tbody-main td[data-col-key]')
expect(tds.length).toBeGreaterThan(0)
// 没有活动编辑行 → 不注入任何编辑器渲染
expect(wrapper.find('.fms-cell-editor-root').exists()).toBe(false)
// 每个数据单元格都走库原生 table-cell-wrapper 路径(无 fms-cell-display 包裹)
const nativeWrappers = wrapper.findAll('.stk-tbody-main .table-cell-wrapper')
expect(nativeWrappers.length).toBe(tds.length)
wrapper.unmount()
})
// 性能优化回归(item #1):进入编辑后仅活动编辑行挂载编辑器,其余行保持原生渲染
it('进入编辑后:仅活动编辑行挂载编辑器,其余行保持原生渲染', async () => {
const wrapper = mount(FmsTable, {
props: { columns: makeColumns(), dataSource: makeRows(20), rowKey: 'id', seq: false },
attachTo: document.body,
})
await nextTick()
await new Promise((r) => setTimeout(r, 100))
const trs = wrapper.findAll('.stk-tbody-main tr')
const td = trs[1].find('td[data-col-key]')
await td.trigger('mousedown', { button: 0 })
await nextTick()
await td.trigger('mouseup')
await nextTick()
await new Promise((r) => setTimeout(r, 100))
const editingRow = wrapper.findAll('.stk-tbody-main tr')[1]
const editorCountInEditingRow = editingRow.findAll('.fms-cell-editor-root').length
expect(editorCountInEditingRow).toBeGreaterThan(0)
expect(editorCountInEditingRow).toBeLessThanOrEqual(makeColumns().length)
// 其余行不挂载编辑器(保持原生渲染,滚动时行间切换不重建整表)
trs.forEach((tr, index) => {
if (index === 1) return
expect(tr.findAll('.fms-cell-editor-root').length).toBe(0)
})
wrapper.unmount()
})
it('点击单元格进入单行编辑(area-selection 注册后生效,编辑器渲染)', async () => {
const wrapper = mount(FmsTable, {
props: { columns: makeColumns(), dataSource: makeRows(20), rowKey: 'id', seq: false },
attachTo: document.body,
})
await nextTick()
await new Promise((r) => setTimeout(r, 100))
// 点击第 2 行第 1 个数据列(跳过 virtualX 的占位 td,选中带 data-col-key 的单元格)
const trs = wrapper.findAll('.stk-tbody-main tr')
const td = trs[1].find('td[data-col-key]')
console.log('=== td outerHTML:', td.element.outerHTML.slice(0, 200))
await td.trigger('mousedown', { button: 0 })
await nextTick()
console.log('=== mousedown 后 td data-cs-s:', td.attributes()['data-cs-s'])
await td.trigger('mouseup')
await nextTick()
await new Promise((r) => setTimeout(r, 100))
const editor = wrapper.find('.fms-cell-editor-root')
console.log('=== 点击后编辑器是否渲染:', editor.exists())
expect(editor.exists()).toBe(true)
wrapper.unmount()
})
// 列宽/列序拖拽依赖 v-model:columns 回写:未绑定的父组件(如 dashboard 单向 :columns)
// 拖了松手就回弹,因此未绑定时直接关闭这两个功能(不渲染把手),绑定时保持开启。
it('未绑定 v-model:columns 时关闭列宽拖拽与列头拖拽(无处回写)', async () => {
const wrapper = mount(FmsTable, {
props: { columns: makeColumns(), dataSource: makeRows(20), rowKey: 'id', seq: false },
attachTo: document.body,
})
await nextTick()
await new Promise((r) => setTimeout(r, 100))
const stkTable = wrapper.findComponent(StkTable)
expect(stkTable.props('colResizable')).toBe(false)
expect(stkTable.props('headerDrag')).toBe(false)
wrapper.unmount()
})
it('绑定 v-model:columns 时列宽拖拽与列头拖拽保持开启', async () => {
const wrapper = mount(FmsTable, {
props: {
columns: makeColumns(),
dataSource: makeRows(20),
rowKey: 'id',
seq: false,
// 模拟 v-model:columns:提供 update:columns 监听器
'onUpdate:columns': () => {},
},
attachTo: document.body,
})
await nextTick()
await new Promise((r) => setTimeout(r, 100))
const stkTable = wrapper.findComponent(StkTable)
// 开启时统一传 { disabled } 回调(库只支持这种按列控制方式):普通列可拖,
// 列上显式写 resizable: false 的(操作列 / 展开列)不拖
const colResizable = stkTable.props('colResizable')
expect(colResizable).toMatchObject({ disabled: expect.any(Function) })
expect(colResizable.disabled({ dataIndex: 'c0', width: 120 })).toBe(false)
expect(colResizable.disabled({ dataIndex: '__actions__', resizable: false })).toBe(true)
expect(stkTable.props('headerDrag')).toBeTruthy()
wrapper.unmount()
})
it('autoFitWidth 默认关闭表格自动铺满宽度', async () => {
const wrapper = mount(FmsTable, {
props: {
columns: makeColumns(),
dataSource: makeRows(20),
rowKey: 'id',
seq: false,
'onUpdate:columns': () => {},
},
})
await nextTick()
expect(wrapper.find('.fms-table').classes()).not.toContain('fms-table--auto-fit-width')
expect(wrapper.findComponent(StkTable).props('colResizable')).toMatchObject({
disabled: expect.any(Function),
})
wrapper.unmount()
})
it('autoFitWidth=true 时关闭列宽拖拽并使用自适应布局', async () => {
const wrapper = mount(FmsTable, {
props: {
columns: makeColumns(),
dataSource: makeRows(20),
rowKey: 'id',
autoFitWidth: true,
'onUpdate:columns': () => {},
},
})
await nextTick()
expect(wrapper.find('.fms-table').classes()).toContain('fms-table--auto-fit-width')
expect(wrapper.findComponent(StkTable).props('colResizable')).toBe(false)
wrapper.unmount()
})
it('序号列和复选框列允许调整宽度且回写后保持', async () => {
const wrapper = mount(FmsTable, {
props: {
columns: [{ title: '名称', dataIndex: 'name', width: 120 }],
dataSource: [{ id: 1, name: '测试' }],
rowKey: 'id',
rowSelection: { selectedRowKeys: [], onChange: () => {} },
'onUpdate:columns': () => {},
},
})
await nextTick()
const table = wrapper.findComponent(StkTable)
const columns = table.props('columns')
const seq = columns.find((column) => column.type === 'seq')
const selection = columns.find((column) => column.__fmsSelection)
expect(seq?.headerAlign).toBe('center')
expect(seq?.resizable).not.toBe(false)
expect(selection?.resizable).not.toBe(false)
await table.vm.$emit(
'update:columns',
columns.map((column) =>
column.type === 'seq'
? { ...column, width: '72px' }
: column.__fmsSelection
? { ...column, width: '56px' }
: column,
),
)
await nextTick()
const updated = wrapper.findComponent(StkTable).props('columns')
expect(updated.find((column) => column.type === 'seq')?.width).toBe(72)
expect(updated.find((column) => column.__fmsSelection)?.width).toBe(56)
wrapper.unmount()
})
})