import { computed, defineComponent } from 'vue' import { shallowMount } from '@vue/test-utils' import { beforeEach, describe, expect, it } from 'vitest' import DataTable, { createPaginationItems, normalizePage, normalizePageSizes, normalizeTotal, type DataTableColumn } from './DataTable.vue' import LayoutPreferenceList from './LayoutPreferenceList.vue' import { UI_PREFERENCE_SCOPE, preferenceStorageKey } from '@/shared/utils/uiLayoutPreference' const TableStub = defineComponent({ props: { border: Boolean, stripe: Boolean, maxHeight: [String, Number], emptyText: String }, template: '
', methods: { doLayout() {} }, }) const ColumnStub = defineComponent({ inheritAttrs: false, props: { label: String, prop: String, fixed: [String, Boolean], showOverflowTooltip: Boolean, className: String }, template: '
', }) const ButtonStub = defineComponent({ template: '' }) const PopoverStub = defineComponent({ template: '
' }) const configurableColumns: DataTableColumn[] = [ { key: 'actions', label: '操作', fixed: 'left' }, { key: 'name', prop: 'name', label: '名称' }, { key: 'status', prop: 'status', label: '状态' }, ] function mountTable() { return shallowMount(DataTable, { props: { data: [], columns: configurableColumns, 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: {} }, }, }) } 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('normalizes totals serialized as JSON strings before calculating pages', () => { expect(normalizeTotal('30')).toBe(30) expect(normalizeTotal(' 30 ')).toBe(30) expect(normalizeTotal(30)).toBe(30) expect(normalizeTotal('')).toBe(0) expect(normalizeTotal('invalid')).toBe(0) }) it('renders page navigation when the API total is a numeric string', () => { const wrapper = shallowMount(DataTable, { props: { data: [], columns: configurableColumns, total: '30', page: 1, pageSize: 20 }, global: { stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub }, directives: { loading: {} }, }, }) expect(wrapper.get('.data-table__page-buttons').text()).toContain('1') expect(wrapper.get('.data-table__page-buttons').text()).toContain('2') }) 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]) }) }) describe('DataTable column preferences', () => { beforeEach(() => localStorage.clear()) it('enables bordered striped business tables by default and allows explicit opt-out', () => { const defaultTable = shallowMount(DataTable, { props: { data: [], columns: configurableColumns }, global: { stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub }, directives: { loading: {} }, }, }) expect(defaultTable.get('.table-stub').attributes()).toMatchObject({ 'data-border': 'true', 'data-stripe': 'true' }) const plainTable = shallowMount(DataTable, { props: { data: [], columns: configurableColumns, border: false, stripe: false }, global: { stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub }, directives: { loading: {} }, }, }) expect(plainTable.get('.table-stub').attributes()).toMatchObject({ 'data-border': 'false', 'data-stripe': 'false' }) }) it('keeps table bodies within the viewport by default and allows a custom height', () => { const defaultTable = mountTable() expect(defaultTable.get('.table-stub').attributes('data-max-height')).toBe('calc(100vh - 300px)') const customTable = shallowMount(DataTable, { props: { data: [], columns: configurableColumns, maxHeight: 420 }, global: { stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub }, directives: { loading: {} }, }, }) expect(customTable.get('.table-stub').attributes('data-max-height')).toBe('420') }) it('keeps fixed actions, long text, empty state, and pagination in the shared list contract', () => { const wrapper = shallowMount(DataTable, { props: { data: [], columns: [ { key: 'actions', label: '操作', fixed: 'left', slotName: 'actions' }, { key: 'description', label: '长文本', showOverflowTooltip: true }, ], emptyText: '暂无符合条件的数据', total: 0, }, global: { stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub }, directives: { loading: {} }, }, }) const columns = wrapper.findAll('.column-stub') expect(columns[0].attributes('data-fixed')).toBe('left') expect(columns[1].attributes('data-overflow-tooltip')).toBe('true') expect(wrapper.get('.table-stub').attributes('data-empty-text')).toBe('暂无符合条件的数据') expect(wrapper.get('.data-table__pagination').text()).toContain('共 0 条') }) it('keeps ordinary cells on one line with overflow tooltips and exempts system columns', () => { const wrapper = shallowMount(DataTable, { props: { data: [], columnSettingsMode: 'hide', columns: [ { key: 'actions', label: '操作', fixed: 'left', slotName: 'actions' }, { key: 'customerName', label: '客户' }, { key: 'remark', label: '备注', singleLine: false }, ], }, global: { stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub, ElSelect: true, ElOption: true }, directives: { loading: {} }, }, }) const columns = wrapper.findAll('.column-stub') expect(columns[0].attributes()).toMatchObject({ 'data-overflow-tooltip': 'undefined', 'data-class-name': 'data-table__system-cell', }) expect(columns[1].attributes()).toMatchObject({ 'data-overflow-tooltip': 'true', 'data-class-name': 'data-table__single-line-cell', }) expect(columns[2].attributes('data-overflow-tooltip')).toBe('undefined') expect(columns[2].attributes('data-class-name')).toBe('') }) it('uses key as the data property for ordinary columns only', () => { const wrapper = shallowMount(DataTable, { props: { data: [{ customerName: '客户A', explicitName: '显式字段' }], columns: [ { key: 'actions', label: '操作', slotName: 'actions' }, { key: 'customerName', label: '客户' }, { key: 'name', prop: 'explicitName', label: '显式字段' }, { key: 'selection', type: 'selection', label: '选择' }, ], }, global: { stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub }, directives: { loading: {} }, }, }) const columns = wrapper.findAll('.column-stub') expect(columns.map((column) => column.attributes('data-prop'))).toEqual([undefined, 'customerName', 'explicitName', undefined]) }) it('restores column order and visibility while locking the actions column', async () => { localStorage.setItem(preferenceStorageKey('table', 'user-1', 'orders-table'), JSON.stringify({ version: 1, order: ['status', 'name', 'actions'], 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(['操作', '名称', '状态']) }) 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(['操作', '名称', '状态']) }) it('keeps business actions, column settings, and trailing controls in one toolbar', () => { const wrapper = shallowMount(DataTable, { props: { data: [], columns: configurableColumns, preferenceKey: 'orders-table', columnSettingsMode: 'show' }, slots: { 'toolbar-start': '

订单列表

', 'toolbar-actions': '', 'toolbar-end': '', }, 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) expect(wrapper.get('.data-table__toolbar').classes()).not.toContain('is-compact') expect(wrapper.get('.data-table__toolbar-start').text()).toBe('订单列表') expect(wrapper.get('.data-table__toolbar-actions').text()).toContain('新增订单') expect(wrapper.get('.data-table__toolbar-actions').text()).toContain('辅助操作') expect(wrapper.get('.data-table__business-actions').text()).toBe('新增订单') expect(wrapper.get('.data-table__utility-actions').text()).toContain('辅助操作') 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, { props: { data: [], columns: configurableColumns, preferenceKey: 'orders-table', columnSettingsMode: 'show' }, slots: { 'toolbar-actions': '' }, 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) expect(wrapper.get('.data-table__toolbar-actions').classes()).toContain('has-business-actions') expect(wrapper.get('.data-table__business-actions').text()).toBe('新增订单') }) it('keeps business actions in a separate mobile-wrappable group from column settings', () => { const wrapper = shallowMount(DataTable, { props: { data: [], columns: configurableColumns, preferenceKey: 'orders-table', columnSettingsMode: 'show' }, slots: { 'toolbar-actions': '' }, 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) }) it('can hide column settings for compact tables without leaving an empty toolbar', () => { 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.find('button[aria-label="列设置"]').exists()).toBe(false) expect(wrapper.find('.data-table__toolbar').exists()).toBe(false) }) 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) }) })