import { afterEach, describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import DataFrame from '../../components/DataFrame.vue' import { useSettingsStore } from '../../stores/settings' import type { DataFrameColumn, SortState, PaginationState } from '../../types' describe('DataFrame', () => { const mockData = [ { id: 1, name: 'Alice', age: 30, email: 'alice@example.com' }, { id: 2, name: 'Bob', age: 25, email: 'bob@example.com' }, { id: 3, name: 'Charlie', age: 35, email: 'charlie@example.com' }, ] const mockColumns: DataFrameColumn[] = [ { key: 'name', label: 'Name' }, { key: 'age', label: 'Age', align: 'right' }, { key: 'email', label: 'Email' }, ] describe('rendering with default props', () => { it('should render table container', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, }) expect(wrapper.find('.mint-dataframe').exists()).toBe(true) expect(wrapper.find('.mint-dataframe__table').exists()).toBe(true) }) it('should render column headers', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, }) const headers = wrapper.findAll('.mint-dataframe__th') expect(headers).toHaveLength(3) expect(headers[0].text()).toBe('Name') expect(headers[1].text()).toBe('Age') expect(headers[2].text()).toBe('Email') }) it('should render data rows', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, }) const rows = wrapper.findAll('.mint-dataframe__row') expect(rows).toHaveLength(3) }) it('should render cell values', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, }) const firstRow = wrapper.findAll('.mint-dataframe__row')[0] const cells = firstRow.findAll('.mint-dataframe__td') expect(cells[0].text()).toBe('Alice') expect(cells[1].text()).toBe('30') expect(cells[2].text()).toBe('alice@example.com') }) it('should apply bordered class by default', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, }) expect(wrapper.find('.mint-dataframe--bordered').exists()).toBe(true) }) it('should apply striped rows by default', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, }) const rows = wrapper.findAll('.mint-dataframe__row') expect(rows[1].classes()).toContain('mint-dataframe__row--striped') }) }) describe('column configuration', () => { it('should apply column alignment', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, }) const ageHeader = wrapper.findAll('.mint-dataframe__th')[1] expect(ageHeader.classes()).toContain('mint-dataframe__th--align-right') }) it('should apply column width', () => { const columns: DataFrameColumn[] = [ { key: 'name', label: 'Name', width: 200 }, ] const wrapper = mount(DataFrame, { props: { data: mockData, columns }, }) const header = wrapper.find('.mint-dataframe__th') expect(header.attributes('style')).toContain('width: 200px') }) it('should support string width values', () => { const columns: DataFrameColumn[] = [ { key: 'name', label: 'Name', width: '20rem' }, ] const wrapper = mount(DataFrame, { props: { data: mockData, columns }, }) const header = wrapper.find('.mint-dataframe__th') expect(header.attributes('style')).toContain('width: 20rem') }) it('should apply minWidth', () => { const columns: DataFrameColumn[] = [ { key: 'name', label: 'Name', minWidth: 150 }, ] const wrapper = mount(DataFrame, { props: { data: mockData, columns }, }) const header = wrapper.find('.mint-dataframe__th') expect(header.attributes('style')).toContain('min-width: 150px') }) it('should apply ellipsis class to cells', () => { const columns: DataFrameColumn[] = [ { key: 'name', label: 'Name', ellipsis: true }, ] const wrapper = mount(DataFrame, { props: { data: mockData, columns }, }) const cell = wrapper.find('.mint-dataframe__td') expect(cell.classes()).toContain('mint-dataframe__td--ellipsis') }) it('should use custom formatter', () => { const columns: DataFrameColumn[] = [ { key: 'age', label: 'Age', formatter: (value) => `${value} years old`, }, ] const wrapper = mount(DataFrame, { props: { data: mockData, columns }, }) const ageCell = wrapper.findAll('.mint-dataframe__row')[0].findAll('.mint-dataframe__td')[0] expect(ageCell.text()).toBe('30 years old') }) }) describe('sorting', () => { const sortableColumns: DataFrameColumn[] = [ { key: 'name', label: 'Name', sortable: true }, { key: 'age', label: 'Age', sortable: true }, ] it('should show sort icon on sortable columns', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: sortableColumns }, }) const sortIcons = wrapper.findAll('.mint-dataframe__sort-icon') expect(sortIcons).toHaveLength(2) }) it('should apply sortable class to sortable headers', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: sortableColumns }, }) const headers = wrapper.findAll('.mint-dataframe__th') expect(headers[0].classes()).toContain('mint-dataframe__th--sortable') }) it('should sort ascending on first click', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: sortableColumns }, }) const nameHeader = wrapper.findAll('.mint-dataframe__th')[0] await nameHeader.get('.mint-dataframe__th-button').trigger('click') expect(wrapper.emitted('update:sort')).toHaveLength(1) const sortState = wrapper.emitted('update:sort')?.[0]?.[0] as SortState expect(sortState.key).toBe('name') expect(sortState.direction).toBe('asc') }) it('should sort descending on second click', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: sortableColumns, sort: { key: 'name', direction: 'asc' }, }, }) const nameHeader = wrapper.findAll('.mint-dataframe__th')[0] await nameHeader.get('.mint-dataframe__th-button').trigger('click') const sortState = wrapper.emitted('update:sort')?.[0]?.[0] as SortState expect(sortState.direction).toBe('desc') }) it('should clear sort on third click', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: sortableColumns, sort: { key: 'name', direction: 'desc' }, }, }) const nameHeader = wrapper.findAll('.mint-dataframe__th')[0] await nameHeader.get('.mint-dataframe__th-button').trigger('click') const sortState = wrapper.emitted('update:sort')?.[0]?.[0] expect(sortState).toBeNull() }) it('should sort data internally when sort prop is not provided', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: sortableColumns }, }) const nameHeader = wrapper.findAll('.mint-dataframe__th')[0] await nameHeader.get('.mint-dataframe__th-button').trigger('click') // After sorting, first row should have name starting with 'A' const firstRow = wrapper.findAll('.mint-dataframe__row')[0] const cells = firstRow.findAll('.mint-dataframe__td') expect(cells[0].text()).toBe('Alice') }) it('should sort numeric columns correctly', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: sortableColumns }, }) const ageHeader = wrapper.findAll('.mint-dataframe__th')[1] await ageHeader.get('.mint-dataframe__th-button').trigger('click') // After sorting ascending, first row should have age 25 const firstRow = wrapper.findAll('.mint-dataframe__row')[0] const cells = firstRow.findAll('.mint-dataframe__td') expect(cells[1].text()).toBe('25') }) it('should apply sorted class to active column', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: sortableColumns, sort: { key: 'name', direction: 'asc' }, }, }) const nameHeader = wrapper.findAll('.mint-dataframe__th')[0] expect(nameHeader.classes()).toContain('mint-dataframe__th--sorted') }) it('should enable sorting globally with sortable prop', () => { const columns: DataFrameColumn[] = [ { key: 'name', label: 'Name' }, { key: 'age', label: 'Age' }, ] const wrapper = mount(DataFrame, { props: { data: mockData, columns, sortable: true }, }) const sortableHeaders = wrapper.findAll('.mint-dataframe__th--sortable') expect(sortableHeaders).toHaveLength(2) }) }) describe('search filtering', () => { it('should show search input when searchable is true', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, searchable: true }, }) expect(wrapper.find('.mint-dataframe__search').exists()).toBe(true) }) it('should filter data based on search query', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, searchable: true }, }) const searchInput = wrapper.find('.mint-dataframe__search') await searchInput.setValue('Alice') const rows = wrapper.findAll('.mint-dataframe__row') expect(rows).toHaveLength(1) expect(rows[0].text()).toContain('Alice') }) it('should search across all columns by default', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, searchable: true }, }) const searchInput = wrapper.find('.mint-dataframe__search') await searchInput.setValue('bob@example.com') const rows = wrapper.findAll('.mint-dataframe__row') expect(rows).toHaveLength(1) expect(rows[0].text()).toContain('Bob') }) it('should search only specified columns', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, searchable: true, searchKeys: ['name'], }, }) const searchInput = wrapper.find('.mint-dataframe__search') await searchInput.setValue('bob@example.com') // Should not find Bob by email since we're only searching name const rows = wrapper.findAll('.mint-dataframe__row') expect(rows).toHaveLength(0) }) it('should treat whitespace-only search as empty', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, searchable: true }, }) const searchInput = wrapper.find('.mint-dataframe__search') await searchInput.setValue(' ') const rows = wrapper.findAll('.mint-dataframe__row') expect(rows).toHaveLength(3) }) it('should be case-insensitive', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, searchable: true }, }) const searchInput = wrapper.find('.mint-dataframe__search') await searchInput.setValue('ALICE') const rows = wrapper.findAll('.mint-dataframe__row') expect(rows).toHaveLength(1) }) it('should use custom placeholder', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, searchable: true, searchPlaceholder: 'Type to search...', }, }) const searchInput = wrapper.find('.mint-dataframe__search') expect(searchInput.attributes('placeholder')).toBe('Type to search...') }) }) describe('pagination', () => { const paginationState: PaginationState = { page: 1, pageSize: 2, total: 3, } it('should show pagination controls', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, pagination: paginationState }, }) expect(wrapper.find('.mint-dataframe__footer').exists()).toBe(true) expect(wrapper.find('.mint-dataframe__page-controls').exists()).toBe(true) }) it('should display page info', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, pagination: paginationState }, }) const pageInfo = wrapper.find('.mint-dataframe__page-info') expect(pageInfo.text()).toContain('1') expect(pageInfo.text()).toContain('3') }) it('should paginate data', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, pagination: paginationState }, }) const rows = wrapper.findAll('.mint-dataframe__row') expect(rows).toHaveLength(2) // pageSize is 2 }) it('should emit pagination event on page change', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, pagination: paginationState }, }) const nextButton = wrapper.findAll('.mint-dataframe__page-btn')[1] await nextButton.trigger('click') expect(wrapper.emitted('update:pagination')).toHaveLength(1) const newPagination = wrapper.emitted('update:pagination')?.[0]?.[0] as PaginationState expect(newPagination.page).toBe(2) }) it('should disable prev button on first page', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, pagination: paginationState }, }) const prevButton = wrapper.findAll('.mint-dataframe__page-btn')[0] expect(prevButton.attributes('disabled')).toBeDefined() }) it('should disable next button on last page', () => { const lastPageState: PaginationState = { page: 2, pageSize: 2, total: 3 } const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, pagination: lastPageState }, }) const nextButton = wrapper.findAll('.mint-dataframe__page-btn')[1] expect(nextButton.attributes('disabled')).toBeDefined() }) it('should not show pagination when false', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, pagination: false }, }) expect(wrapper.find('.mint-dataframe__footer').exists()).toBe(false) }) }) describe('row selection', () => { it('should show checkbox column when selectable', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, selectable: true }, }) expect(wrapper.find('.mint-dataframe__th--checkbox').exists()).toBe(true) }) it('should show checkboxes for each row', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, selectable: true }, }) const checkboxes = wrapper.findAll('.mint-dataframe__checkbox') // One for header + one for each row expect(checkboxes.length).toBeGreaterThan(mockData.length) }) it('should emit selectedKeys on row selection', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, selectable: true, rowKey: 'id' }, }) const rowCheckboxes = wrapper.findAll('.mint-dataframe__td--checkbox .mint-dataframe__checkbox input') await rowCheckboxes[0].trigger('change') expect(wrapper.emitted('update:selectedKeys')).toHaveLength(1) const keys = wrapper.emitted('update:selectedKeys')?.[0]?.[0] as number[] expect(keys).toContain(1) }) it('should toggle selection on row checkbox click', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, selectable: true, rowKey: 'id', selectedKeys: [1], }, }) const rowCheckboxes = wrapper.findAll('.mint-dataframe__td--checkbox .mint-dataframe__checkbox input') await rowCheckboxes[0].trigger('change') const keys = wrapper.emitted('update:selectedKeys')?.[0]?.[0] as number[] expect(keys).not.toContain(1) }) it('should select all rows with header checkbox', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, selectable: true, rowKey: 'id' }, }) const headerCheckbox = wrapper.find('.mint-dataframe__th--checkbox .mint-dataframe__checkbox input') await headerCheckbox.trigger('change') const keys = wrapper.emitted('update:selectedKeys')?.[0]?.[0] as number[] expect(keys).toHaveLength(3) }) it('should apply selected class to selected rows', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, selectable: true, rowKey: 'id', selectedKeys: [1], }, }) const selectedRow = wrapper.find('.mint-dataframe__row--selected') expect(selectedRow.exists()).toBe(true) }) it('should render row checkboxes as BaseCheckbox sm when selectable', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, selectable: true, rowKey: 'id' }, }) expect(wrapper.find('.mint-dataframe__td--checkbox .mint-checkbox__box--sm').exists()).toBe(true) }) it('should mark the header checkbox mixed when some rows are selected', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, selectable: true, rowKey: 'id', selectedKeys: [1] }, }) const header = wrapper.find('.mint-dataframe__th--checkbox input') expect(header.attributes('aria-checked')).toBe('mixed') }) it('should select all rows when the mixed header checkbox is clicked', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, selectable: true, rowKey: 'id', selectedKeys: [1] }, }) await wrapper.find('.mint-dataframe__th--checkbox input').setValue(true) const keys = wrapper.emitted('update:selectedKeys')?.[0]?.[0] as number[] expect(keys).toHaveLength(3) }) }) describe('events', () => { it('should emit row-click event', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, }) const firstRow = wrapper.findAll('.mint-dataframe__row')[0] await firstRow.trigger('click') expect(wrapper.emitted('row-click')).toHaveLength(1) const [row, index] = wrapper.emitted('row-click')?.[0] as [Record, number] expect(row.name).toBe('Alice') expect(index).toBe(0) }) it('should emit cell-click event', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, }) const firstCell = wrapper.find('.mint-dataframe__td') await firstCell.trigger('click') expect(wrapper.emitted('cell-click')).toHaveLength(1) const [value, column, row] = wrapper.emitted('cell-click')?.[0] as [unknown, DataFrameColumn, Record] expect(value).toBe('Alice') expect(column.key).toBe('name') expect(row.name).toBe('Alice') }) it('should emit row-click from cell clicks when clickableRows is true', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, clickableRows: true }, }) const firstCell = wrapper.find('.mint-dataframe__td') await firstCell.trigger('click') expect(wrapper.emitted('cell-click')).toHaveLength(1) expect(wrapper.emitted('row-click')).toHaveLength(1) const [row, index] = wrapper.emitted('row-click')?.[0] as [Record, number] expect(row.name).toBe('Alice') expect(index).toBe(0) }) it('should make clickable rows keyboard accessible', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, clickableRows: true }, }) const firstRow = wrapper.find('.mint-dataframe__row') expect(firstRow.attributes('tabindex')).toBe('0') expect(firstRow.attributes('role')).toBe('button') await firstRow.trigger('keydown', { key: 'Enter' }) await firstRow.trigger('keydown', { key: ' ' }) expect(wrapper.emitted('row-click')).toHaveLength(2) }) it('should keep non-clickable rows out of the tab order', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, }) const firstRow = wrapper.find('.mint-dataframe__row') expect(firstRow.attributes('tabindex')).toBeUndefined() expect(firstRow.attributes('role')).toBeUndefined() }) }) describe('empty state', () => { it('should show empty state when no data', () => { const wrapper = mount(DataFrame, { props: { data: [], columns: mockColumns }, }) expect(wrapper.find('.mint-dataframe__state').exists()).toBe(true) expect(wrapper.find('[data-state="empty"]').exists()).toBe(true) }) it('should display empty text', () => { const wrapper = mount(DataFrame, { props: { data: [], columns: mockColumns }, }) expect(wrapper.find('.mint-empty-state__title').text()).toBe('No data') }) it('should use custom empty text', () => { const wrapper = mount(DataFrame, { props: { data: [], columns: mockColumns, emptyText: 'No records found' }, }) expect(wrapper.find('.mint-empty-state__title').text()).toBe('No records found') }) it('should not show empty state when loading', () => { const wrapper = mount(DataFrame, { props: { data: [], columns: mockColumns, loading: true }, }) expect(wrapper.find('[data-state="empty"]').exists()).toBe(false) }) it('should render the empty slot instead of the default placeholder', () => { const wrapper = mount(DataFrame, { props: { data: [], columns: mockColumns }, slots: { empty: '

