UNPKG

vuetify

Version:

Vue Material Component Framework

1,040 lines (906 loc) 24.9 kB
import VDataTable from '../VDataTable' import { mount, Wrapper, MountOptions, } from '@vue/test-utils' import { Breakpoint } from '../../../services/breakpoint' import ripple from '../../../directives/ripple/index' import Vue from 'vue' import { Lang } from '../../../services/lang' import { preset } from '../../../presets/default' import { resizeWindow } from '../../../../test' Vue.prototype.$vuetify = { rtl: false, lang: new Lang(preset), } Vue.directive('ripple', ripple) const testHeaders = [ { text: 'Dessert (100g serving)', align: 'left', sortable: false, value: 'name', }, { text: 'Calories', value: 'calories' }, { text: 'Fat (g)', value: 'fat' }, { text: 'Carbs (g)', value: 'carbs' }, { text: 'Protein (g)', value: 'protein' }, { text: 'Iron (%)', value: 'iron' }, ] const testItems = [ { name: 'Frozen Yogurt', calories: 159, fat: 6.0, carbs: 24, protein: 4.0, iron: '1%', class: 'test', }, { name: 'Ice cream sandwich', calories: 237, fat: 9.0, carbs: 37, protein: 4.3, iron: '1%', class: ['test', 'second'], }, { name: 'Eclair', calories: 262, fat: 16.0, carbs: 23, protein: 6.0, iron: '7%', class: { test: true, second: false }, }, { name: 'Cupcake', calories: 305, fat: 3.7, carbs: 67, protein: 4.3, iron: '8%', }, { name: 'Gingerbread', calories: 356, fat: 16.0, carbs: 49, protein: 3.9, iron: '16%', }, { name: 'Jelly bean', calories: 375, fat: 0.0, carbs: 94, protein: 0.0, iron: '0%', }, { name: 'Lollipop', calories: 392, fat: 0.2, carbs: 98, protein: 0, iron: '2%', }, { name: 'Honeycomb', calories: 408, fat: 3.2, carbs: 87, protein: 6.5, iron: '45%', }, { name: 'Donut', calories: 452, fat: 25.0, carbs: 51, protein: 4.9, iron: '22%', }, { name: 'KitKat', calories: 518, fat: 26.0, carbs: 65, protein: 7, iron: '6%', }, ] /* eslint-disable max-statements */ describe('VDataTable.ts', () => { type Instance = InstanceType<typeof VDataTable> let mountFunction: (options?: MountOptions<Instance>) => Wrapper<Instance> beforeEach(() => { document.body.setAttribute('data-app', 'true') mountFunction = (options?: MountOptions<Instance>) => { return mount(VDataTable, { mocks: { $vuetify: { breakpoint: new Breakpoint(preset), lang: new Lang(preset), theme: { dark: false, }, }, }, sync: false, ...options, }) } return resizeWindow(0) }) it('should render', () => { const wrapper = mountFunction() expect(wrapper.html()).toMatchSnapshot() }) it('should render with data', () => { const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, itemsPerPage: 5, }, }) expect(wrapper.html()).toMatchSnapshot() }) it('should render with body slot', () => { const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, itemsPerPage: 5, }, scopedSlots: { body (props) { return this.$createElement('div', [props.items.length]) }, }, }) expect(wrapper.html()).toMatchSnapshot() }) it.skip('should render virtual table', () => { const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, itemsPerPage: 5, virtualRows: true, }, }) expect(wrapper.html()).toMatchSnapshot() }) it('should render with showExpand', async () => { const expand = jest.fn() const wrapper = mountFunction({ propsData: { headers: testHeaders, itemKey: 'name', items: testItems, itemsPerPage: 5, showExpand: true, }, listeners: { 'update:expanded': expand, }, }) expect(wrapper.html()).toMatchSnapshot() const expandIcon = wrapper.findAll('.v-data-table__expand-icon').at(0) expandIcon.trigger('click') await wrapper.vm.$nextTick() expect(expand).toHaveBeenCalledWith(testItems.slice(0, 1)) expect(wrapper.html()).toMatchSnapshot() }) it('should render with showSelect', () => { const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, itemsPerPage: 5, showSelect: true, }, }) expect(wrapper.html()).toMatchSnapshot() }) it('should render with item.expanded scoped slot', async () => { const vm = new Vue() const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, itemsPerPage: 5, expanded: testItems, }, scopedSlots: { 'expanded-item': props => vm.$createElement('div', ['expanded']), }, }) await wrapper.vm.$nextTick() expect(wrapper.html()).toMatchSnapshot() }) it('should render with group.summary scoped slot', () => { const vm = new Vue() const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, itemsPerPage: 5, groupBy: 'calories', }, scopedSlots: { 'group.summary': props => vm.$createElement('div', ['summary']), }, }) expect(wrapper.html()).toMatchSnapshot() }) it('should render with item scoped slot', () => { const vm = new Vue() const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, itemsPerPage: 5, }, scopedSlots: { item: props => vm.$createElement('div', [JSON.stringify(props)]), }, }) expect(wrapper.html()).toMatchSnapshot() }) it('should render with grouped rows', () => { const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, itemsPerPage: 5, groupBy: ['protein'], }, }) expect(wrapper.html()).toMatchSnapshot() }) it('should render with group scoped slot', () => { const vm = new Vue() const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, itemsPerPage: 5, groupBy: ['protein'], }, scopedSlots: { group: props => vm.$createElement('div', [JSON.stringify(props)]), }, }) expect(wrapper.html()).toMatchSnapshot() }) it('should render loading state', () => { const wrapper = mountFunction({ propsData: { loading: true, }, }) expect(wrapper.html()).toMatchSnapshot() const wrapper2 = mountFunction({ propsData: { headers: testHeaders, loading: true, }, slots: { progress: '<div class="progress">50%</div>', }, }) expect(wrapper2.html()).toMatchSnapshot() }) it.each([ 'click', 'contextmenu', 'dblclick', ])('should emit event when %sing on internally created row', async event => { const eventToEmit = event + ':row' const fn = jest.fn() const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, }, listeners: { [eventToEmit]: fn, }, }) wrapper.find('tbody tr').trigger(event) await wrapper.vm.$nextTick() expect(fn).toHaveBeenCalled() }) // https://github.com/vuetifyjs/vuetify/issues/8254 it('should pass kebab-case footer props correctly', () => { const wrapper = mountFunction({ propsData: { headers: [], items: [], footerProps: { 'items-per-page-text': 'Foo:', }, }, }) expect(wrapper.html()).toMatchSnapshot() }) // https://github.com/vuetifyjs/vuetify/issues/8266 it('should use options prop for initial values', () => { const fn = jest.fn() const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, options: { page: 2, itemsPerPage: 5, }, }, listeners: { 'update:options': fn, }, }) expect(fn).toHaveBeenCalledWith(expect.objectContaining({ page: 2, })) }) it('should render footer.page-text slot content', () => { const wrapper = mountFunction({ propsData: { headers: [], items: [{}], }, scopedSlots: { 'footer.page-text' ({ pageStart, pageStop }) { return this.$createElement('div', [`foo ${pageStart} bar ${pageStop}`]) }, }, }) expect(wrapper.html()).toMatchSnapshot() }) // https://github.com/vuetifyjs/vuetify/issues/8359 it('should not limit page to current item count when using server-items-length', async () => { const wrapper = mountFunction({ propsData: { headers: testHeaders, items: [], page: 2, itemsPerPage: 5, serverItemsLength: 0, }, }) expect(wrapper.html()).toMatchSnapshot() wrapper.setProps({ items: testItems.slice(5), serverItemsLength: 20, }) await wrapper.vm.$nextTick() expect(wrapper.html()).toMatchSnapshot() }) it('should not search column with filterable set to false', async () => { const wrapper = mountFunction({ propsData: { items: testItems, headers: [ { text: 'Dessert (100g serving)', align: 'left', filterable: false, value: 'name', }, { text: 'Calories', value: 'calories' }, { text: 'Fat (g)', value: 'fat' }, { text: 'Carbs (g)', value: 'carbs' }, { text: 'Protein (g)', value: 'protein' }, { text: 'Iron (%)', value: 'iron' }, ], }, }) expect(wrapper.html()).toMatchSnapshot() wrapper.setProps({ search: 'cup', }) await wrapper.vm.$nextTick() expect(wrapper.html()).toMatchSnapshot() }) it('should not search column with filterable set to false and has filter function', async () => { const wrapper = mountFunction({ propsData: { items: testItems, headers: [ { text: 'Dessert (100g serving)', align: 'left', value: 'name', }, { text: 'Calories', value: 'calories', filter: v => v > 400 }, { text: 'Fat (g)', value: 'fat' }, { text: 'Carbs (g)', value: 'carbs' }, { text: 'Protein (g)', value: 'protein' }, { text: 'Iron (%)', value: 'iron' }, ], }, }) expect(wrapper.html()).toMatchSnapshot() wrapper.setProps({ headers: [ { text: 'Dessert (100g serving)', align: 'left', value: 'name', }, { text: 'Calories', value: 'calories', filter: v => v > 400, filterable: false }, { text: 'Fat (g)', value: 'fat' }, { text: 'Carbs (g)', value: 'carbs' }, { text: 'Protein (g)', value: 'protein' }, { text: 'Iron (%)', value: 'iron' }, ], }) await wrapper.vm.$nextTick() expect(wrapper.html()).toMatchSnapshot() }) // https://github.com/vuetifyjs/vuetify/issues/8359 it('should limit page to current page count if not using server-items-length', async () => { const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, page: 3, itemsPerPage: 5, }, }) expect(wrapper.html()).toMatchSnapshot() }) // https://github.com/vuetifyjs/vuetify/issues/8184 it('should default to first option in itemsPerPageOptions if it does not include itemsPerPage', async () => { const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, footerProps: { itemsPerPageOptions: [6, 7], }, }, }) expect(wrapper.html()).toMatchSnapshot() }) // https://github.com/vuetifyjs/vuetify/issues/8817 it('should handle object when checking if it should default to first option in itemsPerPageOptions', async () => { const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, itemsPerPage: -1, footerProps: { itemsPerPageOptions: [6, { text: 'All', value: -1 }], }, }, }) expect(wrapper.html()).toMatchSnapshot() }) // https://github.com/vuetifyjs/vuetify/issues/9599 it('should not immediately emit items-per-page', async () => { const itemsPerPage = jest.fn() const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, footerProps: { itemsPerPageOptions: [6, 7], }, }, listeners: { 'update:itemsPerPage': itemsPerPage, }, }) expect(itemsPerPage).not.toHaveBeenCalled() }) // https://github.com/vuetifyjs/vuetify/issues/9010 it('should change page if item count decreases below page start', async () => { const page = jest.fn() const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems.slice(0, 4), itemsPerPage: 2, footerProps: { itemsPerPageOptions: [2], }, page: 2, }, listeners: { 'update:page': page, }, }) expect(wrapper.html()).toMatchSnapshot() wrapper.setProps({ items: testItems.slice(0, 2) }) await wrapper.vm.$nextTick() expect(page).toHaveBeenCalledWith(1) }) // https://github.com/vuetifyjs/vuetify/issues/8477 it('should emit two item-selected events when using single-select prop and selecting new item', async () => { const itemSelected = jest.fn() const wrapper = mountFunction({ propsData: { headers: testHeaders, itemKey: 'name', items: testItems.slice(0, 2), value: [testItems[0]], showSelect: true, singleSelect: true, }, listeners: { 'item-selected': itemSelected, }, }) const checkbox = wrapper.findAll('.v-data-table__checkbox').at(1) checkbox.trigger('click') await wrapper.vm.$nextTick() expect(itemSelected).toHaveBeenCalledTimes(2) expect(itemSelected).toHaveBeenCalledWith({ item: testItems[0], value: false }) expect(itemSelected).toHaveBeenCalledWith({ item: testItems[1], value: true }) }) // https://github.com/vuetifyjs/vuetify/issues/8915 it('should not select item that is not selectable', async () => { const items = [ { ...testItems[0], isSelectable: false }, { ...testItems[1] }, ] const input = jest.fn() const wrapper = mountFunction({ propsData: { headers: testHeaders, items, showSelect: true, }, listeners: { input, }, }) expect(wrapper.html()).toMatchSnapshot() const selectAll = wrapper.findAll('.v-simple-checkbox').at(0) selectAll.trigger('click') await wrapper.vm.$nextTick() expect(input).toHaveBeenNthCalledWith(1, [testItems[1]]) const single = wrapper.findAll('.v-simple-checkbox').at(1) single.trigger('click') await wrapper.vm.$nextTick() expect(input.mock.calls).toHaveLength(1) }) // https://github.com/vuetifyjs/vuetify/issues/8915 it('should toggle all selectable items', async () => { const items = [ { ...testItems[0], isSelectable: false }, { ...testItems[1] }, ] const input = jest.fn() const wrapper = mountFunction({ propsData: { headers: testHeaders, items, showSelect: true, }, listeners: { input, }, }) const selectAll = wrapper.findAll('.v-simple-checkbox').at(0) selectAll.trigger('click') await wrapper.vm.$nextTick() expect(input).toHaveBeenNthCalledWith(1, [testItems[1]]) selectAll.trigger('click') await wrapper.vm.$nextTick() expect(input).toHaveBeenNthCalledWith(2, []) }) // https://github.com/vuetifyjs/vuetify/issues/10392 it('should search group-by column', async () => { const headers = [ { text: 'Name', value: 'name', }, { text: 'ID', value: 'id', }, ] const items = [ { name: 'Assistance', id: 1, }, { name: 'Candidat', id: 2, }, ] const wrapper = mountFunction({ propsData: { headers, items, itemKey: 'id', groupBy: 'name', }, }) expect(wrapper.html()).toMatchSnapshot() wrapper.setProps({ search: 'candidat' }) await wrapper.vm.$nextTick() expect(wrapper.html()).toMatchSnapshot() }) // https://github.com/vuetifyjs/vuetify/issues/10289 it('should render item slot when using group-by function', async () => { const wrapper = mountFunction({ propsData: { headers: testHeaders, itemKey: 'name', items: testItems.slice(0, 2), groupBy: 'name', }, scopedSlots: { item () { return this.$createElement('div', ['scoped']) }, }, }) expect(wrapper.html()).toMatchSnapshot() }) // https://github.com/vuetifyjs/vuetify/issues/10392 it('should emit pagination event when filtering', async () => { const headers = [ { text: 'Name', value: 'name', }, { text: 'ID', value: 'id', }, ] const items = [ { name: 'Assistance', id: 1, }, { name: 'Candidat', id: 2, }, ] const pagination = jest.fn() const wrapper = mountFunction({ propsData: { headers, items, itemKey: 'id', }, listeners: { pagination, }, }) expect(pagination).toHaveBeenLastCalledWith({ itemsLength: 2, itemsPerPage: 10, page: 1, pageCount: 1, pageStart: 0, pageStop: 2, }) wrapper.setProps({ search: 'candidat' }) await wrapper.vm.$nextTick() expect(pagination).toHaveBeenLastCalledWith({ itemsLength: 1, itemsPerPage: 10, page: 1, pageCount: 1, pageStart: 0, pageStop: 1, }) expect(pagination).toHaveBeenCalledTimes(2) }) // https://github.com/vuetifyjs/vuetify/issues/10715 // NOTE: This test currently succeeds regardless of fix // It seems like the test environment does not double // fire the events in the same way the browser does it('should not emit too many pagination events', async () => { const headers = [ { text: 'Name', value: 'name', }, { text: 'ID', value: 'id', }, ] const items = [ { name: 'Assistance', id: 1, }, { name: 'Candidat', id: 2, }, ] const wrapper = mountFunction({ propsData: { headers, itemKey: 'id', serverItemsLength: 0, }, }) wrapper.setProps({ items, serverItemsLength: items.length }) await wrapper.vm.$nextTick() expect(wrapper.emitted().pagination).toHaveLength(2) }) // https://github.com/vuetifyjs/vuetify/issues/4975 it('should show correct aria-labels when sorting', async () => { const wrapper = mountFunction({ propsData: { headers: testHeaders, itemKey: 'name', items: testItems.slice(0, 5), sortBy: 'calories', }, }) wrapper.setProps({ sortDesc: true }) await wrapper.vm.$nextTick() expect(wrapper.html()).toMatchSnapshot() wrapper.setProps({ mustSort: true }) await wrapper.vm.$nextTick() expect(wrapper.html()).toMatchSnapshot() }) it('should apply class list to rows', () => { const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, itemsPerPage: 5, itemClass: () => ['my-class', 'my-other-class'], }, }) expect(wrapper.html()).toMatchSnapshot() }) it('should apply class unique to rows', () => { const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, itemsPerPage: 5, itemClass: () => 'my-unique-class', }, }) expect(wrapper.html()).toMatchSnapshot() }) it('should apply class function to rows', () => { const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, itemsPerPage: 5, itemClass: (item: Object) => ({ 'first-class': item.fat < 10, 'second-class': item.protein > 4.0, }), }, }) expect(wrapper.html()).toMatchSnapshot() }) it('should apply class from item to rows', () => { const wrapper = mountFunction({ propsData: { headers: testHeaders, items: testItems, itemsPerPage: 5, itemClass: 'class', }, }) expect(wrapper.html()).toMatchSnapshot() }) // https://github.com/vuetifyjs/vuetify/issues/11179 it('should return rows from columns that exclusively match custom filters', async () => { const wrapper = mountFunction({ propsData: { items: testItems, headers: [ { text: 'Dessert (100g serving)', align: 'left', value: 'name' }, { text: 'Calories', value: 'calories', filter: value => value === 159 }, { text: 'Fat (g)', value: 'fat' }, { text: 'Carbs (g)', value: 'carbs' }, { text: 'Protein (g)', value: 'protein' }, { text: 'Iron (%)', value: 'iron' }, ], }, }) await wrapper.vm.$nextTick() expect(wrapper.vm.internalCurrentItems).toHaveLength(1) }) // https://github.com/vuetifyjs/vuetify/issues/10244 it('should respect mustSort property on options', async () => { const wrapper = mountFunction({ propsData: { items: testItems, headers: [ { text: 'Dessert (100g serving)', value: 'name' }, ], options: { mustSort: true, }, }, }) wrapper.find('th').trigger('click') await wrapper.vm.$nextTick() wrapper.find('th').trigger('click') await wrapper.vm.$nextTick() wrapper.find('th').trigger('click') await wrapper.vm.$nextTick() expect(wrapper.html()).toMatchSnapshot() }) it('should hide group button when column is not groupable', async () => { const wrapper = mountFunction({ propsData: { showGroupBy: true, items: testItems, headers: [ { text: 'Dessert (100g serving)', align: 'left', value: 'name', groupable: false, }, { text: 'Calories', value: 'calories' }, { text: 'Fat (g)', value: 'fat' }, { text: 'Carbs (g)', value: 'carbs' }, { text: 'Protein (g)', value: 'protein' }, { text: 'Iron (%)', value: 'iron' }, ], }, }) expect(wrapper.html()).toMatchSnapshot() }) it('should return rows matching search term if specified', async () => { const wrapper = mountFunction({ propsData: { items: testItems, headers: [ { text: 'Dessert (100g serving)', align: 'left', value: 'name' }, { text: 'Calories', value: 'calories' }, { text: 'Fat (g)', value: 'fat' }, { text: 'Carbs (g)', value: 'carbs' }, { text: 'Protein (g)', value: 'protein' }, { text: 'Iron (%)', value: 'iron' }, ], }, }) wrapper.setProps({ search: 'unknown-term' }) await wrapper.vm.$nextTick() expect(wrapper.vm.internalCurrentItems).toHaveLength(0) wrapper.setProps({ search: 'Eclair' }) await wrapper.vm.$nextTick() expect(wrapper.vm.internalCurrentItems).toHaveLength(1) }) it('should return results which match both search term and column filters if both specified', async () => { const wrapper = mountFunction({ propsData: { items: testItems, headers: [ { text: 'Dessert (100g serving)', align: 'left', value: 'name' }, { text: 'Calories', value: 'calories', filter: value => value < 300 }, { text: 'Fat (g)', value: 'fat' }, { text: 'Carbs (g)', value: 'carbs' }, { text: 'Protein (g)', value: 'protein' }, { text: 'Iron (%)', value: 'iron' }, ], }, }) wrapper.setProps({ search: 'EA' }) await wrapper.vm.$nextTick() expect(wrapper.vm.internalCurrentItems).toHaveLength(1) }) })