Files
mortis.yi 9eadaed2b1 fix(components): [table-v2] keep scrollToRow from changing horiz offset (#22930)
* fix(components): [table-v2] Rebase merge dev branch

closed #22928

* feat(components): [table-v2] keep scrollToRow from changing horiz offset

closed #22928

---------

Co-authored-by: alex.yang <alex.yang@hytechc.com>
Co-authored-by: 云游君 <me@yunyoujun.cn>
2026-01-09 15:22:37 +08:00

403 lines
11 KiB
TypeScript

import { h, nextTick, ref } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, expect, test } from 'vitest'
import TableV2, { type TableV2Instance } from '../src/table-v2'
import { TableV2SortOrder } from '../index'
import type { ScrollPos } from '../src/composables/use-scrollbar'
import type {
TableV2HeaderRowCellRendererParams,
TableV2RowCellRenderParam,
} from '../src/components'
import type { SortBy } from '../src/types'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
describe('TableV2.vue', () => {
test('slots cell', async () => {
const columns = ref(generateColumns(10))
const data = ref(generateData(columns.value, 20))
const customText = 'custom cell'
const wrapper = mount(() => (
<TableV2
columns={columns.value}
data={data.value}
width={700}
height={400}
v-slots={{
cell: ({ columnIndex, rowIndex }: TableV2RowCellRenderParam) =>
columnIndex === 0 && rowIndex === 0 ? (
<span>{customText}</span>
) : null,
}}
/>
))
expect(wrapper.find('.el-table-v2').exists()).toBe(true)
const cell = wrapper.findAll('.el-table-v2__row-cell')
expect(cell[0].find('.el-table-v2__cell-text').exists()).toBe(false)
expect(cell[0].find('span').exists()).toBe(true)
expect(cell[0].find('span').text()).toBe(customText)
expect(cell[1].find('span').exists()).toBe(false)
expect(cell[1].find('.el-table-v2__cell-text').exists()).toBe(true)
expect(cell[1].find('.el-table-v2__cell-text').text()).toBe('Row 0 - Col 1')
})
test('slots header-cell', async () => {
const columns = ref(generateColumns(10))
const data = ref(generateData(columns.value, 20))
const customText = 'header'
const wrapper = mount(() => (
<TableV2
columns={columns.value}
data={data.value}
width={700}
height={400}
v-slots={{
'header-cell': ({
columnIndex,
}: TableV2HeaderRowCellRendererParams) =>
columnIndex === 0 ? <span>{customText}</span> : null,
}}
/>
))
expect(wrapper.find('.el-table-v2').exists()).toBe(true)
const cell = wrapper.findAll('.el-table-v2__header-cell')
expect(cell[0].find('.el-table-v2__header-cell-text').exists()).toBe(false)
expect(cell[0].find('span').exists()).toBe(true)
expect(cell[0].find('span').text()).toBe(customText)
expect(cell[1].find('span').exists()).toBe(false)
expect(cell[1].find('.el-table-v2__header-cell-text').exists()).toBe(true)
expect(cell[1].find('.el-table-v2__header-cell-text').text()).toBe(
'Column 1'
)
})
test('slots empty', async () => {
const columns = ref(generateColumns(3))
const data = ref(generateData(columns.value, 0))
const isCustomEmpty = ref(true)
const wrapper = mount(() => (
<TableV2
columns={columns.value}
data={data.value}
width={700}
height={400}
v-slots={{
empty: () =>
isCustomEmpty.value ? (
<span class="custom-empty">custom-empty</span>
) : null,
}}
/>
))
expect(wrapper.find('.el-table-v2').exists()).toBe(true)
let customEmpty = wrapper.find('span.custom-empty')
expect(customEmpty.exists()).toBe(true)
let defaultEmpty = wrapper.find('.el-empty')
expect(defaultEmpty.exists()).toBe(false)
isCustomEmpty.value = false
await nextTick()
customEmpty = wrapper.find('span.custom-empty')
expect(customEmpty.exists()).toBe(false)
defaultEmpty = wrapper.find('.el-empty')
expect(defaultEmpty.exists()).toBe(true)
})
test('slots cell scope', async () => {
const columns = ref(generateColumns(10))
const data = ref(generateData(columns.value, 20))
const customText = 'custom cell'
const wrapper = mount(() => (
<TableV2
columns={columns.value}
data={data.value}
width={700}
height={400}
v-slots={{
cell: (scope: TableV2RowCellRenderParam) => (
<span>
{scope.rowData[scope.column.dataKey!]}
{customText}
</span>
),
}}
/>
))
expect(wrapper.find('.el-table-v2').exists()).toBe(true)
const cell = wrapper.find('.el-table-v2__row-cell')
expect(cell.exists()).toBe(true)
expect(cell.find('span').text()).toBe(
`${data.value[0][columns.value[0].dataKey]}${customText}`
)
})
test('slots header-cell scope', async () => {
const columns = ref(generateColumns(10))
const data = ref(generateData(columns.value, 20))
const customText = 'header'
const wrapper = mount(() => (
<TableV2
columns={columns.value}
data={data.value}
width={700}
height={400}
v-slots={{
'header-cell': (scope: TableV2HeaderRowCellRendererParams) => (
<span>
{scope.column.title}
{customText}
</span>
),
}}
/>
))
expect(wrapper.find('.el-table-v2').exists()).toBe(true)
const cell = wrapper.find('.el-table-v2__header-cell')
expect(cell.exists()).toBe(true)
expect(cell.find('span').text()).toBe(
`${columns.value[0].title}${customText}`
)
})
test('respects column flexShrink = 0', async () => {
const columns = ref([
{
key: 'col-0',
dataKey: 'col-0',
title: 'Col 0',
width: 200,
flexShrink: 0,
},
{
key: 'col-1',
dataKey: 'col-1',
title: 'Col 1',
width: 200,
flexShrink: 0,
},
])
const data = ref([
{
id: 'row-0',
'col-0': 'Row 0 - Col 0',
'col-1': 'Row 0 - Col 1',
},
])
const wrapper = mount(() => (
<TableV2
columns={columns.value}
data={data.value}
width={300}
height={200}
/>
))
const rowCells = wrapper.findAll('.el-table-v2__row-cell')
expect(rowCells.length).toBeGreaterThanOrEqual(2)
expect(rowCells[0].attributes('style')).toContain('flex-shrink: 0;')
expect(rowCells[1].attributes('style')).toContain('flex-shrink: 0;')
})
test('expandable mode wrongly enabled, by column not key', async () => {
const columns = ref([
{
width: 50,
title: 'd',
cellRenderer: ({ rowIndex }: { rowIndex: number }) => {
return h('span', null, rowIndex)
},
},
{
width: 50,
title: 'd',
key: 'expandColumnKey',
cellRenderer: ({ rowIndex }: { rowIndex: number }) => {
return h('span', null, rowIndex)
},
},
])
const data = ref(generateData(columns.value, 20))
const wrapper = mount(() => (
<TableV2
columns={columns.value}
data={data.value}
width={700}
height={400}
/>
))
expect(wrapper.find('.el-table-v2').exists()).toBe(true)
const cell = wrapper.find('.el-table-v2__row-cell')
expect(cell.exists()).toBe(true)
expect(cell.find('div [style^=margin-inline-star]').exists()).toBe(false)
})
test('scrollToRow keeps horizontal offset unchanged', async () => {
const columns = ref(generateColumns(10))
const data = ref(generateData(columns.value, 200))
const scrollLogs: ScrollPos[] = []
const wrapper = mount(() => (
<TableV2
fixed
columns={columns.value}
data={data.value}
width={400}
height={400}
onScroll={(pos: ScrollPos) => scrollLogs.push({ ...pos })}
/>
))
const tableVm = wrapper.findComponent(TableV2).vm.$
.exposed as TableV2Instance
tableVm.scrollToLeft(150)
await nextTick()
await nextTick()
const prevScrollLeft = scrollLogs[scrollLogs.length - 1]?.scrollLeft ?? 0
expect(prevScrollLeft).toBeGreaterThan(0)
tableVm.scrollToRow(50)
await nextTick()
await nextTick()
const lastScrollLeft = scrollLogs[scrollLogs.length - 1]?.scrollLeft ?? 0
expect(lastScrollLeft).toBe(prevScrollLeft)
})
describe('a11y', () => {
test('expand button', async () => {
const columns = generateColumns(10)
const data = [
{
id: 0,
[columns[0].dataKey]: 'Row 0 - Col 0',
children: generateData(columns, 20),
},
]
const wrapper = mount(() => (
<TableV2
columns={columns}
data={data}
width={700}
height={400}
expand-column-key="column-0"
/>
))
const expandButton = wrapper.find('.el-table-v2__expand-icon')
expect(expandButton.attributes('arialabel')).toBe('Expand this row')
expect(expandButton.attributes('ariaexpanded')).toBe('false')
await expandButton.trigger('click')
await nextTick()
expect(expandButton.attributes('ariaexpanded')).toBe('true')
})
test('sort button', async () => {
const sortState = ref<SortBy>({
key: 'column-0',
order: TableV2SortOrder.ASC,
})
const columns = generateColumns(10)
const data = generateData(columns, 20)
columns[0].sortable = true
const wrapper = mount(() => (
<TableV2
columns={columns}
data={data}
width={700}
height={400}
sortBy={sortState.value}
/>
))
const sortButton = wrapper.find('.el-table-v2__sort-icon')
const header = wrapper.find('.el-table-v2__header-cell.is-sortable')
console.log(header.attributes())
expect(sortButton.attributes('aria-label')).toBe('Sort by Column 0')
expect(header.attributes('ariasort')).toBe('ascending')
expect(header.attributes('role')).toBe('columnheader')
})
})
test('expand button of sub-row should not apply margin style of icon', async () => {
const columns = [
{
key: 'column-0',
dataKey: 'column-0',
title: 'Column 0',
width: 150,
},
]
const data = [
{
id: 'row-0',
'column-0': 'Row 0 - Col 0',
children: [
{
id: 'row-0-sub-0',
parentId: 'row-0',
'column-0': 'Sub 0',
children: [
{
id: 'row-0-sub-0-sub-0',
parentId: 'row-0-sub-0',
'column-0': 'Sub-Sub 0',
},
],
},
],
},
]
const wrapper = mount(() => (
<TableV2
columns={columns}
data={data}
width={300}
height={200}
expand-column-key="column-0"
/>
))
const expandButton = wrapper.find('.el-table-v2__expand-icon')
await expandButton.trigger('click')
await nextTick()
const subExpandButton = wrapper.findAll('.el-table-v2__expand-icon')[1]
expect(subExpandButton.attributes('style')).toBeFalsy()
})
})