Nothing here

' }, }) expect(wrapper.find('.custom-empty').exists()).toBe(true) expect(wrapper.find('[data-state="empty"]').exists()).toBe(false) }) }) describe('loading state', () => { it('should show loading overlay', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, loading: true }, }) expect(wrapper.find('.mint-dataframe__loading').exists()).toBe(true) }) it('should show loading spinner', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, loading: true }, }) expect(wrapper.find('.mint-spinner').exists()).toBe(true) }) }) describe('error state', () => { it('should render nothing extra when error is not passed', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, }) expect(wrapper.find('[data-state="error"]').exists()).toBe(false) }) it('should replace the empty state with an error state', () => { const wrapper = mount(DataFrame, { props: { data: [], columns: mockColumns, error: 'Request failed' }, }) expect(wrapper.find('[data-state="error"]').exists()).toBe(true) expect(wrapper.find('[data-state="empty"]').exists()).toBe(false) expect(wrapper.find('.mint-empty-state__title').text()).toBe('Request failed') }) it('should take precedence over the empty state even with rows present', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, error: 'Boom' }, }) expect(wrapper.find('[data-state="error"]').exists()).toBe(true) }) it('should render the error slot with the message', () => { const wrapper = mount(DataFrame, { props: { data: [], columns: mockColumns, error: 'Boom' }, slots: { error: '

