2026-08-06 19:03:13 +08:00
|
|
|
import { computed, defineComponent } from 'vue'
|
|
|
|
|
import { shallowMount } from '@vue/test-utils'
|
|
|
|
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
|
|
|
import DataTable, { createPaginationItems, normalizePage, normalizePageSizes, type DataTableColumn } from './DataTable.vue'
|
|
|
|
|
import LayoutPreferenceList from './LayoutPreferenceList.vue'
|
|
|
|
|
import { UI_PREFERENCE_SCOPE, preferenceStorageKey } from '@/shared/utils/uiLayoutPreference'
|
|
|
|
|
|
|
|
|
|
const TableStub = defineComponent({ template: '<div><slot /></div>', methods: { doLayout() {} } })
|
|
|
|
|
const ColumnStub = defineComponent({
|
|
|
|
|
inheritAttrs: false,
|
|
|
|
|
props: { label: String },
|
|
|
|
|
template: '<div class="column-stub" :data-label="label"><slot /></div>',
|
|
|
|
|
})
|
|
|
|
|
const ButtonStub = defineComponent({ template: '<button type="button"><slot /></button>' })
|
|
|
|
|
const PopoverStub = defineComponent({ template: '<div><slot name="reference" /><slot /></div>' })
|
|
|
|
|
|
|
|
|
|
const configurableColumns: DataTableColumn[] = [
|
|
|
|
|
{ key: 'actions', label: '操作', fixed: 'left' },
|
|
|
|
|
{ key: 'name', prop: 'name', label: '名称' },
|
|
|
|
|
{ key: 'status', prop: 'status', label: '状态' },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
function mountTable() {
|
|
|
|
|
return shallowMount(DataTable, {
|
2026-08-11 20:39:34 +08:00
|
|
|
props: { data: [], columns: configurableColumns, preferenceKey: 'orders-table', columnSettingsMode: 'show' },
|
2026-08-06 19:03:13 +08:00
|
|
|
global: {
|
|
|
|
|
provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'user-1') },
|
|
|
|
|
stubs: {
|
|
|
|
|
ElTable: TableStub,
|
|
|
|
|
ElTableColumn: ColumnStub,
|
|
|
|
|
ElButton: ButtonStub,
|
|
|
|
|
ElPopover: PopoverStub,
|
|
|
|
|
},
|
|
|
|
|
directives: { loading: {} },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-08-01 12:17:33 +08:00
|
|
|
|
|
|
|
|
describe('DataTable pagination helpers', () => {
|
|
|
|
|
it('normalizes page-size options', () => {
|
|
|
|
|
expect(normalizePageSizes([20, 10, 20, 0, -1, 0.8, 10.8])).toEqual([20, 10])
|
|
|
|
|
expect(normalizePageSizes([])).toEqual([10])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps the page inside the available range', () => {
|
|
|
|
|
expect(normalizePage(0, 8)).toBe(1)
|
|
|
|
|
expect(normalizePage(12, 8)).toBe(8)
|
|
|
|
|
expect(normalizePage(3.8, 8)).toBe(3)
|
|
|
|
|
expect(normalizePage(Number.NaN, 8)).toBe(1)
|
|
|
|
|
expect(normalizePage(4, 0)).toBe(1)
|
|
|
|
|
expect(normalizePage(4, Number.NaN)).toBe(1)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('does not create page buttons for an empty result', () => {
|
|
|
|
|
expect(createPaginationItems(1, 0)).toEqual([])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows all pages for a short result set', () => {
|
|
|
|
|
expect(createPaginationItems(3, 5)).toEqual([1, 2, 3, 4, 5])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps first and last pages visible for long result sets', () => {
|
|
|
|
|
expect(createPaginationItems(1, 30)).toEqual([1, 2, 3, 4, 5, 'ellipsis-end', 30])
|
|
|
|
|
expect(createPaginationItems(15, 30)).toEqual([1, 'ellipsis-start', 14, 15, 16, 'ellipsis-end', 30])
|
|
|
|
|
expect(createPaginationItems(30, 30)).toEqual([1, 'ellipsis-start', 26, 27, 28, 29, 30])
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-08-06 19:03:13 +08:00
|
|
|
|
|
|
|
|
describe('DataTable column preferences', () => {
|
|
|
|
|
beforeEach(() => localStorage.clear())
|
|
|
|
|
|
|
|
|
|
it('restores column order and visibility while locking the actions column', async () => {
|
|
|
|
|
localStorage.setItem(preferenceStorageKey('table', 'user-1', 'orders-table'), JSON.stringify({
|
|
|
|
|
version: 1,
|
2026-08-06 19:18:48 +08:00
|
|
|
order: ['status', 'name', 'actions'],
|
2026-08-06 19:03:13 +08:00
|
|
|
hidden: ['name', 'actions'],
|
|
|
|
|
}))
|
|
|
|
|
const wrapper = mountTable()
|
|
|
|
|
expect(wrapper.findAll('.column-stub').map((column) => column.attributes('data-label'))).toEqual(['操作', '状态'])
|
|
|
|
|
|
|
|
|
|
const settings = wrapper.findComponent(LayoutPreferenceList)
|
|
|
|
|
const items = settings.props('items') as Array<{ key: string; movable: boolean; hideable: boolean }>
|
|
|
|
|
expect(items.find((item) => item.key === 'actions')).toMatchObject({ movable: false, hideable: false })
|
|
|
|
|
|
|
|
|
|
settings.vm.$emit('visibility', 'name', true)
|
|
|
|
|
settings.vm.$emit('move', 'name', 0)
|
|
|
|
|
await wrapper.vm.$nextTick()
|
|
|
|
|
expect(wrapper.findAll('.column-stub').map((column) => column.attributes('data-label'))).toEqual(['操作', '名称', '状态'])
|
|
|
|
|
})
|
2026-08-06 20:14:56 +08:00
|
|
|
|
2026-08-12 14:08:33 +08:00
|
|
|
it('keeps a newly introduced default-hidden column hidden for an existing preference', () => {
|
|
|
|
|
localStorage.setItem(preferenceStorageKey('table', 'user-1', 'orders-table'), JSON.stringify({
|
|
|
|
|
version: 1,
|
|
|
|
|
order: ['actions', 'name', 'status'],
|
|
|
|
|
hidden: [],
|
|
|
|
|
}))
|
|
|
|
|
const wrapper = shallowMount(DataTable, {
|
|
|
|
|
props: {
|
|
|
|
|
data: [],
|
|
|
|
|
columns: [...configurableColumns, { key: 'internalCode', label: '内部编码', defaultVisible: false }],
|
|
|
|
|
preferenceKey: 'orders-table',
|
|
|
|
|
columnSettingsMode: 'show',
|
|
|
|
|
},
|
|
|
|
|
global: {
|
|
|
|
|
provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'user-1') },
|
|
|
|
|
stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub },
|
|
|
|
|
directives: { loading: {} },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(wrapper.findAll('.column-stub').map((column) => column.attributes('data-label'))).toEqual(['操作', '名称', '状态'])
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-06 20:14:56 +08:00
|
|
|
it('keeps business actions, column settings, and trailing controls in one toolbar', () => {
|
|
|
|
|
const wrapper = shallowMount(DataTable, {
|
2026-08-11 20:39:34 +08:00
|
|
|
props: { data: [], columns: configurableColumns, preferenceKey: 'orders-table', columnSettingsMode: 'show' },
|
2026-08-06 20:14:56 +08:00
|
|
|
slots: {
|
|
|
|
|
'toolbar-start': '<h2>订单列表</h2>',
|
|
|
|
|
'toolbar-actions': '<button>新增订单</button>',
|
2026-08-10 16:24:54 +08:00
|
|
|
'toolbar-end': '<button>辅助操作</button>',
|
2026-08-06 20:14:56 +08:00
|
|
|
},
|
|
|
|
|
global: {
|
|
|
|
|
provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'user-1') },
|
|
|
|
|
stubs: {
|
|
|
|
|
ElTable: TableStub,
|
|
|
|
|
ElTableColumn: ColumnStub,
|
|
|
|
|
ElButton: ButtonStub,
|
|
|
|
|
ElPopover: PopoverStub,
|
|
|
|
|
},
|
|
|
|
|
directives: { loading: {} },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(wrapper.findAll('.data-table__toolbar')).toHaveLength(1)
|
2026-08-10 16:24:54 +08:00
|
|
|
expect(wrapper.get('.data-table__toolbar').classes()).not.toContain('is-compact')
|
2026-08-06 20:14:56 +08:00
|
|
|
expect(wrapper.get('.data-table__toolbar-start').text()).toBe('订单列表')
|
|
|
|
|
expect(wrapper.get('.data-table__toolbar-actions').text()).toContain('新增订单')
|
2026-08-10 16:24:54 +08:00
|
|
|
expect(wrapper.get('.data-table__toolbar-actions').text()).toContain('辅助操作')
|
|
|
|
|
expect(wrapper.get('.data-table__business-actions').text()).toBe('新增订单')
|
2026-08-10 17:42:17 +08:00
|
|
|
expect(wrapper.get('.data-table__utility-actions').text()).toContain('辅助操作')
|
2026-08-10 16:24:54 +08:00
|
|
|
expect(wrapper.get('.data-table__toolbar-end').text()).toBe('辅助操作')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('uses a compact right-aligned toolbar when no leading content is provided', () => {
|
|
|
|
|
const wrapper = shallowMount(DataTable, {
|
2026-08-11 20:39:34 +08:00
|
|
|
props: { data: [], columns: configurableColumns, preferenceKey: 'orders-table', columnSettingsMode: 'show' },
|
2026-08-10 16:24:54 +08:00
|
|
|
slots: { 'toolbar-actions': '<button>新增订单</button>' },
|
|
|
|
|
global: {
|
|
|
|
|
provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'user-1') },
|
|
|
|
|
stubs: {
|
|
|
|
|
ElTable: TableStub,
|
|
|
|
|
ElTableColumn: ColumnStub,
|
|
|
|
|
ElButton: ButtonStub,
|
|
|
|
|
ElPopover: PopoverStub,
|
|
|
|
|
},
|
|
|
|
|
directives: { loading: {} },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(wrapper.get('.data-table__toolbar').classes()).toContain('is-compact')
|
|
|
|
|
expect(wrapper.find('.data-table__toolbar-start').exists()).toBe(false)
|
2026-08-10 17:42:17 +08:00
|
|
|
expect(wrapper.get('.data-table__toolbar-actions').classes()).toContain('has-business-actions')
|
2026-08-06 20:14:56 +08:00
|
|
|
expect(wrapper.get('.data-table__business-actions').text()).toBe('新增订单')
|
|
|
|
|
})
|
2026-08-10 17:42:17 +08:00
|
|
|
|
|
|
|
|
it('keeps business actions in a separate mobile-wrappable group from column settings', () => {
|
|
|
|
|
const wrapper = shallowMount(DataTable, {
|
2026-08-11 20:39:34 +08:00
|
|
|
props: { data: [], columns: configurableColumns, preferenceKey: 'orders-table', columnSettingsMode: 'show' },
|
2026-08-10 17:42:17 +08:00
|
|
|
slots: { 'toolbar-actions': '<button class="create-action">新增快递公司</button>' },
|
|
|
|
|
global: {
|
|
|
|
|
provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'user-1') },
|
|
|
|
|
stubs: {
|
|
|
|
|
ElTable: TableStub,
|
|
|
|
|
ElTableColumn: ColumnStub,
|
|
|
|
|
ElButton: ButtonStub,
|
|
|
|
|
ElPopover: PopoverStub,
|
|
|
|
|
},
|
|
|
|
|
directives: { loading: {} },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
const toolbarActions = wrapper.get('.data-table__toolbar-actions')
|
|
|
|
|
const businessActions = toolbarActions.get('.data-table__business-actions')
|
|
|
|
|
|
|
|
|
|
expect(toolbarActions.classes()).toContain('has-business-actions')
|
|
|
|
|
expect(businessActions.get('.create-action').text()).toBe('新增快递公司')
|
|
|
|
|
expect(toolbarActions.find('.data-table__utility-actions').exists()).toBe(true)
|
|
|
|
|
expect(toolbarActions.find('button[aria-label="列设置"]').exists()).toBe(true)
|
|
|
|
|
})
|
2026-08-11 20:02:38 +08:00
|
|
|
|
|
|
|
|
it('can hide column settings for compact tables without leaving an empty toolbar', () => {
|
|
|
|
|
const wrapper = shallowMount(DataTable, {
|
2026-08-11 20:39:34 +08:00
|
|
|
props: { data: [], columns: configurableColumns, preferenceKey: 'summary-table', columnSettingsMode: 'hide' },
|
2026-08-11 20:02:38 +08:00
|
|
|
global: {
|
|
|
|
|
provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'user-1') },
|
|
|
|
|
stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub },
|
|
|
|
|
directives: { loading: {} },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(wrapper.find('button[aria-label="列设置"]').exists()).toBe(false)
|
|
|
|
|
expect(wrapper.find('.data-table__toolbar').exists()).toBe(false)
|
|
|
|
|
})
|
2026-08-11 20:39:34 +08:00
|
|
|
|
|
|
|
|
it('shows every column when column settings are hidden', () => {
|
|
|
|
|
localStorage.setItem('cc:ui-layout:user-1:table:summary-table', JSON.stringify({
|
|
|
|
|
version: 1,
|
|
|
|
|
order: configurableColumns.map((column) => String(column.key)),
|
|
|
|
|
hidden: ['status'],
|
|
|
|
|
}))
|
|
|
|
|
const wrapper = shallowMount(DataTable, {
|
|
|
|
|
props: { data: [], columns: configurableColumns, preferenceKey: 'summary-table', columnSettingsMode: 'hide' },
|
|
|
|
|
global: {
|
|
|
|
|
provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'user-1') },
|
|
|
|
|
stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub },
|
|
|
|
|
directives: { loading: {} },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(wrapper.findAllComponents(ColumnStub)).toHaveLength(configurableColumns.length)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('hides column settings by default for four or fewer configurable business columns', () => {
|
|
|
|
|
const wrapper = shallowMount(DataTable, {
|
|
|
|
|
props: { data: [], columns: configurableColumns, preferenceKey: 'compact-table' },
|
|
|
|
|
global: {
|
|
|
|
|
provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'user-1') },
|
|
|
|
|
stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub },
|
|
|
|
|
directives: { loading: {} },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(wrapper.find('button[aria-label="列设置"]').exists()).toBe(false)
|
|
|
|
|
expect(wrapper.find('.data-table__toolbar').exists()).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps column settings by default for complex tables', () => {
|
|
|
|
|
const complexColumns: DataTableColumn[] = [
|
|
|
|
|
{ key: 'actions', label: '操作' },
|
|
|
|
|
...Array.from({ length: 5 }, (_, index) => ({ key: `field-${index}`, label: `字段 ${index + 1}` })),
|
|
|
|
|
]
|
|
|
|
|
const wrapper = shallowMount(DataTable, {
|
|
|
|
|
props: { data: [], columns: complexColumns, preferenceKey: 'complex-table' },
|
|
|
|
|
global: {
|
|
|
|
|
provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'user-1') },
|
|
|
|
|
stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub },
|
|
|
|
|
directives: { loading: {} },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(wrapper.find('button[aria-label="列设置"]').exists()).toBe(true)
|
|
|
|
|
})
|
2026-08-06 19:03:13 +08:00
|
|
|
})
|