UNPKG

buefy

Version:

Lightweight UI components for Vue.js (v3) based on Bulma

623 lines (566 loc) 22.1 kB
import '@testing-library/jest-dom/vitest' import { toRaw } from 'vue' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { shallowMount } from '@vue/test-utils' import type { DOMWrapper, VueWrapper } from '@vue/test-utils' import BInput from '@components/input/Input.vue' import BTable from '@components/table/Table.vue' import BTablePagination from '@components/table/TablePagination.vue' import type { ITableColumn } from '@components/table/types' describe('BTable', () => { let wrapper: VueWrapper<InstanceType<typeof BTable>> beforeEach(() => { wrapper = shallowMount(BTable) }) const tableCols = shallowMount(BTable, { props: { columns: [ { label: 'default', width: '100px' }, { label: 'pecent', width: '50%' }, { label: 'fixed_num', width: 100 }, { label: 'fixed_str', width: '100' } ] } }) it('is called', () => { expect(wrapper.vm).toBeTruthy() expect(wrapper.vm.$options.name).toBe('BTable') expect(tableCols.vm).toBeTruthy() expect(tableCols.vm.$options.name).toBe('BTable') }) it('has the filter row visible when searchable', async () => { await wrapper.setProps({ columns: [ { field: 'id', label: 'ID', width: '40', numeric: true } ] }) // Don't show if no searchable column expect(wrapper.vm.hasSearchablenewColumns).toBe(false) // Show if one or more searchable column await wrapper.setProps({ columns: [ { field: 'id', label: 'ID', width: '40', numeric: true, searchable: true } ] }) expect(wrapper.vm.hasSearchablenewColumns).toBe(true) }) it('render correctly', () => { expect(wrapper.html()).toMatchSnapshot() }) it('holds columns', () => { const headers = tableCols.findAll('th') expect(headers.length).toBeGreaterThanOrEqual(4) const cols = headers.filter((th) => { const div = th.find('div') return div.classes('th-wrap') }) expect(cols.length).toBe(4) expect(cols[0].attributes('style')).toBe('width: 100px;') expect(cols[1].attributes('style')).toBe('width: 50%;') expect(cols[2].attributes('style')).toBe('width: 100px;') expect(cols[3].attributes('style')).toBe('width: 100px;') }) describe('Selectable', () => { const data = [ { id: 1, name: 'Jesse' }, { id: 2, name: 'John' }, { id: 3, name: 'Tina' }, { id: 4, name: 'Anne' }, { id: 5, name: 'Clarence' } ] beforeEach(() => { wrapper = shallowMount(BTable, { props: { columns: [ { label: 'ID', field: 'id' }, { label: 'Name', field: 'name' } ], data } }) }) it('unselected( no column-row-key )', () => { expect(wrapper.findAll('tbody tr.is-selected')).toHaveLength(0) }) it('unselected( column-row-key )', async () => { await wrapper.setProps({ customRowKey: 'id' }) expect(wrapper.findAll('tbody tr.is-selected')).toHaveLength(0) }) it('compare by instance itself', async () => { await wrapper.setProps({ selected: data[0] }) const rows = wrapper.findAll('tbody tr') expect(rows[0].classes()).toContain('is-selected') }) it('target data and key match', async () => { await wrapper.setProps({ selected: data[1], customRowKey: 'id' }) const rows = wrapper.findAll('tbody tr') expect(rows[1].classes()).toContain('is-selected') }) it('clear data', async () => { await wrapper.setProps({ selected: data[0], customRowKey: 'id' }) const rows = wrapper.findAll('tbody tr') expect(rows[0].classes()).toContain('is-selected') await wrapper.setProps({ selected: undefined }) expect(wrapper.findAll('tbody tr.is-selected')).toHaveLength(0) }) }) describe('Searchable', () => { const data = [ { id: 1, name: 'Jesse' }, { id: 2, name: 'João' }, { id: 3, name: 'Tina' }, { id: 4, name: 'Anne' }, { id: 5, name: 'Clarence' } ] let headRows: DOMWrapper<Element>[] let bodyRows: DOMWrapper<Element>[] let searchInput: VueWrapper<InstanceType<typeof BInput>> beforeEach(() => { wrapper = shallowMount(BTable, { props: { columns: [ { label: 'ID', field: 'id', numeric: true }, { label: 'Name', field: 'name', searchable: true } ], data } }) headRows = wrapper.findAll('thead tr') bodyRows = wrapper.findAll('tbody tr') searchInput = wrapper.findComponent(BInput) }) it('displays filter row when at least one column is searchable', () => { expect(headRows).toHaveLength(2) }) it('displays filter input only on searchable columns', () => { const filterCells = headRows[1].findAll('.th-wrap') expect(filterCells[0].element).toBeEmptyDOMElement() // ID column is not searchable expect( filterCells[1].findComponent(BInput).exists() ).toBe(true) // Name column is searchable }) it('displays all data', () => { expect(bodyRows).toHaveLength(5) }) it('displays filtered data when searching', async () => { searchInput.vm.$emit('update:modelValue', 'J') await searchInput.vm.$nextTick() // makes sure the DOM is updated bodyRows = wrapper.findAll('tbody tr') expect(bodyRows).toHaveLength(2) // Jesse and João }) it('displays filtered data when searching by name without accent', async () => { searchInput.vm.$emit('update:modelValue', 'Joao') await searchInput.vm.$nextTick() // makes sure the DOM is updated bodyRows = wrapper.findAll('tbody tr') expect(bodyRows).toHaveLength(1) // João }) it('displays filtered data when searching by name with accent', async () => { searchInput.vm.$emit('update:modelValue', 'João') await searchInput.vm.$nextTick() // makes sure the DOM is updated bodyRows = wrapper.findAll('tbody tr') expect(bodyRows).toHaveLength(1) // João }) it('displays filtered data when searching and updating data', async () => { searchInput.vm.$emit('update:modelValue', 'J') await wrapper.setProps({ data: [ ...data, { id: 6, name: 'Justin' } ] }) bodyRows = wrapper.findAll('tbody tr') expect(bodyRows).toHaveLength(3) // Jesse, João and Justin }) it('debounce search filtering when debounce-search is defined', async () => { vi.useFakeTimers() await wrapper.setProps({ debounceSearch: 1000 }) for (let i = 0; i < 10; i++) { searchInput.vm.$emit('update:modelValue', 'J'.repeat(10 - i)) vi.advanceTimersByTime(500) await wrapper.vm.$nextTick() // makes sure the DOM is updated bodyRows = wrapper.findAll('tbody tr') expect(bodyRows).toHaveLength(5) // No filtering yet } vi.advanceTimersByTime(1000) await wrapper.vm.$nextTick() // makes sure the DOM is updated bodyRows = wrapper.findAll('tbody tr') expect(bodyRows).toHaveLength(2) // Filtering after debounce vi.useRealTimers() }) }) describe('Sortable', () => { let wrapper: VueWrapper<InstanceType<typeof BTable>> const data = [ { id: 1, name: 'Jesse' }, { id: 2, name: 'João' }, { id: 3, name: 'Tina' }, { id: 4, name: 'Anne' }, { id: 5, name: 'Clarence' } ] const columnsData = [ { field: 'id', label: 'ID', numeric: true, sortable: true }, { field: 'name', label: 'Name', sortable: true } ] let columns: ITableColumn[] beforeEach(() => { wrapper = shallowMount(BTable, { props: { columns: columnsData, data } }) // columnsData is transformed into newColumns with new objects columns = wrapper.vm.newColumns }) it('should be able to sort by ID', () => { const sorted = [...data] wrapper.vm.sort(columns[0]) expect(toRaw(wrapper.vm.currentSortColumn)).toBe(toRaw(columns[0])) expect(wrapper.vm.isAsc).toBe(true) expect(wrapper.vm.visibleData).toEqual(sorted) // toggles wrapper.vm.sort(columns[0]) expect(wrapper.vm.isAsc).toBe(false) expect(wrapper.vm.visibleData).toEqual(sorted.reverse()) }) it('should be able to sort by Name', () => { const sorted = [ data[3], data[4], data[0], data[1], data[2] ] wrapper.vm.sort(columns[1]) expect(toRaw(wrapper.vm.currentSortColumn)).toBe(toRaw(columns[1])) expect(wrapper.vm.isAsc).toBe(true) expect(wrapper.vm.visibleData).toEqual(sorted) // toggles wrapper.vm.sort(columns[1]) expect(wrapper.vm.isAsc).toBe(false) expect(wrapper.vm.visibleData).toEqual(sorted.reverse()) }) }) describe('Multi-sortable', () => { let wrapper: VueWrapper<InstanceType<typeof BTable>> const data = [ { id: 1, name: 'Jesse', age: 23 }, { id: 2, name: 'João', age: 22 }, { id: 3, name: 'Tina', age: 22 }, { id: 4, name: 'Anne', age: 23 }, { id: 5, name: 'Clarence', age: 22 } ] const columnsData = [ { field: 'id', label: 'ID' }, { field: 'name', label: 'Name', sortable: true }, { field: 'age', label: 'Age', numeric: true, sortable: true } ] let columns: ITableColumn[] beforeEach(() => { wrapper = shallowMount(BTable, { props: { columns: columnsData, data, sortMultiple: true } }) // columnsData is transformed into newColumns with new objects columns = wrapper.vm.newColumns }) it('should be able to sort by Age then Name', () => { wrapper.vm.sort(columns[2]) wrapper.vm.sort(columns[1]) expect(wrapper.vm.sortMultipleDataLocal).toEqual([ { field: 'age', order: 'asc' }, { field: 'name', order: 'asc' } ]) expect(wrapper.vm.visibleData).toEqual([ data[4], data[1], data[2], data[3], data[0] ]) // toggles age wrapper.vm.sort(columns[2]) expect(wrapper.vm.sortMultipleDataLocal).toEqual([ { field: 'age', order: 'desc' }, { field: 'name', order: 'asc' } ]) expect(wrapper.vm.visibleData).toEqual([ data[3], data[0], data[4], data[1], data[2] ]) // toggles name wrapper.vm.sort(columns[1]) expect(wrapper.vm.sortMultipleDataLocal).toEqual([ { field: 'age', order: 'desc' }, { field: 'name', order: 'desc' } ]) expect(wrapper.vm.visibleData).toEqual([ data[0], data[3], data[2], data[1], data[4] ]) }) }) describe('Sortable with custom sort', () => { let wrapper: VueWrapper<InstanceType<typeof BTable>> const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] const data = weekdays.map((day, i) => ({ id: i + 1, day })) const customSort = vi.fn((a, b, isAsc) => { const ord = weekdays.indexOf(a.day) - weekdays.indexOf(b.day) return isAsc ? ord : -ord }) const columnsData = [ { field: 'id', label: 'ID', numeric: true }, { field: 'day', label: 'Day', sortable: true, customSort } ] let columns: ITableColumn[] beforeEach(() => { wrapper = shallowMount(BTable, { props: { columns: columnsData, data } }) // columnsData is transformed into newColumns with new objects columns = wrapper.vm.newColumns }) afterEach(() => { customSort.mockClear() }) it('should be able to sort by Day with custom sort', async () => { const sorted = [...data] wrapper.vm.sort(columns[1]) expect(toRaw(wrapper.vm.currentSortColumn)).toBe(toRaw(columns[1])) expect(wrapper.vm.isAsc).toBe(true) expect(wrapper.vm.visibleData).toEqual(sorted) expect(customSort).toHaveBeenCalled() // toggles wrapper.vm.sort(columns[1]) expect(wrapper.vm.isAsc).toBe(false) expect(wrapper.vm.visibleData).toEqual(sorted.reverse()) }) }) describe('Multi-sortable with custom sort', () => { let wrapper: VueWrapper<InstanceType<typeof BTable>> const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] const data = [ { id: 1, day: 'Sun', fee: 15 }, { id: 2, day: 'Mon', fee: 12 }, { id: 3, day: 'Tue', fee: 12 }, { id: 4, day: 'Wed', fee: 12 }, { id: 5, day: 'Thu', fee: 12 }, { id: 6, day: 'Fri', fee: 12 }, { id: 7, day: 'Sat', fee: 15 } ] const dayCustomSort = vi.fn((a, b, isAsc) => { const ord = weekdays.indexOf(a.day) - weekdays.indexOf(b.day) return isAsc ? ord : -ord }) const feeCustomSort = vi.fn((a, b, isAsc) => { const ord = a.fee - b.fee return isAsc ? -ord : ord }) const columnsData = [ { field: 'id', label: 'ID', numeric: true }, { field: 'day', label: 'Day', sortable: true, customSort: dayCustomSort }, { field: 'fee', label: 'Fee', sortable: true, customSort: feeCustomSort } ] let columns: ITableColumn[] beforeEach(() => { wrapper = shallowMount(BTable, { props: { columns: columnsData, data, sortMultiple: true } }) columns = wrapper.vm.newColumns }) afterEach(() => { dayCustomSort.mockClear() feeCustomSort.mockClear() }) it('should be able to sort by Fee then Day with custom sort', () => { wrapper.vm.sort(columns[2]) wrapper.vm.sort(columns[1]) expect(wrapper.vm.sortMultipleDataLocal).toEqual([ { field: 'fee', order: 'asc', customSort: feeCustomSort }, { field: 'day', order: 'asc', customSort: dayCustomSort } ]) expect(wrapper.vm.visibleData).toEqual([ data[0], data[6], data[1], data[2], data[3], data[4], data[5] ]) expect(feeCustomSort).toHaveBeenCalled() expect(dayCustomSort).toHaveBeenCalled() // toggles fee wrapper.vm.sort(columns[2]) expect(wrapper.vm.sortMultipleDataLocal).toEqual([ { field: 'fee', order: 'desc', customSort: feeCustomSort }, { field: 'day', order: 'asc', customSort: dayCustomSort } ]) expect(wrapper.vm.visibleData).toEqual([ data[1], data[2], data[3], data[4], data[5], data[0], data[6] ]) // toggles day wrapper.vm.sort(columns[1]) expect(wrapper.vm.sortMultipleDataLocal).toEqual([ { field: 'fee', order: 'desc', customSort: feeCustomSort }, { field: 'day', order: 'desc', customSort: dayCustomSort } ]) expect(wrapper.vm.visibleData).toEqual([ data[5], data[4], data[3], data[2], data[1], data[6], data[0] ]) }) it('should be able to remove column from sort (Fee+Day → Day)', () => { wrapper.vm.sort(columns[2]) wrapper.vm.sort(columns[1]) wrapper.vm.sort(columns[1]) // day → descending order expect(wrapper.vm.sortMultipleDataLocal).toEqual([ { field: 'fee', order: 'asc', customSort: feeCustomSort }, { field: 'day', order: 'desc', customSort: dayCustomSort } ]) expect(wrapper.vm.visibleData).toEqual([ data[6], data[0], data[5], data[4], data[3], data[2], data[1] ]) // removes fee wrapper.vm.removeSortingPriority(columns[2]) expect(wrapper.vm.sortMultipleDataLocal).toEqual([ { field: 'day', order: 'desc', customSort: dayCustomSort } ]) expect(wrapper.vm.visibleData).toEqual([ data[6], data[5], data[4], data[3], data[2], data[1], data[0] ]) }) }) describe('with fallthrough attributes', () => { const data = [ { id: 1, name: 'Jesse' }, { id: 2, name: 'John' }, { id: 3, name: 'Tina' }, { id: 4, name: 'Anne' }, { id: 5, name: 'Clarence' } ] const columns = [ { label: 'ID', field: 'id' }, { label: 'Name', field: 'name' } ] const attrs = { class: 'fallthrough-class', style: 'font-size: 2rem;', id: 'fallthrough-id' } it('should apply class, style, and id to the root <div> element if compatFallthrough is true (default)', () => { const wrapper = shallowMount(BTable, { attrs, props: { paginated: true, paginationPosition: 'both', columns, data } }) const root = wrapper.find('div.b-table') expect(root.classes(attrs.class)).toBe(true) expect(root.attributes('style')).toBe(attrs.style) expect(root.attributes('id')).toBe(attrs.id) const paginations = wrapper.findAllComponents(BTablePagination) // top expect(paginations[0].classes(attrs.class)).toBe(false) expect(paginations[0].attributes('style')).toBeUndefined() expect(paginations[0].attributes('id')).toBeUndefined() // bottom expect(paginations[1].classes(attrs.class)).toBe(false) expect(paginations[1].attributes('style')).toBeUndefined() expect(paginations[1].attributes('id')).toBeUndefined() }) it('should apply class, style, and id to the underlying <b-table-pagination> components if compatFallthrough is false', () => { const wrapper = shallowMount(BTable, { attrs, props: { compatFallthrough: false, paginated: true, paginationPosition: 'both', columns, data } }) const root = wrapper.find('div.b-table') expect(root.classes(attrs.class)).toBe(false) expect(root.attributes('style')).toBeUndefined() expect(root.attributes('id')).toBeUndefined() const paginations = wrapper.findAllComponents(BTablePagination) // top expect(paginations[0].classes(attrs.class)).toBe(true) expect(paginations[0].attributes('style')).toBe(attrs.style) expect(paginations[0].attributes('id')).toBe(attrs.id) // bottom expect(paginations[1].classes(attrs.class)).toBe(true) expect(paginations[1].attributes('style')).toBe(attrs.style) expect(paginations[1].attributes('id')).toBe(attrs.id) }) }) })