custom error

' }, }) expect(wrapper.find('.custom-error').exists()).toBe(true) }) }) describe('styling props', () => { it('should not apply striped when striped is false', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, striped: false }, }) expect(wrapper.find('.mint-dataframe__row--striped').exists()).toBe(false) }) it('should not apply bordered when bordered is false', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, bordered: false }, }) expect(wrapper.find('.mint-dataframe--bordered').exists()).toBe(false) }) it('should apply size class to cells', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, size: 'sm' }, }) const cell = wrapper.find('.mint-dataframe__td') expect(cell.classes()).toContain('mint-dataframe__td--sm') }) it('should apply sticky header class', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, stickyHeader: true }, }) expect(wrapper.find('.mint-dataframe__thead--sticky').exists()).toBe(true) }) it('should apply maxHeight style', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, maxHeight: 400 }, }) const tableWrapper = wrapper.find('.mint-dataframe__table-wrapper') expect(tableWrapper.attributes('style')).toContain('max-height: 400px') }) it('should support string maxHeight', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, maxHeight: '50vh' }, }) const tableWrapper = wrapper.find('.mint-dataframe__table-wrapper') expect(tableWrapper.attributes('style')).toContain('max-height: 50vh') }) }) describe('nested keys', () => { it('should access nested object properties', () => { const nestedData = [ { id: 1, user: { name: 'Alice', email: 'alice@example.com' } }, ] const nestedColumns: DataFrameColumn[] = [ { key: 'user.name', label: 'Name' }, { key: 'user.email', label: 'Email' }, ] const wrapper = mount(DataFrame, { props: { data: nestedData, columns: nestedColumns }, }) const cells = wrapper.findAll('.mint-dataframe__td') expect(cells[0].text()).toBe('Alice') expect(cells[1].text()).toBe('alice@example.com') }) }) describe('rowKey prop', () => { it('should use string rowKey', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, selectable: true, rowKey: 'id', }, }) expect(wrapper.find('.mint-dataframe').exists()).toBe(true) }) it('should use function rowKey', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, selectable: true, rowKey: (row) => `row-${row.id}`, }, }) expect(wrapper.find('.mint-dataframe').exists()).toBe(true) }) }) describe('slots', () => { it('should support header slot', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, slots: { 'header-name': '
Custom Name
', }, }) expect(wrapper.find('.custom-header').exists()).toBe(true) expect(wrapper.find('.custom-header').text()).toBe('Custom Name') }) it('should support cell slot', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, slots: { 'cell-name': '
{{ value }}
', }, }) expect(wrapper.find('.custom-cell').exists()).toBe(true) }) it('should support empty slot', () => { const wrapper = mount(DataFrame, { props: { data: [], columns: mockColumns }, slots: { empty: '
Nothing here
', }, }) expect(wrapper.find('.custom-empty').exists()).toBe(true) expect(wrapper.find('.custom-empty').text()).toBe('Nothing here') }) it('should support loading slot', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, loading: true }, slots: { loading: '
Loading...
', }, }) expect(wrapper.find('.custom-loading').exists()).toBe(true) }) it('should support footer slot', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, pagination: { page: 1, pageSize: 2, total: 3 }, }, slots: { footer: '', }, }) expect(wrapper.find('.custom-footer').exists()).toBe(true) }) }) describe('edge cases', () => { it('should handle null and undefined values', () => { const dataWithNulls = [ { id: 1, name: null, age: undefined, email: 'test@example.com' }, ] const wrapper = mount(DataFrame, { props: { data: dataWithNulls, columns: mockColumns }, }) const cells = wrapper.findAll('.mint-dataframe__td') expect(cells[0].text()).toBe('') expect(cells[1].text()).toBe('') }) it('should handle empty columns array', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: [] }, }) expect(wrapper.find('.mint-dataframe__table').exists()).toBe(true) }) it('should handle large datasets efficiently', () => { const largeData = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `User ${i}`, age: 20 + (i % 50), })) const wrapper = mount(DataFrame, { props: { data: largeData, columns: mockColumns, pagination: { page: 1, pageSize: 10, total: 1000 }, }, }) const rows = wrapper.findAll('.mint-dataframe__row') expect(rows).toHaveLength(10) }) it('should handle search with filtered pagination', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, searchable: true, pagination: { page: 1, pageSize: 2, total: 3 }, }, }) const searchInput = wrapper.find('.mint-dataframe__search') await searchInput.setValue('Alice') const rows = wrapper.findAll('.mint-dataframe__row') expect(rows).toHaveLength(1) }) it('should stop propagation on checkbox clicks', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, selectable: true, rowKey: 'id', }, }) const checkbox = wrapper.find('.mint-dataframe__td--checkbox .mint-dataframe__checkbox input') await checkbox.trigger('click') // Row click should not be emitted expect(wrapper.emitted('row-click')).toBeUndefined() }) }) describe('row deletion', () => { it('should not show an actions column by default', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, }) expect(wrapper.find('.mint-dataframe__td--actions').exists()).toBe(false) }) it('should show a delete button per row when deletable', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, deletable: true }, }) expect(wrapper.find('.mint-dataframe__th--actions').exists()).toBe(true) expect(wrapper.findAll('.mint-dataframe__delete-btn')).toHaveLength(mockData.length) }) it('should emit delete-row with the row and index', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, deletable: true }, }) await wrapper.findAll('.mint-dataframe__delete-btn')[1].trigger('click') expect(wrapper.emitted('delete-row')).toHaveLength(1) const [row, index] = wrapper.emitted('delete-row')?.[0] as [Record, number] expect(row.name).toBe('Bob') expect(index).toBe(1) }) it('should not emit row-click when the delete button is clicked', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, deletable: true }, }) await wrapper.find('.mint-dataframe__delete-btn').trigger('click') expect(wrapper.emitted('row-click')).toBeUndefined() }) it('should use a custom delete label', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, deletable: true, deleteLabel: 'Remove' }, }) expect(wrapper.find('.mint-dataframe__delete-btn').attributes('aria-label')).toBe('Remove') }) }) describe('column resizing', () => { it('should not show resize handles by default', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, }) expect(wrapper.find('.mint-dataframe__resize-handle').exists()).toBe(false) }) it('should show a resize handle per column when resizable', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, resizable: true }, }) expect(wrapper.findAll('.mint-dataframe__resize-handle')).toHaveLength(mockColumns.length) }) it('should emit update:columnWidths while dragging a handle', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, resizable: true }, }) const handle = wrapper.findAll('.mint-dataframe__resize-handle')[0] await handle.trigger('mousedown', { clientX: 100 }) window.dispatchEvent(new MouseEvent('mousemove', { clientX: 220 })) window.dispatchEvent(new MouseEvent('mouseup')) const events = wrapper.emitted('update:columnWidths') expect(events).toBeTruthy() const widths = events?.[events.length - 1]?.[0] as Record expect(widths.name).toBeGreaterThanOrEqual(48) // MIN_COL_WIDTH floor }) it('should honor controlled columnWidths', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, resizable: true, columnWidths: { name: 321 }, }, }) const header = wrapper.find('.mint-dataframe__th') expect(header.attributes('style')).toContain('width: 321px') }) it('should not trigger sort when the resize handle is clicked', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: [{ key: 'name', label: 'Name', sortable: true }], resizable: true, }, }) await wrapper.find('.mint-dataframe__resize-handle').trigger('click') expect(wrapper.emitted('update:sort')).toBeUndefined() }) it('should expose the resize handle as a slider with its current width', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: [{ key: 'name', label: 'Name', minWidth: 80 }], resizable: true, columnWidths: { name: 240 }, }, }) const handle = wrapper.get('.mint-dataframe__resize-handle') expect(handle.attributes('role')).toBe('slider') expect(handle.attributes('tabindex')).toBe('0') expect(handle.attributes('aria-label')).toBe('Resize Name column') expect(handle.attributes('aria-valuenow')).toBe('240') expect(handle.attributes('aria-valuemin')).toBe('80') }) it('should resize a column with the arrow keys, clamped to the minimum width', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: [{ key: 'name', label: 'Name' }], resizable: true, columnWidths: { name: 120 }, }, }) const handle = wrapper.get('.mint-dataframe__resize-handle') await handle.trigger('keydown', { key: 'ArrowRight' }) expect(wrapper.emitted('update:columnWidths')?.[0]).toEqual([{ name: 136 }]) await handle.trigger('keydown', { key: 'ArrowLeft' }) expect(wrapper.emitted('update:columnWidths')?.[1]).toEqual([{ name: 104 }]) await wrapper.setProps({ columnWidths: { name: 50 } }) await handle.trigger('keydown', { key: 'ArrowLeft' }) expect(wrapper.emitted('update:columnWidths')?.[2]).toEqual([{ name: 48 }]) }) }) describe('sortable header accessibility', () => { const sortableColumn = [{ key: 'name', label: 'Name', sortable: true }] it('should render sortable headers as buttons and plain headers without one', () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: [...sortableColumn, { key: 'age', label: 'Age' }], }, }) const headers = wrapper.findAll('.mint-dataframe__th') expect(headers[0].get('.mint-dataframe__th-button').element.tagName).toBe('BUTTON') expect(headers[1].get('.mint-dataframe__th-button').element.tagName).toBe('DIV') }) it('should reflect the sort direction with aria-sort', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: [...sortableColumn, { key: 'age', label: 'Age' }], }, }) const nameHeader = () => wrapper.findAll('.mint-dataframe__th')[0] expect(nameHeader().attributes('aria-sort')).toBe('none') expect(wrapper.findAll('.mint-dataframe__th')[1].attributes('aria-sort')).toBeUndefined() await nameHeader().get('.mint-dataframe__th-button').trigger('click') expect(nameHeader().attributes('aria-sort')).toBe('ascending') await nameHeader().get('.mint-dataframe__th-button').trigger('click') expect(nameHeader().attributes('aria-sort')).toBe('descending') await nameHeader().get('.mint-dataframe__th-button').trigger('click') expect(nameHeader().attributes('aria-sort')).toBe('none') }) it('should sort from the keyboard with Enter and Space', async () => { const wrapper = mount(DataFrame, { props: { data: mockData, columns: sortableColumn }, }) const button = wrapper.get('.mint-dataframe__th-button') await button.trigger('keydown', { key: 'Enter' }) expect(wrapper.emitted('update:sort')?.[0]?.[0]).toEqual({ key: 'name', direction: 'asc' }) await button.trigger('keydown', { key: ' ' }) expect(wrapper.emitted('update:sort')?.[1]?.[0]).toEqual({ key: 'name', direction: 'desc' }) }) }) describe('table density default', () => { afterEach(() => { setActivePinia(undefined) }) it('falls back to md when no settings store is installed', () => { setActivePinia(undefined) const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, }) expect(wrapper.find('.mint-dataframe__td--md').exists()).toBe(true) }) it('follows the tableDensity setting when size is not passed', () => { setActivePinia(createPinia()) useSettingsStore().tableDensity = 'compact' const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns }, }) expect(wrapper.find('.mint-dataframe__td--sm').exists()).toBe(true) expect(wrapper.find('.mint-dataframe__td--md').exists()).toBe(false) }) it('lets an explicit size prop win over the density setting', () => { setActivePinia(createPinia()) useSettingsStore().tableDensity = 'compact' const wrapper = mount(DataFrame, { props: { data: mockData, columns: mockColumns, size: 'lg' }, }) expect(wrapper.find('.mint-dataframe__td--lg').exists()).toBe(true) }) }) describe('column unit, type and precision (2a)', () => { const measureData = [{ id: 1, sample: 'A-01', conc: 12.5, rt: 2.4 }] const measureColumns: DataFrameColumn[] = [ { key: 'sample', label: 'Sample' }, { key: 'conc', label: 'Conc', unit: 'µM', type: 'number' }, { key: 'rt', label: 'RT', unit: 'min', type: 'number', precision: 2 }, ] const mountMeasures = () => mount(DataFrame, { props: { data: measureData, columns: measureColumns } }) it('should render the unit in its own element when a column declares a unit', () => { const header = mountMeasures().findAll('.mint-dataframe__th')[1] expect(header.find('.mint-dataframe__th-unit').text()).toBe('µM') }) it('should keep the label text free of the unit when a column declares a unit', () => { const header = mountMeasures().findAll('.mint-dataframe__th')[1] expect(header.find('.mint-dataframe__th-label').text()).toBe('Conc') }) it('should render no unit element when a column has no unit', () => { const header = mountMeasures().findAll('.mint-dataframe__th')[0] expect(header.find('.mint-dataframe__th-unit').exists()).toBe(false) }) it('should mark cells as numeric when the column type is number', () => { const cell = mountMeasures().findAll('.mint-dataframe__td')[1] expect(cell.classes()).toContain('mint-dataframe__td--number') }) it('should right-align cells when the column type is number', () => { const cell = mountMeasures().findAll('.mint-dataframe__td')[1] expect(cell.classes()).toContain('mint-dataframe__td--align-right') }) it('should right-align the header when the column type is number', () => { const header = mountMeasures().findAll('.mint-dataframe__th')[1] expect(header.classes()).toContain('mint-dataframe__th--align-right') }) it('should keep an explicit align when the column type is number', () => { const wrapper = mount(DataFrame, { props: { data: measureData, columns: [{ key: 'conc', label: 'Conc', type: 'number', align: 'center' }] }, }) expect(wrapper.find('.mint-dataframe__td').classes()).toContain('mint-dataframe__td--align-center') }) it('should not mark cells as numeric when the column has no type', () => { const cell = mountMeasures().findAll('.mint-dataframe__td')[0] expect(cell.classes()).not.toContain('mint-dataframe__td--number') }) it('should format numbers to the given precision when a column declares precision', () => { const cell = mountMeasures().findAll('.mint-dataframe__td')[2] expect(cell.text()).toBe('2.40') }) it('should leave numbers unformatted when a column declares no precision', () => { const cell = mountMeasures().findAll('.mint-dataframe__td')[1] expect(cell.text()).toBe('12.5') }) it('should let the formatter win when a column declares both formatter and precision', () => { const wrapper = mount(DataFrame, { props: { data: measureData, columns: [{ key: 'rt', label: 'RT', precision: 2, formatter: (v: unknown) => `~${String(v)}` }], }, }) expect(wrapper.find('.mint-dataframe__td').text()).toBe('~2.4') }) it('should leave non-numeric values untouched when a column declares precision', () => { const wrapper = mount(DataFrame, { props: { data: [{ rt: 'n/a' }], columns: [{ key: 'rt', label: 'RT', type: 'number', precision: 2 }] }, }) expect(wrapper.find('.mint-dataframe__td').text()).toBe('n/a') }) }) })