| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [x] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [x] CHANGELOG modifié Reviewed-on: #27 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
279 lines
10 KiB
TypeScript
279 lines
10 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { h } from 'vue'
|
|
import { mount } from '@vue/test-utils'
|
|
import type { DefineComponent } from 'vue'
|
|
import DataTable from './DataTable.vue'
|
|
|
|
type DataTableProps = {
|
|
id?: string
|
|
columns?: { key: string; label: string }[]
|
|
items?: Record<string, unknown>[]
|
|
totalItems?: number
|
|
page?: number
|
|
perPage?: number
|
|
perPageOptions?: number[]
|
|
rowClickable?: boolean
|
|
tableClass?: string
|
|
emptyMessage?: string
|
|
}
|
|
|
|
const DataTableForTest = DataTable as DefineComponent<DataTableProps>
|
|
|
|
const defaultColumns = [
|
|
{ key: 'nom', label: 'Nom' },
|
|
{ key: 'ville', label: 'Ville' },
|
|
]
|
|
|
|
const defaultItems = [
|
|
{ nom: 'Dupont', ville: 'Paris' },
|
|
{ nom: 'Martin', ville: 'Lyon' },
|
|
{ nom: 'Bernard', ville: 'Marseille' },
|
|
]
|
|
|
|
function mountComponent(props: DataTableProps = {}, slots?: Record<string, unknown>) {
|
|
return mount(DataTableForTest, {
|
|
props: {
|
|
columns: defaultColumns,
|
|
items: defaultItems,
|
|
totalItems: 3,
|
|
...props,
|
|
},
|
|
slots,
|
|
global: {
|
|
stubs: {
|
|
MalioSelect: {
|
|
name: 'MalioSelect',
|
|
template: '<div data-test="malio-select"><slot /></div>',
|
|
props: ['modelValue', 'options'],
|
|
emits: ['update:modelValue'],
|
|
},
|
|
MalioButton: {
|
|
template: '<button v-bind="$attrs" :disabled="disabled" @click="$emit(\'click\', $event)"><slot>{{ label }}</slot></button>',
|
|
props: ['label', 'disabled', 'variant', 'buttonClass'],
|
|
emits: ['click'],
|
|
inheritAttrs: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
describe('MalioDataTable', () => {
|
|
describe('Table rendering', () => {
|
|
it('renders column headers as text when no header slot', () => {
|
|
const wrapper = mountComponent()
|
|
const headers = wrapper.findAll('th')
|
|
expect(headers).toHaveLength(2)
|
|
expect(headers[0].text()).toBe('Nom')
|
|
expect(headers[1].text()).toBe('Ville')
|
|
})
|
|
|
|
it('renders header slot when provided', () => {
|
|
const wrapper = mountComponent({}, {
|
|
'header-nom': '<input data-test="filter-nom" placeholder="Nom" />',
|
|
})
|
|
expect(wrapper.find('[data-test="filter-nom"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('renders items as rows', () => {
|
|
const wrapper = mountComponent()
|
|
const rows = wrapper.findAll('[data-test="row"]')
|
|
expect(rows).toHaveLength(3)
|
|
expect(rows[0].text()).toContain('Dupont')
|
|
expect(rows[0].text()).toContain('Paris')
|
|
})
|
|
|
|
it('renders cell slot when provided', () => {
|
|
const wrapper = mountComponent({}, {
|
|
'cell-nom': ({ item }: { item: Record<string, unknown> }) => h('strong', String(item.nom)),
|
|
})
|
|
const firstRow = wrapper.findAll('[data-test="row"]')[0]
|
|
expect(firstRow.find('strong').text()).toBe('Dupont')
|
|
})
|
|
|
|
it('renders empty message when items is empty', () => {
|
|
const wrapper = mountComponent({ items: [], totalItems: 0 })
|
|
expect(wrapper.find('[data-test="empty-row"]').text()).toBe('Aucune donnée')
|
|
})
|
|
|
|
it('renders custom empty message', () => {
|
|
const wrapper = mountComponent({ items: [], totalItems: 0, emptyMessage: 'Rien ici' })
|
|
expect(wrapper.find('[data-test="empty-row"]').text()).toBe('Rien ici')
|
|
})
|
|
|
|
it('renders empty slot when provided', () => {
|
|
const wrapper = mountComponent(
|
|
{ items: [], totalItems: 0 },
|
|
{ empty: '<p data-test="custom-empty">Vide</p>' },
|
|
)
|
|
expect(wrapper.find('[data-test="custom-empty"]').text()).toBe('Vide')
|
|
})
|
|
|
|
it('empty row has colspan equal to columns length', () => {
|
|
const wrapper = mountComponent({ items: [], totalItems: 0 })
|
|
const td = wrapper.find('[data-test="empty-row"] td')
|
|
expect(td.attributes('colspan')).toBe('2')
|
|
})
|
|
})
|
|
|
|
describe('Row click', () => {
|
|
it('emits row-click with item on row click', async () => {
|
|
const wrapper = mountComponent()
|
|
await wrapper.findAll('[data-test="row"]')[0].trigger('click')
|
|
expect(wrapper.emitted('row-click')?.[0]).toEqual([{ nom: 'Dupont', ville: 'Paris' }])
|
|
})
|
|
|
|
it('emits row-click on Enter key', async () => {
|
|
const wrapper = mountComponent()
|
|
await wrapper.findAll('[data-test="row"]')[0].trigger('keydown.enter')
|
|
expect(wrapper.emitted('row-click')?.[0]).toEqual([{ nom: 'Dupont', ville: 'Paris' }])
|
|
})
|
|
|
|
it('emits row-click on Space key', async () => {
|
|
const wrapper = mountComponent()
|
|
await wrapper.findAll('[data-test="row"]')[0].trigger('keydown.space')
|
|
expect(wrapper.emitted('row-click')?.[0]).toEqual([{ nom: 'Dupont', ville: 'Paris' }])
|
|
})
|
|
|
|
it('rows have tabindex when clickable', () => {
|
|
const wrapper = mountComponent()
|
|
expect(wrapper.findAll('[data-test="row"]')[0].attributes('tabindex')).toBe('0')
|
|
})
|
|
|
|
it('rows have cursor-pointer when clickable', () => {
|
|
const wrapper = mountComponent()
|
|
expect(wrapper.findAll('[data-test="row"]')[0].classes()).toContain('cursor-pointer')
|
|
})
|
|
|
|
it('rows are not clickable when rowClickable is false', async () => {
|
|
const wrapper = mountComponent({ rowClickable: false })
|
|
await wrapper.findAll('[data-test="row"]')[0].trigger('click')
|
|
expect(wrapper.emitted('row-click')).toBeUndefined()
|
|
})
|
|
|
|
it('rows have no tabindex when not clickable', () => {
|
|
const wrapper = mountComponent({ rowClickable: false })
|
|
expect(wrapper.findAll('[data-test="row"]')[0].attributes('tabindex')).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('Accessibility', () => {
|
|
it('th elements have scope="col"', () => {
|
|
const wrapper = mountComponent()
|
|
const ths = wrapper.findAll('th')
|
|
ths.forEach(th => {
|
|
expect(th.attributes('scope')).toBe('col')
|
|
})
|
|
})
|
|
|
|
it('generates an id when not provided', () => {
|
|
const wrapper = mountComponent()
|
|
const id = wrapper.find('div').attributes('id')
|
|
expect(id).toMatch(/^malio-datatable-/)
|
|
})
|
|
|
|
it('uses custom id when provided', () => {
|
|
const wrapper = mountComponent({ id: 'my-table' })
|
|
expect(wrapper.find('div').attributes('id')).toBe('my-table')
|
|
})
|
|
})
|
|
|
|
describe('Pagination', () => {
|
|
it('hides pagination when totalItems is 0', () => {
|
|
const wrapper = mountComponent({ items: [], totalItems: 0 })
|
|
expect(wrapper.find('[data-test="pagination"]').exists()).toBe(false)
|
|
})
|
|
|
|
it('shows pagination when totalItems > 0', () => {
|
|
const wrapper = mountComponent({ totalItems: 30 })
|
|
expect(wrapper.find('[data-test="pagination"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('renders all pages when totalPages <= 5', () => {
|
|
const wrapper = mountComponent({ totalItems: 50, perPage: 10 })
|
|
for (let i = 1; i <= 5; i++) {
|
|
expect(wrapper.find(`[data-test="page-${i}"]`).exists()).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('highlights current page', () => {
|
|
const wrapper = mountComponent({ totalItems: 50, perPage: 10, page: 3 })
|
|
expect(wrapper.find('[data-test="page-3"]').attributes('aria-current')).toBe('page')
|
|
})
|
|
|
|
it('emits update:page on page button click', async () => {
|
|
const wrapper = mountComponent({ totalItems: 50, perPage: 10, page: 1 })
|
|
await wrapper.find('[data-test="page-3"]').trigger('click')
|
|
expect(wrapper.emitted('update:page')?.[0]).toEqual([3])
|
|
})
|
|
|
|
it('Prev button is disabled on page 1', () => {
|
|
const wrapper = mountComponent({ totalItems: 50, perPage: 10, page: 1 })
|
|
expect(wrapper.find('[data-test="prev-button"]').attributes('disabled')).toBeDefined()
|
|
})
|
|
|
|
it('Next button is disabled on last page', () => {
|
|
const wrapper = mountComponent({ totalItems: 50, perPage: 10, page: 5 })
|
|
expect(wrapper.find('[data-test="next-button"]').attributes('disabled')).toBeDefined()
|
|
})
|
|
|
|
it('Prev button emits update:page with page - 1', async () => {
|
|
const wrapper = mountComponent({ totalItems: 50, perPage: 10, page: 3 })
|
|
await wrapper.find('[data-test="prev-button"]').trigger('click')
|
|
expect(wrapper.emitted('update:page')?.[0]).toEqual([2])
|
|
})
|
|
|
|
it('Next button emits update:page with page + 1', async () => {
|
|
const wrapper = mountComponent({ totalItems: 50, perPage: 10, page: 3 })
|
|
await wrapper.find('[data-test="next-button"]').trigger('click')
|
|
expect(wrapper.emitted('update:page')?.[0]).toEqual([4])
|
|
})
|
|
|
|
it('shows ellipsis for truncated pages (> 5 pages)', () => {
|
|
const wrapper = mountComponent({ totalItems: 200, perPage: 10, page: 10 })
|
|
const ellipsis = wrapper.findAll('[aria-hidden="true"]')
|
|
expect(ellipsis.length).toBeGreaterThan(0)
|
|
expect(ellipsis[0].text()).toBe('…')
|
|
})
|
|
|
|
it('always shows first and last page when > 5 pages', () => {
|
|
const wrapper = mountComponent({ totalItems: 200, perPage: 10, page: 10 })
|
|
expect(wrapper.find('[data-test="page-1"]').exists()).toBe(true)
|
|
expect(wrapper.find('[data-test="page-20"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('shows 1 neighbor on each side of current page', () => {
|
|
const wrapper = mountComponent({ totalItems: 200, perPage: 10, page: 10 })
|
|
expect(wrapper.find('[data-test="page-9"]').exists()).toBe(true)
|
|
expect(wrapper.find('[data-test="page-10"]').exists()).toBe(true)
|
|
expect(wrapper.find('[data-test="page-11"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('pagination nav has aria-label', () => {
|
|
const wrapper = mountComponent({ totalItems: 30 })
|
|
expect(wrapper.find('[data-test="pagination-nav"]').attributes('aria-label')).toBe('Pagination')
|
|
})
|
|
|
|
it('Prev button has aria-label "Page précédente"', () => {
|
|
const wrapper = mountComponent({ totalItems: 30 })
|
|
expect(wrapper.find('[data-test="prev-button"]').attributes('aria-label')).toBe('Page précédente')
|
|
})
|
|
|
|
it('Next button has aria-label "Page suivante"', () => {
|
|
const wrapper = mountComponent({ totalItems: 30 })
|
|
expect(wrapper.find('[data-test="next-button"]').attributes('aria-label')).toBe('Page suivante')
|
|
})
|
|
})
|
|
|
|
describe('Per-page selector', () => {
|
|
it('emits update:per-page and reset page to 1 on change', async () => {
|
|
const wrapper = mountComponent({ totalItems: 100, perPage: 10, page: 5 })
|
|
const select = wrapper.findComponent({ name: 'MalioSelect' })
|
|
select.vm.$emit('update:modelValue', 25)
|
|
await wrapper.vm.$nextTick()
|
|
expect(wrapper.emitted('update:per-page')?.[0]).toEqual([25])
|
|
expect(wrapper.emitted('update:page')?.[0]).toEqual([1])
|
|
})
|
|
})
|
|
})
|