@sabrenski/spire-ui 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/index.d.ts +4981 -0
- package/dist/spire-ui.css +1 -0
- package/dist/spire-ui.es.js +18403 -0
- package/dist/spire-ui.umd.js +45 -0
- package/package.json +83 -0
- package/src/components/Accordion/Accordion.test.ts +218 -0
- package/src/components/Accordion/AccordionContent.vue +112 -0
- package/src/components/Accordion/AccordionItem.vue +87 -0
- package/src/components/Accordion/AccordionRoot.vue +111 -0
- package/src/components/Accordion/AccordionTrigger.vue +125 -0
- package/src/components/Accordion/index.ts +11 -0
- package/src/components/Accordion/keys.ts +23 -0
- package/src/components/Avatar/Avatar.test.ts +181 -0
- package/src/components/Avatar/Avatar.vue +150 -0
- package/src/components/Avatar/index.ts +2 -0
- package/src/components/Badge/Badge.test.ts +141 -0
- package/src/components/Badge/Badge.vue +133 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/BadgeContainer/BadgeContainer.test.ts +150 -0
- package/src/components/BadgeContainer/BadgeContainer.vue +90 -0
- package/src/components/BadgeContainer/index.ts +2 -0
- package/src/components/Breadcrumb/Breadcrumb.test.ts +342 -0
- package/src/components/Breadcrumb/BreadcrumbEllipsis.vue +96 -0
- package/src/components/Breadcrumb/BreadcrumbItem.vue +16 -0
- package/src/components/Breadcrumb/BreadcrumbLink.vue +67 -0
- package/src/components/Breadcrumb/BreadcrumbList.vue +20 -0
- package/src/components/Breadcrumb/BreadcrumbPage.vue +25 -0
- package/src/components/Breadcrumb/BreadcrumbRoot.vue +41 -0
- package/src/components/Breadcrumb/BreadcrumbSeparator.vue +63 -0
- package/src/components/Breadcrumb/index.ts +13 -0
- package/src/components/Breadcrumb/keys.ts +7 -0
- package/src/components/Button/Button.test.ts +231 -0
- package/src/components/Button/Button.vue +349 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Callout/Callout.test.ts +260 -0
- package/src/components/Callout/Callout.vue +341 -0
- package/src/components/Callout/index.ts +2 -0
- package/src/components/Card/Card.test.ts +565 -0
- package/src/components/Card/Card.vue +209 -0
- package/src/components/Card/CardContent.vue +57 -0
- package/src/components/Card/CardFooter.vue +72 -0
- package/src/components/Card/CardHeader.vue +111 -0
- package/src/components/Card/CardImage.vue +124 -0
- package/src/components/Card/index.ts +14 -0
- package/src/components/Chart/BarChart.vue +208 -0
- package/src/components/Chart/BaseChart.vue +444 -0
- package/src/components/Chart/Chart.test.ts +359 -0
- package/src/components/Chart/DonutChart.vue +283 -0
- package/src/components/Chart/LineChart.vue +211 -0
- package/src/components/Chart/index.ts +20 -0
- package/src/components/Chart/useChartTheme.ts +192 -0
- package/src/components/Checkbox/Checkbox.test.ts +209 -0
- package/src/components/Checkbox/Checkbox.vue +285 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/ChoiceChip/ChoiceChip.test.ts +142 -0
- package/src/components/ChoiceChip/ChoiceChip.vue +218 -0
- package/src/components/ChoiceChip/index.ts +2 -0
- package/src/components/ChoiceChipGroup/ChoiceChipGroup.test.ts +151 -0
- package/src/components/ChoiceChipGroup/ChoiceChipGroup.vue +70 -0
- package/src/components/ChoiceChipGroup/index.ts +2 -0
- package/src/components/ColorPicker/ColorArea.vue +159 -0
- package/src/components/ColorPicker/ColorPicker.test.ts +250 -0
- package/src/components/ColorPicker/ColorPicker.vue +339 -0
- package/src/components/ColorPicker/ColorSlider.vue +191 -0
- package/src/components/ColorPicker/index.ts +7 -0
- package/src/components/Combobox/Combobox.test.ts +891 -0
- package/src/components/Combobox/Combobox.vue +934 -0
- package/src/components/Combobox/index.ts +2 -0
- package/src/components/DataTable/DataTable.test.ts +1221 -0
- package/src/components/DataTable/DataTable.vue +1415 -0
- package/src/components/DataTable/index.ts +10 -0
- package/src/components/DatePicker/DatePicker.test.ts +625 -0
- package/src/components/DatePicker/DatePicker.vue +1586 -0
- package/src/components/DatePicker/index.ts +2 -0
- package/src/components/Drawer/Drawer.test.ts +336 -0
- package/src/components/Drawer/Drawer.vue +466 -0
- package/src/components/Drawer/index.ts +2 -0
- package/src/components/Dropdown/Dropdown.test.ts +607 -0
- package/src/components/Dropdown/Dropdown.vue +807 -0
- package/src/components/Dropdown/DropdownItem.vue +227 -0
- package/src/components/Dropdown/DropdownSeparator.vue +14 -0
- package/src/components/Dropdown/DropdownSub.vue +104 -0
- package/src/components/Dropdown/DropdownSubContent.vue +187 -0
- package/src/components/Dropdown/DropdownSubTrigger.vue +151 -0
- package/src/components/Dropdown/index.ts +14 -0
- package/src/components/EmptyState/EmptyState.test.ts +180 -0
- package/src/components/EmptyState/EmptyState.vue +137 -0
- package/src/components/EmptyState/index.ts +2 -0
- package/src/components/FileUpload/FileUpload.test.ts +1151 -0
- package/src/components/FileUpload/FileUpload.vue +1042 -0
- package/src/components/FileUpload/index.ts +2 -0
- package/src/components/Heading/Heading.test.ts +107 -0
- package/src/components/Heading/Heading.vue +67 -0
- package/src/components/Heading/index.ts +2 -0
- package/src/components/Icon/Icon.test.ts +157 -0
- package/src/components/Icon/Icon.vue +86 -0
- package/src/components/Icon/index.ts +2 -0
- package/src/components/Input/Input.test.ts +273 -0
- package/src/components/Input/Input.vue +388 -0
- package/src/components/Input/index.ts +2 -0
- package/src/components/Layout/Container.vue +67 -0
- package/src/components/Layout/Grid.vue +159 -0
- package/src/components/Layout/GridItem.vue +154 -0
- package/src/components/Layout/Layout.test.ts +202 -0
- package/src/components/Layout/Stack.vue +128 -0
- package/src/components/Layout/index.ts +9 -0
- package/src/components/Layout/keys.ts +7 -0
- package/src/components/Modal/Modal.test.ts +311 -0
- package/src/components/Modal/Modal.vue +336 -0
- package/src/components/Modal/index.ts +2 -0
- package/src/components/Pagination/Pagination.test.ts +303 -0
- package/src/components/Pagination/Pagination.vue +212 -0
- package/src/components/Pagination/index.ts +3 -0
- package/src/components/Pagination/utils.ts +86 -0
- package/src/components/Popover/Popover.test.ts +285 -0
- package/src/components/Popover/Popover.vue +441 -0
- package/src/components/Popover/index.ts +2 -0
- package/src/components/Progress/Progress.test.ts +361 -0
- package/src/components/Progress/Progress.vue +363 -0
- package/src/components/Progress/index.ts +7 -0
- package/src/components/Radio/Radio.test.ts +216 -0
- package/src/components/Radio/Radio.vue +214 -0
- package/src/components/Radio/index.ts +2 -0
- package/src/components/Rating/Rating.test.ts +319 -0
- package/src/components/Rating/Rating.vue +247 -0
- package/src/components/Rating/index.ts +2 -0
- package/src/components/SegmentedControl/SegmentedControl.test.ts +292 -0
- package/src/components/SegmentedControl/SegmentedControl.vue +288 -0
- package/src/components/SegmentedControl/index.ts +2 -0
- package/src/components/Select/Select.test.ts +589 -0
- package/src/components/Select/Select.vue +666 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/Sidebar/Sidebar.test.ts +301 -0
- package/src/components/Sidebar/SidebarGroup.vue +103 -0
- package/src/components/Sidebar/SidebarItem.vue +196 -0
- package/src/components/Sidebar/SidebarLayout.vue +42 -0
- package/src/components/Sidebar/SidebarRoot.vue +122 -0
- package/src/components/Sidebar/index.ts +11 -0
- package/src/components/Sidebar/keys.ts +14 -0
- package/src/components/Skeleton/Skeleton.test.ts +130 -0
- package/src/components/Skeleton/Skeleton.vue +104 -0
- package/src/components/Skeleton/index.ts +2 -0
- package/src/components/Slider/Slider.test.ts +416 -0
- package/src/components/Slider/Slider.vue +435 -0
- package/src/components/Slider/index.ts +2 -0
- package/src/components/Slider/utils.ts +91 -0
- package/src/components/Spinner/Spinner.test.ts +79 -0
- package/src/components/Spinner/Spinner.vue +159 -0
- package/src/components/Spinner/index.ts +2 -0
- package/src/components/SpireProvider/SpireProvider.vue +71 -0
- package/src/components/SpireProvider/index.ts +11 -0
- package/src/components/Stepper/Stepper.test.ts +221 -0
- package/src/components/Stepper/StepperContent.vue +51 -0
- package/src/components/Stepper/StepperItem.vue +89 -0
- package/src/components/Stepper/StepperRoot.vue +101 -0
- package/src/components/Stepper/StepperSeparator.vue +52 -0
- package/src/components/Stepper/StepperTrigger.vue +144 -0
- package/src/components/Stepper/index.ts +11 -0
- package/src/components/Stepper/keys.ts +27 -0
- package/src/components/Switch/Switch.test.ts +214 -0
- package/src/components/Switch/Switch.vue +235 -0
- package/src/components/Switch/index.ts +2 -0
- package/src/components/Tabs/Tabs.test.ts +363 -0
- package/src/components/Tabs/Tabs.vue +318 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/Text/Text.test.ts +154 -0
- package/src/components/Text/Text.vue +100 -0
- package/src/components/Text/index.ts +2 -0
- package/src/components/Textarea/Textarea.test.ts +432 -0
- package/src/components/Textarea/Textarea.vue +411 -0
- package/src/components/Textarea/index.ts +2 -0
- package/src/components/TimePicker/TimePicker.test.ts +352 -0
- package/src/components/TimePicker/TimePicker.vue +569 -0
- package/src/components/TimePicker/index.ts +2 -0
- package/src/components/Timeline/Timeline.test.ts +193 -0
- package/src/components/Timeline/Timeline.vue +111 -0
- package/src/components/Timeline/TimelineItem.vue +167 -0
- package/src/components/Timeline/index.ts +13 -0
- package/src/components/Timeline/keys.ts +21 -0
- package/src/components/Toast/ToastItem.test.ts +289 -0
- package/src/components/Toast/ToastItem.vue +370 -0
- package/src/components/Toast/ToastProvider.test.ts +158 -0
- package/src/components/Toast/ToastProvider.vue +181 -0
- package/src/components/Toast/index.ts +83 -0
- package/src/components/Toast/toastState.test.ts +165 -0
- package/src/components/Toast/toastState.ts +161 -0
- package/src/components/ToggleButton/ToggleButton.test.ts +166 -0
- package/src/components/ToggleButton/ToggleButton.vue +197 -0
- package/src/components/ToggleButton/index.ts +2 -0
- package/src/components/ToggleGroup/ToggleGroup.test.ts +181 -0
- package/src/components/ToggleGroup/ToggleGroup.vue +130 -0
- package/src/components/ToggleGroup/index.ts +2 -0
- package/src/components/Tooltip/Tooltip.test.ts +238 -0
- package/src/components/Tooltip/Tooltip.vue +217 -0
- package/src/components/Tooltip/index.ts +2 -0
- package/src/components/TreeView/TreeView.test.ts +357 -0
- package/src/components/TreeView/TreeView.vue +251 -0
- package/src/components/TreeView/TreeViewItem.vue +288 -0
- package/src/components/TreeView/index.ts +11 -0
- package/src/components/TreeView/keys.ts +35 -0
- package/src/composables/index.ts +12 -0
- package/src/composables/useClickOutside.ts +36 -0
- package/src/composables/useClipboard.ts +35 -0
- package/src/composables/useEventListener.ts +48 -0
- package/src/composables/useFocusTrap.ts +58 -0
- package/src/composables/useHoverReveal.ts +98 -0
- package/src/composables/useId.ts +10 -0
- package/src/composables/useMagnetic.ts +171 -0
- package/src/composables/useRelativePosition.ts +127 -0
- package/src/composables/useRipple.ts +146 -0
- package/src/composables/useScrollLock.ts +25 -0
- package/src/composables/useSpireConfig.ts +27 -0
- package/src/composables/useStagger.ts +224 -0
- package/src/config/icons.test.ts +115 -0
- package/src/config/icons.ts +170 -0
- package/src/index.ts +361 -0
- package/src/styles/depth.css +129 -0
- package/src/styles/effects.css +169 -0
- package/src/styles/fallback.css +152 -0
- package/src/styles/main.css +25 -0
- package/src/styles/mood.css +211 -0
- package/src/styles/motion.css +159 -0
- package/src/styles/reset.css +97 -0
- package/src/styles/theme.css +708 -0
- package/src/styles/tokens.css +183 -0
- package/src/utils/.gitkeep +0 -0
- package/src/utils/color.ts +277 -0
- package/src/utils/date.test.ts +522 -0
- package/src/utils/date.ts +380 -0
- package/src/utils/index.ts +23 -0
- package/src/utils/object.test.ts +80 -0
- package/src/utils/object.ts +25 -0
- package/src/utils/string.test.ts +64 -0
- package/src/utils/string.ts +32 -0
- package/src/utils/time.ts +156 -0
|
@@ -0,0 +1,1221 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import DataTable from './DataTable.vue'
|
|
4
|
+
|
|
5
|
+
const defaultColumns = [
|
|
6
|
+
{ key: 'name', label: 'Name' },
|
|
7
|
+
{ key: 'email', label: 'Email' },
|
|
8
|
+
{ key: 'role', label: 'Role' }
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
const defaultData = [
|
|
12
|
+
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' },
|
|
13
|
+
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' },
|
|
14
|
+
{ id: 3, name: 'Bob Wilson', email: 'bob@example.com', role: 'Editor' }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
describe('DataTable', () => {
|
|
18
|
+
describe('Rendering', () => {
|
|
19
|
+
it('renders with default props', () => {
|
|
20
|
+
const wrapper = mount(DataTable, {
|
|
21
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
22
|
+
})
|
|
23
|
+
expect(wrapper.find('.ui-table').exists()).toBe(true)
|
|
24
|
+
expect(wrapper.find('.ui-table__table').exists()).toBe(true)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('renders all column headers', () => {
|
|
28
|
+
const wrapper = mount(DataTable, {
|
|
29
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
30
|
+
})
|
|
31
|
+
const headers = wrapper.findAll('.ui-table__header-cell')
|
|
32
|
+
expect(headers.length).toBe(3)
|
|
33
|
+
expect(headers[0].text()).toBe('Name')
|
|
34
|
+
expect(headers[1].text()).toBe('Email')
|
|
35
|
+
expect(headers[2].text()).toBe('Role')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('renders all data rows', () => {
|
|
39
|
+
const wrapper = mount(DataTable, {
|
|
40
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
41
|
+
})
|
|
42
|
+
const rows = wrapper.findAll('.ui-table__body .ui-table__row')
|
|
43
|
+
expect(rows.length).toBe(3)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('renders cell values', () => {
|
|
47
|
+
const wrapper = mount(DataTable, {
|
|
48
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
49
|
+
})
|
|
50
|
+
const cells = wrapper.findAll('.ui-table__cell')
|
|
51
|
+
expect(cells[0].text()).toBe('John Doe')
|
|
52
|
+
expect(cells[1].text()).toBe('john@example.com')
|
|
53
|
+
expect(cells[2].text()).toBe('Admin')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('renders nested property values', () => {
|
|
57
|
+
const columns = [{ key: 'user.email', label: 'User Email' }]
|
|
58
|
+
const data = [{ id: 1, user: { email: 'nested@example.com' } }]
|
|
59
|
+
const wrapper = mount(DataTable, {
|
|
60
|
+
props: { columns, data }
|
|
61
|
+
})
|
|
62
|
+
expect(wrapper.find('.ui-table__cell').text()).toBe('nested@example.com')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('handles missing nested values gracefully', () => {
|
|
66
|
+
const columns = [{ key: 'user.email', label: 'User Email' }]
|
|
67
|
+
const data = [{ id: 1, user: {} }]
|
|
68
|
+
const wrapper = mount(DataTable, {
|
|
69
|
+
props: { columns, data }
|
|
70
|
+
})
|
|
71
|
+
expect(wrapper.find('.ui-table__cell').text()).toBe('')
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('Empty State', () => {
|
|
76
|
+
it('renders empty state when data is empty', () => {
|
|
77
|
+
const wrapper = mount(DataTable, {
|
|
78
|
+
props: { columns: defaultColumns, data: [] }
|
|
79
|
+
})
|
|
80
|
+
expect(wrapper.find('.ui-table__row--empty').exists()).toBe(true)
|
|
81
|
+
expect(wrapper.findComponent({ name: 'EmptyState' }).exists()).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('renders default empty message', () => {
|
|
85
|
+
const wrapper = mount(DataTable, {
|
|
86
|
+
props: { columns: defaultColumns, data: [] }
|
|
87
|
+
})
|
|
88
|
+
const emptyState = wrapper.findComponent({ name: 'EmptyState' })
|
|
89
|
+
expect(emptyState.props('title')).toBe('No data available')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('renders custom empty message', () => {
|
|
93
|
+
const wrapper = mount(DataTable, {
|
|
94
|
+
props: {
|
|
95
|
+
columns: defaultColumns,
|
|
96
|
+
data: [],
|
|
97
|
+
emptyMessage: 'No users found'
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
const emptyState = wrapper.findComponent({ name: 'EmptyState' })
|
|
101
|
+
expect(emptyState.props('title')).toBe('No users found')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('renders empty slot when provided', () => {
|
|
105
|
+
const wrapper = mount(DataTable, {
|
|
106
|
+
props: { columns: defaultColumns, data: [] },
|
|
107
|
+
slots: {
|
|
108
|
+
empty: '<div class="custom-empty">Custom empty state</div>'
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
expect(wrapper.find('.custom-empty').exists()).toBe(true)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('Loading State', () => {
|
|
116
|
+
it('applies loading class when loading', () => {
|
|
117
|
+
const wrapper = mount(DataTable, {
|
|
118
|
+
props: { columns: defaultColumns, data: defaultData, loading: true }
|
|
119
|
+
})
|
|
120
|
+
expect(wrapper.find('.ui-table--loading').exists()).toBe(true)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('sets aria-busy when loading', () => {
|
|
124
|
+
const wrapper = mount(DataTable, {
|
|
125
|
+
props: { columns: defaultColumns, data: defaultData, loading: true }
|
|
126
|
+
})
|
|
127
|
+
expect(wrapper.find('.ui-table__table').attributes('aria-busy')).toBe('true')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('renders skeleton rows when loading', () => {
|
|
131
|
+
const wrapper = mount(DataTable, {
|
|
132
|
+
props: { columns: defaultColumns, data: defaultData, loading: true }
|
|
133
|
+
})
|
|
134
|
+
const skeletonRows = wrapper.findAll('.ui-table__row--skeleton')
|
|
135
|
+
expect(skeletonRows.length).toBe(5) // default skeletonRows
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('renders custom number of skeleton rows', () => {
|
|
139
|
+
const wrapper = mount(DataTable, {
|
|
140
|
+
props: {
|
|
141
|
+
columns: defaultColumns,
|
|
142
|
+
data: defaultData,
|
|
143
|
+
loading: true,
|
|
144
|
+
skeletonRows: 10
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
const skeletonRows = wrapper.findAll('.ui-table__row--skeleton')
|
|
148
|
+
expect(skeletonRows.length).toBe(10)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('renders skeleton bars in each cell', () => {
|
|
152
|
+
const wrapper = mount(DataTable, {
|
|
153
|
+
props: {
|
|
154
|
+
columns: defaultColumns,
|
|
155
|
+
data: defaultData,
|
|
156
|
+
loading: true,
|
|
157
|
+
skeletonRows: 1
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
const skeletons = wrapper.findAllComponents({ name: 'Skeleton' })
|
|
161
|
+
expect(skeletons.length).toBe(3)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('Selection', () => {
|
|
166
|
+
it('renders checkbox column when selectable', () => {
|
|
167
|
+
const wrapper = mount(DataTable, {
|
|
168
|
+
props: {
|
|
169
|
+
columns: defaultColumns,
|
|
170
|
+
data: defaultData,
|
|
171
|
+
selectable: true
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
expect(wrapper.find('.ui-table__header-cell--checkbox').exists()).toBe(true)
|
|
175
|
+
const checkboxCells = wrapper.findAll('.ui-table__cell--checkbox')
|
|
176
|
+
expect(checkboxCells.length).toBe(3)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('does not render checkbox column when not selectable', () => {
|
|
180
|
+
const wrapper = mount(DataTable, {
|
|
181
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
182
|
+
})
|
|
183
|
+
expect(wrapper.find('.ui-table__header-cell--checkbox').exists()).toBe(false)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('emits update:selectedIds when row is selected', async () => {
|
|
187
|
+
const wrapper = mount(DataTable, {
|
|
188
|
+
props: {
|
|
189
|
+
columns: defaultColumns,
|
|
190
|
+
data: defaultData,
|
|
191
|
+
selectable: true,
|
|
192
|
+
selectedIds: []
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
const checkbox = wrapper.findAllComponents({ name: 'Checkbox' })[1]
|
|
196
|
+
await checkbox.vm.$emit('update:modelValue', true)
|
|
197
|
+
expect(wrapper.emitted('update:selectedIds')).toBeTruthy()
|
|
198
|
+
expect(wrapper.emitted('update:selectedIds')![0]).toEqual([[1]])
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('emits update:selectedIds when row is deselected', async () => {
|
|
202
|
+
const wrapper = mount(DataTable, {
|
|
203
|
+
props: {
|
|
204
|
+
columns: defaultColumns,
|
|
205
|
+
data: defaultData,
|
|
206
|
+
selectable: true,
|
|
207
|
+
selectedIds: [1, 2]
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
const checkbox = wrapper.findAllComponents({ name: 'Checkbox' })[1]
|
|
211
|
+
await checkbox.vm.$emit('update:modelValue', false)
|
|
212
|
+
expect(wrapper.emitted('update:selectedIds')![0]).toEqual([[2]])
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('applies selected class to selected rows', () => {
|
|
216
|
+
const wrapper = mount(DataTable, {
|
|
217
|
+
props: {
|
|
218
|
+
columns: defaultColumns,
|
|
219
|
+
data: defaultData,
|
|
220
|
+
selectable: true,
|
|
221
|
+
selectedIds: [1]
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
const rows = wrapper.findAll('.ui-table__body .ui-table__row')
|
|
225
|
+
expect(rows[0].classes()).toContain('ui-table__row--selected')
|
|
226
|
+
expect(rows[1].classes()).not.toContain('ui-table__row--selected')
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('select all selects all rows', async () => {
|
|
230
|
+
const wrapper = mount(DataTable, {
|
|
231
|
+
props: {
|
|
232
|
+
columns: defaultColumns,
|
|
233
|
+
data: defaultData,
|
|
234
|
+
selectable: true,
|
|
235
|
+
selectedIds: []
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
const selectAllCheckbox = wrapper.findAllComponents({ name: 'Checkbox' })[0]
|
|
239
|
+
await selectAllCheckbox.vm.$emit('update:modelValue', true)
|
|
240
|
+
expect(wrapper.emitted('update:selectedIds')![0]).toEqual([[1, 2, 3]])
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('select all deselects all rows', async () => {
|
|
244
|
+
const wrapper = mount(DataTable, {
|
|
245
|
+
props: {
|
|
246
|
+
columns: defaultColumns,
|
|
247
|
+
data: defaultData,
|
|
248
|
+
selectable: true,
|
|
249
|
+
selectedIds: [1, 2, 3]
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
const selectAllCheckbox = wrapper.findAllComponents({ name: 'Checkbox' })[0]
|
|
253
|
+
await selectAllCheckbox.vm.$emit('update:modelValue', false)
|
|
254
|
+
expect(wrapper.emitted('update:selectedIds')![0]).toEqual([[]])
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('select all checkbox is indeterminate when some selected', () => {
|
|
258
|
+
const wrapper = mount(DataTable, {
|
|
259
|
+
props: {
|
|
260
|
+
columns: defaultColumns,
|
|
261
|
+
data: defaultData,
|
|
262
|
+
selectable: true,
|
|
263
|
+
selectedIds: [1]
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
const selectAllCheckbox = wrapper.findAllComponents({ name: 'Checkbox' })[0]
|
|
267
|
+
expect(selectAllCheckbox.props('indeterminate')).toBe(true)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('select all checkbox is checked when all selected', () => {
|
|
271
|
+
const wrapper = mount(DataTable, {
|
|
272
|
+
props: {
|
|
273
|
+
columns: defaultColumns,
|
|
274
|
+
data: defaultData,
|
|
275
|
+
selectable: true,
|
|
276
|
+
selectedIds: [1, 2, 3]
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
const selectAllCheckbox = wrapper.findAllComponents({ name: 'Checkbox' })[0]
|
|
280
|
+
expect(selectAllCheckbox.props('modelValue')).toBe(true)
|
|
281
|
+
expect(selectAllCheckbox.props('indeterminate')).toBe(false)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('uses custom rowKey for selection', async () => {
|
|
285
|
+
const data = [
|
|
286
|
+
{ uuid: 'a', name: 'John' },
|
|
287
|
+
{ uuid: 'b', name: 'Jane' }
|
|
288
|
+
]
|
|
289
|
+
const wrapper = mount(DataTable, {
|
|
290
|
+
props: {
|
|
291
|
+
columns: [{ key: 'name', label: 'Name' }],
|
|
292
|
+
data,
|
|
293
|
+
selectable: true,
|
|
294
|
+
selectedIds: [],
|
|
295
|
+
rowKey: 'uuid'
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
const checkbox = wrapper.findAllComponents({ name: 'Checkbox' })[1]
|
|
299
|
+
await checkbox.vm.$emit('update:modelValue', true)
|
|
300
|
+
expect(wrapper.emitted('update:selectedIds')![0]).toEqual([['a']])
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
describe('Sorting', () => {
|
|
305
|
+
it('renders sortable header when column is sortable', () => {
|
|
306
|
+
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
|
307
|
+
const wrapper = mount(DataTable, {
|
|
308
|
+
props: { columns, data: defaultData }
|
|
309
|
+
})
|
|
310
|
+
expect(wrapper.find('.ui-table__header-cell--sortable').exists()).toBe(true)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('does not render sortable header when column is not sortable', () => {
|
|
314
|
+
const wrapper = mount(DataTable, {
|
|
315
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
316
|
+
})
|
|
317
|
+
expect(wrapper.find('.ui-table__header-cell--sortable').exists()).toBe(false)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('renders sort icon on sortable columns', () => {
|
|
321
|
+
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
|
322
|
+
const wrapper = mount(DataTable, {
|
|
323
|
+
props: { columns, data: defaultData }
|
|
324
|
+
})
|
|
325
|
+
expect(wrapper.find('.ui-table__sort-icon').exists()).toBe(true)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('emits sort-change on header click', async () => {
|
|
329
|
+
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
|
330
|
+
const wrapper = mount(DataTable, {
|
|
331
|
+
props: { columns, data: defaultData }
|
|
332
|
+
})
|
|
333
|
+
await wrapper.find('.ui-table__header-cell--sortable').trigger('click')
|
|
334
|
+
expect(wrapper.emitted('sort-change')).toBeTruthy()
|
|
335
|
+
expect(wrapper.emitted('sort-change')![0]).toEqual([{ key: 'name', direction: 'asc' }])
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('cycles through sort directions: asc -> desc -> null', async () => {
|
|
339
|
+
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
|
340
|
+
const wrapper = mount(DataTable, {
|
|
341
|
+
props: { columns, data: defaultData }
|
|
342
|
+
})
|
|
343
|
+
const header = wrapper.find('.ui-table__header-cell--sortable')
|
|
344
|
+
|
|
345
|
+
await header.trigger('click')
|
|
346
|
+
expect(wrapper.emitted('sort-change')![0]).toEqual([{ key: 'name', direction: 'asc' }])
|
|
347
|
+
|
|
348
|
+
await header.trigger('click')
|
|
349
|
+
expect(wrapper.emitted('sort-change')![1]).toEqual([{ key: 'name', direction: 'desc' }])
|
|
350
|
+
|
|
351
|
+
await header.trigger('click')
|
|
352
|
+
expect(wrapper.emitted('sort-change')![2]).toEqual([{ key: null, direction: null }])
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('resets to asc when switching to different sortable column', async () => {
|
|
356
|
+
const columns = [
|
|
357
|
+
{ key: 'name', label: 'Name', sortable: true },
|
|
358
|
+
{ key: 'email', label: 'Email', sortable: true }
|
|
359
|
+
]
|
|
360
|
+
const wrapper = mount(DataTable, {
|
|
361
|
+
props: { columns, data: defaultData }
|
|
362
|
+
})
|
|
363
|
+
const headers = wrapper.findAll('.ui-table__header-cell--sortable')
|
|
364
|
+
|
|
365
|
+
await headers[0].trigger('click')
|
|
366
|
+
expect(wrapper.emitted('sort-change')![0]).toEqual([{ key: 'name', direction: 'asc' }])
|
|
367
|
+
|
|
368
|
+
await headers[1].trigger('click')
|
|
369
|
+
expect(wrapper.emitted('sort-change')![1]).toEqual([{ key: 'email', direction: 'asc' }])
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('sorts data locally in ascending order', async () => {
|
|
373
|
+
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
|
374
|
+
const data = [
|
|
375
|
+
{ id: 1, name: 'Charlie' },
|
|
376
|
+
{ id: 2, name: 'Alice' },
|
|
377
|
+
{ id: 3, name: 'Bob' }
|
|
378
|
+
]
|
|
379
|
+
const wrapper = mount(DataTable, {
|
|
380
|
+
props: { columns, data }
|
|
381
|
+
})
|
|
382
|
+
await wrapper.find('.ui-table__header-cell--sortable').trigger('click')
|
|
383
|
+
const cells = wrapper.findAll('.ui-table__cell')
|
|
384
|
+
expect(cells[0].text()).toBe('Alice')
|
|
385
|
+
expect(cells[1].text()).toBe('Bob')
|
|
386
|
+
expect(cells[2].text()).toBe('Charlie')
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('sorts data locally in descending order', async () => {
|
|
390
|
+
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
|
391
|
+
const data = [
|
|
392
|
+
{ id: 1, name: 'Alice' },
|
|
393
|
+
{ id: 2, name: 'Charlie' },
|
|
394
|
+
{ id: 3, name: 'Bob' }
|
|
395
|
+
]
|
|
396
|
+
const wrapper = mount(DataTable, {
|
|
397
|
+
props: { columns, data }
|
|
398
|
+
})
|
|
399
|
+
const header = wrapper.find('.ui-table__header-cell--sortable')
|
|
400
|
+
await header.trigger('click')
|
|
401
|
+
await header.trigger('click')
|
|
402
|
+
const cells = wrapper.findAll('.ui-table__cell')
|
|
403
|
+
expect(cells[0].text()).toBe('Charlie')
|
|
404
|
+
expect(cells[1].text()).toBe('Bob')
|
|
405
|
+
expect(cells[2].text()).toBe('Alice')
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('sorts numeric values correctly', async () => {
|
|
409
|
+
const columns = [{ key: 'age', label: 'Age', sortable: true }]
|
|
410
|
+
const data = [
|
|
411
|
+
{ id: 1, age: 25 },
|
|
412
|
+
{ id: 2, age: 10 },
|
|
413
|
+
{ id: 3, age: 100 }
|
|
414
|
+
]
|
|
415
|
+
const wrapper = mount(DataTable, {
|
|
416
|
+
props: { columns, data }
|
|
417
|
+
})
|
|
418
|
+
await wrapper.find('.ui-table__header-cell--sortable').trigger('click')
|
|
419
|
+
const cells = wrapper.findAll('.ui-table__cell')
|
|
420
|
+
expect(cells[0].text()).toBe('10')
|
|
421
|
+
expect(cells[1].text()).toBe('25')
|
|
422
|
+
expect(cells[2].text()).toBe('100')
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('handles null values in sorting', async () => {
|
|
426
|
+
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
|
427
|
+
const data = [
|
|
428
|
+
{ id: 1, name: 'Bob' },
|
|
429
|
+
{ id: 2, name: null },
|
|
430
|
+
{ id: 3, name: 'Alice' }
|
|
431
|
+
]
|
|
432
|
+
const wrapper = mount(DataTable, {
|
|
433
|
+
props: { columns, data }
|
|
434
|
+
})
|
|
435
|
+
await wrapper.find('.ui-table__header-cell--sortable').trigger('click')
|
|
436
|
+
const cells = wrapper.findAll('.ui-table__cell')
|
|
437
|
+
expect(cells[0].text()).toBe('Alice')
|
|
438
|
+
expect(cells[1].text()).toBe('Bob')
|
|
439
|
+
expect(cells[2].text()).toBe('')
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('applies sorted class to active sort column', async () => {
|
|
443
|
+
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
|
444
|
+
const wrapper = mount(DataTable, {
|
|
445
|
+
props: { columns, data: defaultData }
|
|
446
|
+
})
|
|
447
|
+
await wrapper.find('.ui-table__header-cell--sortable').trigger('click')
|
|
448
|
+
expect(wrapper.find('.ui-table__header-cell--sorted').exists()).toBe(true)
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('applies asc class to sort icon when ascending', async () => {
|
|
452
|
+
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
|
453
|
+
const wrapper = mount(DataTable, {
|
|
454
|
+
props: { columns, data: defaultData }
|
|
455
|
+
})
|
|
456
|
+
await wrapper.find('.ui-table__header-cell--sortable').trigger('click')
|
|
457
|
+
expect(wrapper.find('.ui-table__sort-icon--asc').exists()).toBe(true)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
it('applies desc class to sort icon when descending', async () => {
|
|
461
|
+
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
|
462
|
+
const wrapper = mount(DataTable, {
|
|
463
|
+
props: { columns, data: defaultData }
|
|
464
|
+
})
|
|
465
|
+
const header = wrapper.find('.ui-table__header-cell--sortable')
|
|
466
|
+
await header.trigger('click')
|
|
467
|
+
await header.trigger('click')
|
|
468
|
+
expect(wrapper.find('.ui-table__sort-icon--desc').exists()).toBe(true)
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it('supports keyboard navigation for sorting', async () => {
|
|
472
|
+
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
|
473
|
+
const wrapper = mount(DataTable, {
|
|
474
|
+
props: { columns, data: defaultData }
|
|
475
|
+
})
|
|
476
|
+
await wrapper.find('.ui-table__header-cell--sortable').trigger('keydown', { key: 'Enter' })
|
|
477
|
+
expect(wrapper.emitted('sort-change')).toBeTruthy()
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('supports Space key for sorting', async () => {
|
|
481
|
+
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
|
482
|
+
const wrapper = mount(DataTable, {
|
|
483
|
+
props: { columns, data: defaultData }
|
|
484
|
+
})
|
|
485
|
+
await wrapper.find('.ui-table__header-cell--sortable').trigger('keydown', { key: ' ' })
|
|
486
|
+
expect(wrapper.emitted('sort-change')).toBeTruthy()
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
it('sets aria-sort attribute', async () => {
|
|
490
|
+
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
|
491
|
+
const wrapper = mount(DataTable, {
|
|
492
|
+
props: { columns, data: defaultData }
|
|
493
|
+
})
|
|
494
|
+
const header = wrapper.find('.ui-table__header-cell--sortable')
|
|
495
|
+
expect(header.attributes('aria-sort')).toBe('none')
|
|
496
|
+
|
|
497
|
+
await header.trigger('click')
|
|
498
|
+
expect(header.attributes('aria-sort')).toBe('ascending')
|
|
499
|
+
|
|
500
|
+
await header.trigger('click')
|
|
501
|
+
expect(header.attributes('aria-sort')).toBe('descending')
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it('sorts mixed-type columns stably by grouping types', async () => {
|
|
505
|
+
const columns = [{ key: 'val', label: 'Value', sortable: true }]
|
|
506
|
+
const data = [
|
|
507
|
+
{ id: 1, val: '10' },
|
|
508
|
+
{ id: 2, val: 5 },
|
|
509
|
+
{ id: 3, val: null },
|
|
510
|
+
{ id: 4, val: 'apple' },
|
|
511
|
+
{ id: 5, val: 100 },
|
|
512
|
+
{ id: 6, val: true }
|
|
513
|
+
]
|
|
514
|
+
const wrapper = mount(DataTable, {
|
|
515
|
+
props: { columns, data }
|
|
516
|
+
})
|
|
517
|
+
await wrapper.find('.ui-table__header-cell--sortable').trigger('click')
|
|
518
|
+
const rows = wrapper.findAll('.ui-table__body .ui-table__row')
|
|
519
|
+
const values = rows.map(row => row.find('.ui-table__cell').text())
|
|
520
|
+
expect(values).toEqual(['5', '100', '10', 'apple', 'true', ''])
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
it('maintains type grouping in descending order', async () => {
|
|
524
|
+
const columns = [{ key: 'val', label: 'Value', sortable: true }]
|
|
525
|
+
const data = [
|
|
526
|
+
{ id: 1, val: 'banana' },
|
|
527
|
+
{ id: 2, val: 10 },
|
|
528
|
+
{ id: 3, val: 'apple' },
|
|
529
|
+
{ id: 4, val: 5 }
|
|
530
|
+
]
|
|
531
|
+
const wrapper = mount(DataTable, {
|
|
532
|
+
props: { columns, data }
|
|
533
|
+
})
|
|
534
|
+
const header = wrapper.find('.ui-table__header-cell--sortable')
|
|
535
|
+
await header.trigger('click')
|
|
536
|
+
await header.trigger('click')
|
|
537
|
+
const rows = wrapper.findAll('.ui-table__body .ui-table__row')
|
|
538
|
+
const values = rows.map(row => row.find('.ui-table__cell').text())
|
|
539
|
+
expect(values).toEqual(['banana', 'apple', '10', '5'])
|
|
540
|
+
})
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
describe('Row Click', () => {
|
|
544
|
+
it('emits row-click when row is clicked', async () => {
|
|
545
|
+
const wrapper = mount(DataTable, {
|
|
546
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
547
|
+
})
|
|
548
|
+
await wrapper.find('.ui-table__row').trigger('click')
|
|
549
|
+
expect(wrapper.emitted('row-click')).toBeTruthy()
|
|
550
|
+
expect(wrapper.emitted('row-click')![0]).toEqual([defaultData[0], 0])
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it('emits row-click with correct row and index', async () => {
|
|
554
|
+
const wrapper = mount(DataTable, {
|
|
555
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
556
|
+
})
|
|
557
|
+
const rows = wrapper.findAll('.ui-table__body .ui-table__row')
|
|
558
|
+
await rows[2].trigger('click')
|
|
559
|
+
expect(wrapper.emitted('row-click')![0]).toEqual([defaultData[2], 2])
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
it('does not trigger row-click when checkbox is clicked', async () => {
|
|
563
|
+
const wrapper = mount(DataTable, {
|
|
564
|
+
props: {
|
|
565
|
+
columns: defaultColumns,
|
|
566
|
+
data: defaultData,
|
|
567
|
+
selectable: true,
|
|
568
|
+
selectedIds: []
|
|
569
|
+
}
|
|
570
|
+
})
|
|
571
|
+
await wrapper.find('.ui-table__cell--checkbox').trigger('click')
|
|
572
|
+
expect(wrapper.emitted('row-click')).toBeFalsy()
|
|
573
|
+
})
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
describe('Sizes', () => {
|
|
577
|
+
it('applies small size', () => {
|
|
578
|
+
const wrapper = mount(DataTable, {
|
|
579
|
+
props: { columns: defaultColumns, data: defaultData, size: 'sm' }
|
|
580
|
+
})
|
|
581
|
+
expect(wrapper.find('.ui-table--sm').exists()).toBe(true)
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
it('applies medium size by default', () => {
|
|
585
|
+
const wrapper = mount(DataTable, {
|
|
586
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
587
|
+
})
|
|
588
|
+
expect(wrapper.find('.ui-table--md').exists()).toBe(true)
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
it('applies large size', () => {
|
|
592
|
+
const wrapper = mount(DataTable, {
|
|
593
|
+
props: { columns: defaultColumns, data: defaultData, size: 'lg' }
|
|
594
|
+
})
|
|
595
|
+
expect(wrapper.find('.ui-table--lg').exists()).toBe(true)
|
|
596
|
+
})
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
describe('Visual Options', () => {
|
|
600
|
+
it('applies striped class', () => {
|
|
601
|
+
const wrapper = mount(DataTable, {
|
|
602
|
+
props: { columns: defaultColumns, data: defaultData, striped: true }
|
|
603
|
+
})
|
|
604
|
+
expect(wrapper.find('.ui-table--striped').exists()).toBe(true)
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
it('applies hoverable class by default', () => {
|
|
608
|
+
const wrapper = mount(DataTable, {
|
|
609
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
610
|
+
})
|
|
611
|
+
expect(wrapper.find('.ui-table--hoverable').exists()).toBe(true)
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
it('removes hoverable class when disabled', () => {
|
|
615
|
+
const wrapper = mount(DataTable, {
|
|
616
|
+
props: { columns: defaultColumns, data: defaultData, hoverable: false }
|
|
617
|
+
})
|
|
618
|
+
expect(wrapper.find('.ui-table--hoverable').exists()).toBe(false)
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
it('applies bordered class', () => {
|
|
622
|
+
const wrapper = mount(DataTable, {
|
|
623
|
+
props: { columns: defaultColumns, data: defaultData, bordered: true }
|
|
624
|
+
})
|
|
625
|
+
expect(wrapper.find('.ui-table--bordered').exists()).toBe(true)
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
it('applies sticky header class', () => {
|
|
629
|
+
const wrapper = mount(DataTable, {
|
|
630
|
+
props: { columns: defaultColumns, data: defaultData, stickyHeader: true }
|
|
631
|
+
})
|
|
632
|
+
expect(wrapper.find('.ui-table--sticky-header').exists()).toBe(true)
|
|
633
|
+
})
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
describe('Column Alignment', () => {
|
|
637
|
+
it('applies left alignment by default', () => {
|
|
638
|
+
const wrapper = mount(DataTable, {
|
|
639
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
640
|
+
})
|
|
641
|
+
expect(wrapper.find('.ui-table__header-cell--left').exists()).toBe(true)
|
|
642
|
+
expect(wrapper.find('.ui-table__cell--left').exists()).toBe(true)
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
it('applies center alignment', () => {
|
|
646
|
+
const columns = [{ key: 'name', label: 'Name', align: 'center' as const }]
|
|
647
|
+
const wrapper = mount(DataTable, {
|
|
648
|
+
props: { columns, data: defaultData }
|
|
649
|
+
})
|
|
650
|
+
expect(wrapper.find('.ui-table__header-cell--center').exists()).toBe(true)
|
|
651
|
+
expect(wrapper.find('.ui-table__cell--center').exists()).toBe(true)
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
it('applies right alignment', () => {
|
|
655
|
+
const columns = [{ key: 'name', label: 'Name', align: 'right' as const }]
|
|
656
|
+
const wrapper = mount(DataTable, {
|
|
657
|
+
props: { columns, data: defaultData }
|
|
658
|
+
})
|
|
659
|
+
expect(wrapper.find('.ui-table__header-cell--right').exists()).toBe(true)
|
|
660
|
+
expect(wrapper.find('.ui-table__cell--right').exists()).toBe(true)
|
|
661
|
+
})
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
describe('Column Width', () => {
|
|
665
|
+
it('applies custom width to column', () => {
|
|
666
|
+
const columns = [{ key: 'name', label: 'Name', width: '200px' }]
|
|
667
|
+
const wrapper = mount(DataTable, {
|
|
668
|
+
props: { columns, data: defaultData }
|
|
669
|
+
})
|
|
670
|
+
expect(wrapper.find('.ui-table__header-cell').attributes('style')).toContain('width: 200px')
|
|
671
|
+
})
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
describe('Slots', () => {
|
|
675
|
+
it('renders custom header slot', () => {
|
|
676
|
+
const columns = [{ key: 'name', label: 'Name' }]
|
|
677
|
+
const wrapper = mount(DataTable, {
|
|
678
|
+
props: { columns, data: defaultData },
|
|
679
|
+
slots: {
|
|
680
|
+
'header-name': '<span class="custom-header">Custom Name</span>'
|
|
681
|
+
}
|
|
682
|
+
})
|
|
683
|
+
expect(wrapper.find('.custom-header').exists()).toBe(true)
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
it('renders custom cell slot', () => {
|
|
687
|
+
const columns = [{ key: 'name', label: 'Name' }]
|
|
688
|
+
const wrapper = mount(DataTable, {
|
|
689
|
+
props: { columns, data: defaultData },
|
|
690
|
+
slots: {
|
|
691
|
+
'cell-name': '<span class="custom-cell">Custom</span>'
|
|
692
|
+
}
|
|
693
|
+
})
|
|
694
|
+
expect(wrapper.findAll('.custom-cell').length).toBe(3)
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
it('passes correct props to cell slot', () => {
|
|
698
|
+
const columns = [{ key: 'name', label: 'Name' }]
|
|
699
|
+
const wrapper = mount(DataTable, {
|
|
700
|
+
props: { columns, data: defaultData },
|
|
701
|
+
slots: {
|
|
702
|
+
'cell-name': `
|
|
703
|
+
<template #cell-name="{ value, row, column, index }">
|
|
704
|
+
<span class="slot-content" data-value="{{ value }}" data-index="{{ index }}">
|
|
705
|
+
{{ value }} - {{ index }}
|
|
706
|
+
</span>
|
|
707
|
+
</template>
|
|
708
|
+
`
|
|
709
|
+
}
|
|
710
|
+
})
|
|
711
|
+
expect(wrapper.find('.slot-content').exists()).toBe(true)
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
it('renders loading slot when provided', () => {
|
|
715
|
+
const wrapper = mount(DataTable, {
|
|
716
|
+
props: { columns: defaultColumns, data: defaultData, loading: true },
|
|
717
|
+
slots: {
|
|
718
|
+
loading: '<div class="custom-loading">Loading...</div>'
|
|
719
|
+
}
|
|
720
|
+
})
|
|
721
|
+
expect(wrapper.find('.ui-table__row--skeleton').exists()).toBe(true)
|
|
722
|
+
})
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
describe('Accessibility', () => {
|
|
726
|
+
it('has accessible select all checkbox', () => {
|
|
727
|
+
const wrapper = mount(DataTable, {
|
|
728
|
+
props: {
|
|
729
|
+
columns: defaultColumns,
|
|
730
|
+
data: defaultData,
|
|
731
|
+
selectable: true
|
|
732
|
+
}
|
|
733
|
+
})
|
|
734
|
+
const selectAllCheckbox = wrapper.findAllComponents({ name: 'Checkbox' })[0]
|
|
735
|
+
expect(selectAllCheckbox.attributes('aria-label')).toBe('Select all rows')
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
it('has accessible row checkboxes', () => {
|
|
739
|
+
const wrapper = mount(DataTable, {
|
|
740
|
+
props: {
|
|
741
|
+
columns: defaultColumns,
|
|
742
|
+
data: defaultData,
|
|
743
|
+
selectable: true
|
|
744
|
+
}
|
|
745
|
+
})
|
|
746
|
+
const rowCheckbox = wrapper.findAllComponents({ name: 'Checkbox' })[1]
|
|
747
|
+
expect(rowCheckbox.attributes('aria-label')).toBe('Select row 1')
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
it('sortable headers have role button', () => {
|
|
751
|
+
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
|
752
|
+
const wrapper = mount(DataTable, {
|
|
753
|
+
props: { columns, data: defaultData }
|
|
754
|
+
})
|
|
755
|
+
expect(wrapper.find('.ui-table__header-cell--sortable').attributes('role')).toBe('button')
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
it('sortable headers have tabindex', () => {
|
|
759
|
+
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
|
760
|
+
const wrapper = mount(DataTable, {
|
|
761
|
+
props: { columns, data: defaultData }
|
|
762
|
+
})
|
|
763
|
+
expect(wrapper.find('.ui-table__header-cell--sortable').attributes('tabindex')).toBe('0')
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
it('non-sortable headers do not have role', () => {
|
|
767
|
+
const wrapper = mount(DataTable, {
|
|
768
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
769
|
+
})
|
|
770
|
+
expect(wrapper.find('.ui-table__header-cell').attributes('role')).toBeUndefined()
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
it('cells have data-label for mobile', () => {
|
|
774
|
+
const wrapper = mount(DataTable, {
|
|
775
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
776
|
+
})
|
|
777
|
+
const cell = wrapper.find('.ui-table__cell')
|
|
778
|
+
expect(cell.attributes('data-label')).toBe('Name')
|
|
779
|
+
})
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
describe('Search', () => {
|
|
783
|
+
it('renders search input when searchable', () => {
|
|
784
|
+
const wrapper = mount(DataTable, {
|
|
785
|
+
props: {
|
|
786
|
+
columns: defaultColumns,
|
|
787
|
+
data: defaultData,
|
|
788
|
+
searchable: true
|
|
789
|
+
}
|
|
790
|
+
})
|
|
791
|
+
expect(wrapper.find('.ui-table__toolbar').exists()).toBe(true)
|
|
792
|
+
expect(wrapper.find('.ui-table__search').exists()).toBe(true)
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
it('does not render search input when not searchable', () => {
|
|
796
|
+
const wrapper = mount(DataTable, {
|
|
797
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
798
|
+
})
|
|
799
|
+
expect(wrapper.find('.ui-table__toolbar').exists()).toBe(false)
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
it('filters data based on search query', async () => {
|
|
803
|
+
const wrapper = mount(DataTable, {
|
|
804
|
+
props: {
|
|
805
|
+
columns: defaultColumns,
|
|
806
|
+
data: defaultData,
|
|
807
|
+
searchable: true
|
|
808
|
+
}
|
|
809
|
+
})
|
|
810
|
+
const input = wrapper.findComponent({ name: 'Input' })
|
|
811
|
+
await input.vm.$emit('update:modelValue', 'John')
|
|
812
|
+
const rows = wrapper.findAll('.ui-table__body .ui-table__row')
|
|
813
|
+
expect(rows.length).toBe(1)
|
|
814
|
+
expect(rows[0].text()).toContain('John Doe')
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
it('emits update:search when search changes', async () => {
|
|
818
|
+
const wrapper = mount(DataTable, {
|
|
819
|
+
props: {
|
|
820
|
+
columns: defaultColumns,
|
|
821
|
+
data: defaultData,
|
|
822
|
+
searchable: true
|
|
823
|
+
}
|
|
824
|
+
})
|
|
825
|
+
const input = wrapper.findComponent({ name: 'Input' })
|
|
826
|
+
await input.vm.$emit('update:modelValue', 'test')
|
|
827
|
+
expect(wrapper.emitted('update:search')).toBeTruthy()
|
|
828
|
+
expect(wrapper.emitted('update:search')![0]).toEqual(['test'])
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
it('emits filter-change when search changes', async () => {
|
|
832
|
+
const wrapper = mount(DataTable, {
|
|
833
|
+
props: {
|
|
834
|
+
columns: defaultColumns,
|
|
835
|
+
data: defaultData,
|
|
836
|
+
searchable: true
|
|
837
|
+
}
|
|
838
|
+
})
|
|
839
|
+
const input = wrapper.findComponent({ name: 'Input' })
|
|
840
|
+
await input.vm.$emit('update:modelValue', 'test')
|
|
841
|
+
expect(wrapper.emitted('filter-change')).toBeTruthy()
|
|
842
|
+
expect(wrapper.emitted('filter-change')![0]).toEqual([{ search: 'test', filters: {} }])
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
it('shows no matching results message when search has no matches', async () => {
|
|
846
|
+
const wrapper = mount(DataTable, {
|
|
847
|
+
props: {
|
|
848
|
+
columns: defaultColumns,
|
|
849
|
+
data: defaultData,
|
|
850
|
+
searchable: true
|
|
851
|
+
}
|
|
852
|
+
})
|
|
853
|
+
const input = wrapper.findComponent({ name: 'Input' })
|
|
854
|
+
await input.vm.$emit('update:modelValue', 'xyz-no-match')
|
|
855
|
+
expect(wrapper.find('.ui-table__row--empty').exists()).toBe(true)
|
|
856
|
+
const emptyState = wrapper.findComponent({ name: 'EmptyState' })
|
|
857
|
+
expect(emptyState.props('title')).toBe('No matching results')
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
it('resets page to 1 when search changes', async () => {
|
|
861
|
+
const wrapper = mount(DataTable, {
|
|
862
|
+
props: {
|
|
863
|
+
columns: defaultColumns,
|
|
864
|
+
data: [...defaultData, ...defaultData, ...defaultData, ...defaultData],
|
|
865
|
+
searchable: true,
|
|
866
|
+
pagination: true,
|
|
867
|
+
pageSize: 2,
|
|
868
|
+
page: 2
|
|
869
|
+
}
|
|
870
|
+
})
|
|
871
|
+
const input = wrapper.findComponent({ name: 'Input' })
|
|
872
|
+
await input.vm.$emit('update:modelValue', 'John')
|
|
873
|
+
expect(wrapper.emitted('update:page')).toBeTruthy()
|
|
874
|
+
expect(wrapper.emitted('update:page')![0]).toEqual([1])
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
it('searches across all columns', async () => {
|
|
878
|
+
const wrapper = mount(DataTable, {
|
|
879
|
+
props: {
|
|
880
|
+
columns: defaultColumns,
|
|
881
|
+
data: defaultData,
|
|
882
|
+
searchable: true
|
|
883
|
+
}
|
|
884
|
+
})
|
|
885
|
+
const input = wrapper.findComponent({ name: 'Input' })
|
|
886
|
+
await input.vm.$emit('update:modelValue', 'Admin')
|
|
887
|
+
const rows = wrapper.findAll('.ui-table__body .ui-table__row')
|
|
888
|
+
expect(rows.length).toBe(1)
|
|
889
|
+
expect(rows[0].text()).toContain('John Doe')
|
|
890
|
+
})
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
describe('Column Filters', () => {
|
|
894
|
+
it('renders filter row when filterable', () => {
|
|
895
|
+
const wrapper = mount(DataTable, {
|
|
896
|
+
props: {
|
|
897
|
+
columns: defaultColumns,
|
|
898
|
+
data: defaultData,
|
|
899
|
+
filterable: true
|
|
900
|
+
}
|
|
901
|
+
})
|
|
902
|
+
expect(wrapper.find('.ui-table__filter-row').exists()).toBe(true)
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
it('does not render filter row when not filterable', () => {
|
|
906
|
+
const wrapper = mount(DataTable, {
|
|
907
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
908
|
+
})
|
|
909
|
+
expect(wrapper.find('.ui-table__filter-row').exists()).toBe(false)
|
|
910
|
+
})
|
|
911
|
+
|
|
912
|
+
it('renders filter input for each column', () => {
|
|
913
|
+
const wrapper = mount(DataTable, {
|
|
914
|
+
props: {
|
|
915
|
+
columns: defaultColumns,
|
|
916
|
+
data: defaultData,
|
|
917
|
+
filterable: true
|
|
918
|
+
}
|
|
919
|
+
})
|
|
920
|
+
const filterInputs = wrapper.findAll('.ui-table__filter-cell input')
|
|
921
|
+
expect(filterInputs.length).toBe(3)
|
|
922
|
+
})
|
|
923
|
+
|
|
924
|
+
it('filters data based on column filter', async () => {
|
|
925
|
+
const wrapper = mount(DataTable, {
|
|
926
|
+
props: {
|
|
927
|
+
columns: defaultColumns,
|
|
928
|
+
data: defaultData,
|
|
929
|
+
filterable: true,
|
|
930
|
+
filters: { name: 'John' }
|
|
931
|
+
}
|
|
932
|
+
})
|
|
933
|
+
const rows = wrapper.findAll('.ui-table__body .ui-table__row')
|
|
934
|
+
expect(rows.length).toBe(1)
|
|
935
|
+
expect(rows[0].text()).toContain('John Doe')
|
|
936
|
+
})
|
|
937
|
+
|
|
938
|
+
it('emits update:filters when filter changes', async () => {
|
|
939
|
+
const wrapper = mount(DataTable, {
|
|
940
|
+
props: {
|
|
941
|
+
columns: defaultColumns,
|
|
942
|
+
data: defaultData,
|
|
943
|
+
filterable: true,
|
|
944
|
+
filters: {}
|
|
945
|
+
}
|
|
946
|
+
})
|
|
947
|
+
const filterInput = wrapper.findAllComponents({ name: 'Input' })[0]
|
|
948
|
+
await filterInput.vm.$emit('update:modelValue', 'test')
|
|
949
|
+
expect(wrapper.emitted('update:filters')).toBeTruthy()
|
|
950
|
+
expect(wrapper.emitted('update:filters')![0]).toEqual([{ name: 'test' }])
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
it('applies multiple column filters', async () => {
|
|
954
|
+
const wrapper = mount(DataTable, {
|
|
955
|
+
props: {
|
|
956
|
+
columns: defaultColumns,
|
|
957
|
+
data: defaultData,
|
|
958
|
+
filterable: true,
|
|
959
|
+
filters: { name: 'Jo', role: 'Admin' }
|
|
960
|
+
}
|
|
961
|
+
})
|
|
962
|
+
const rows = wrapper.findAll('.ui-table__body .ui-table__row')
|
|
963
|
+
expect(rows.length).toBe(1)
|
|
964
|
+
})
|
|
965
|
+
|
|
966
|
+
it('excludes columns with filterable: false', () => {
|
|
967
|
+
const columns = [
|
|
968
|
+
{ key: 'name', label: 'Name' },
|
|
969
|
+
{ key: 'email', label: 'Email', filterable: false },
|
|
970
|
+
{ key: 'role', label: 'Role' }
|
|
971
|
+
]
|
|
972
|
+
const wrapper = mount(DataTable, {
|
|
973
|
+
props: {
|
|
974
|
+
columns,
|
|
975
|
+
data: defaultData,
|
|
976
|
+
filterable: true
|
|
977
|
+
}
|
|
978
|
+
})
|
|
979
|
+
const filterCells = wrapper.findAll('.ui-table__filter-cell')
|
|
980
|
+
const filterInputs = wrapper.findAll('.ui-table__filter-cell input')
|
|
981
|
+
expect(filterCells.length).toBe(3)
|
|
982
|
+
expect(filterInputs.length).toBe(2)
|
|
983
|
+
})
|
|
984
|
+
})
|
|
985
|
+
|
|
986
|
+
describe('Column Resizing', () => {
|
|
987
|
+
it('renders resize handles when resizable', () => {
|
|
988
|
+
const wrapper = mount(DataTable, {
|
|
989
|
+
props: {
|
|
990
|
+
columns: defaultColumns,
|
|
991
|
+
data: defaultData,
|
|
992
|
+
resizable: true
|
|
993
|
+
}
|
|
994
|
+
})
|
|
995
|
+
expect(wrapper.find('.ui-table__resize-handle').exists()).toBe(true)
|
|
996
|
+
})
|
|
997
|
+
|
|
998
|
+
it('does not render resize handles when not resizable', () => {
|
|
999
|
+
const wrapper = mount(DataTable, {
|
|
1000
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
1001
|
+
})
|
|
1002
|
+
expect(wrapper.find('.ui-table__resize-handle').exists()).toBe(false)
|
|
1003
|
+
})
|
|
1004
|
+
|
|
1005
|
+
it('applies resizable class to table', () => {
|
|
1006
|
+
const wrapper = mount(DataTable, {
|
|
1007
|
+
props: {
|
|
1008
|
+
columns: defaultColumns,
|
|
1009
|
+
data: defaultData,
|
|
1010
|
+
resizable: true
|
|
1011
|
+
}
|
|
1012
|
+
})
|
|
1013
|
+
expect(wrapper.find('.ui-table--resizable').exists()).toBe(true)
|
|
1014
|
+
})
|
|
1015
|
+
|
|
1016
|
+
it('applies resizable class to header cells', () => {
|
|
1017
|
+
const wrapper = mount(DataTable, {
|
|
1018
|
+
props: {
|
|
1019
|
+
columns: defaultColumns,
|
|
1020
|
+
data: defaultData,
|
|
1021
|
+
resizable: true
|
|
1022
|
+
}
|
|
1023
|
+
})
|
|
1024
|
+
expect(wrapper.find('.ui-table__header-cell--resizable').exists()).toBe(true)
|
|
1025
|
+
})
|
|
1026
|
+
|
|
1027
|
+
it('excludes resize handle from last column', () => {
|
|
1028
|
+
const wrapper = mount(DataTable, {
|
|
1029
|
+
props: {
|
|
1030
|
+
columns: defaultColumns,
|
|
1031
|
+
data: defaultData,
|
|
1032
|
+
resizable: true
|
|
1033
|
+
}
|
|
1034
|
+
})
|
|
1035
|
+
const handles = wrapper.findAll('.ui-table__resize-handle')
|
|
1036
|
+
expect(handles.length).toBe(2)
|
|
1037
|
+
})
|
|
1038
|
+
|
|
1039
|
+
it('excludes column with resizable: false', () => {
|
|
1040
|
+
const columns = [
|
|
1041
|
+
{ key: 'name', label: 'Name', resizable: false },
|
|
1042
|
+
{ key: 'email', label: 'Email' },
|
|
1043
|
+
{ key: 'role', label: 'Role' }
|
|
1044
|
+
]
|
|
1045
|
+
const wrapper = mount(DataTable, {
|
|
1046
|
+
props: {
|
|
1047
|
+
columns,
|
|
1048
|
+
data: defaultData,
|
|
1049
|
+
resizable: true
|
|
1050
|
+
}
|
|
1051
|
+
})
|
|
1052
|
+
const resizableHeaders = wrapper.findAll('.ui-table__header-cell--resizable')
|
|
1053
|
+
expect(resizableHeaders.length).toBe(2)
|
|
1054
|
+
})
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
describe('Expandable Rows', () => {
|
|
1058
|
+
it('renders expand column when expandable', () => {
|
|
1059
|
+
const wrapper = mount(DataTable, {
|
|
1060
|
+
props: {
|
|
1061
|
+
columns: defaultColumns,
|
|
1062
|
+
data: defaultData,
|
|
1063
|
+
expandable: true
|
|
1064
|
+
}
|
|
1065
|
+
})
|
|
1066
|
+
expect(wrapper.find('.ui-table__header-cell--expand').exists()).toBe(true)
|
|
1067
|
+
expect(wrapper.find('.ui-table__cell--expand').exists()).toBe(true)
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
it('does not render expand column when not expandable', () => {
|
|
1071
|
+
const wrapper = mount(DataTable, {
|
|
1072
|
+
props: { columns: defaultColumns, data: defaultData }
|
|
1073
|
+
})
|
|
1074
|
+
expect(wrapper.find('.ui-table__header-cell--expand').exists()).toBe(false)
|
|
1075
|
+
})
|
|
1076
|
+
|
|
1077
|
+
it('renders expand button in each row', () => {
|
|
1078
|
+
const wrapper = mount(DataTable, {
|
|
1079
|
+
props: {
|
|
1080
|
+
columns: defaultColumns,
|
|
1081
|
+
data: defaultData,
|
|
1082
|
+
expandable: true
|
|
1083
|
+
}
|
|
1084
|
+
})
|
|
1085
|
+
const expandButtons = wrapper.findAll('.ui-table__expand-button')
|
|
1086
|
+
expect(expandButtons.length).toBe(3)
|
|
1087
|
+
})
|
|
1088
|
+
|
|
1089
|
+
it('emits update:expandedIds when expand button is clicked', async () => {
|
|
1090
|
+
const wrapper = mount(DataTable, {
|
|
1091
|
+
props: {
|
|
1092
|
+
columns: defaultColumns,
|
|
1093
|
+
data: defaultData,
|
|
1094
|
+
expandable: true,
|
|
1095
|
+
expandedIds: []
|
|
1096
|
+
}
|
|
1097
|
+
})
|
|
1098
|
+
await wrapper.find('.ui-table__expand-button').trigger('click')
|
|
1099
|
+
expect(wrapper.emitted('update:expandedIds')).toBeTruthy()
|
|
1100
|
+
expect(wrapper.emitted('update:expandedIds')![0]).toEqual([[1]])
|
|
1101
|
+
})
|
|
1102
|
+
|
|
1103
|
+
it('emits row-expand when row is expanded', async () => {
|
|
1104
|
+
const wrapper = mount(DataTable, {
|
|
1105
|
+
props: {
|
|
1106
|
+
columns: defaultColumns,
|
|
1107
|
+
data: defaultData,
|
|
1108
|
+
expandable: true,
|
|
1109
|
+
expandedIds: []
|
|
1110
|
+
}
|
|
1111
|
+
})
|
|
1112
|
+
await wrapper.find('.ui-table__expand-button').trigger('click')
|
|
1113
|
+
expect(wrapper.emitted('row-expand')).toBeTruthy()
|
|
1114
|
+
expect(wrapper.emitted('row-expand')![0]).toEqual([defaultData[0], true])
|
|
1115
|
+
})
|
|
1116
|
+
|
|
1117
|
+
it('shows expansion row when row is expanded', () => {
|
|
1118
|
+
const wrapper = mount(DataTable, {
|
|
1119
|
+
props: {
|
|
1120
|
+
columns: defaultColumns,
|
|
1121
|
+
data: defaultData,
|
|
1122
|
+
expandable: true,
|
|
1123
|
+
expandedIds: [1]
|
|
1124
|
+
}
|
|
1125
|
+
})
|
|
1126
|
+
expect(wrapper.find('.ui-table__row--expansion').exists()).toBe(true)
|
|
1127
|
+
})
|
|
1128
|
+
|
|
1129
|
+
it('hides expansion row when row is collapsed', () => {
|
|
1130
|
+
const wrapper = mount(DataTable, {
|
|
1131
|
+
props: {
|
|
1132
|
+
columns: defaultColumns,
|
|
1133
|
+
data: defaultData,
|
|
1134
|
+
expandable: true,
|
|
1135
|
+
expandedIds: []
|
|
1136
|
+
}
|
|
1137
|
+
})
|
|
1138
|
+
expect(wrapper.find('.ui-table__row--expansion').exists()).toBe(false)
|
|
1139
|
+
})
|
|
1140
|
+
|
|
1141
|
+
it('applies expanded class to expanded row', () => {
|
|
1142
|
+
const wrapper = mount(DataTable, {
|
|
1143
|
+
props: {
|
|
1144
|
+
columns: defaultColumns,
|
|
1145
|
+
data: defaultData,
|
|
1146
|
+
expandable: true,
|
|
1147
|
+
expandedIds: [1]
|
|
1148
|
+
}
|
|
1149
|
+
})
|
|
1150
|
+
expect(wrapper.find('.ui-table__row--expanded').exists()).toBe(true)
|
|
1151
|
+
})
|
|
1152
|
+
|
|
1153
|
+
it('renders expanded slot content', () => {
|
|
1154
|
+
const wrapper = mount(DataTable, {
|
|
1155
|
+
props: {
|
|
1156
|
+
columns: defaultColumns,
|
|
1157
|
+
data: defaultData,
|
|
1158
|
+
expandable: true,
|
|
1159
|
+
expandedIds: [1]
|
|
1160
|
+
},
|
|
1161
|
+
slots: {
|
|
1162
|
+
expanded: '<div class="custom-expanded">Expanded content</div>'
|
|
1163
|
+
}
|
|
1164
|
+
})
|
|
1165
|
+
expect(wrapper.find('.custom-expanded').exists()).toBe(true)
|
|
1166
|
+
})
|
|
1167
|
+
|
|
1168
|
+
it('collapses expanded row when button is clicked again', async () => {
|
|
1169
|
+
const wrapper = mount(DataTable, {
|
|
1170
|
+
props: {
|
|
1171
|
+
columns: defaultColumns,
|
|
1172
|
+
data: defaultData,
|
|
1173
|
+
expandable: true,
|
|
1174
|
+
expandedIds: [1]
|
|
1175
|
+
}
|
|
1176
|
+
})
|
|
1177
|
+
await wrapper.find('.ui-table__expand-button').trigger('click')
|
|
1178
|
+
expect(wrapper.emitted('update:expandedIds')![0]).toEqual([[]])
|
|
1179
|
+
})
|
|
1180
|
+
|
|
1181
|
+
it('has accessible expand button', () => {
|
|
1182
|
+
const wrapper = mount(DataTable, {
|
|
1183
|
+
props: {
|
|
1184
|
+
columns: defaultColumns,
|
|
1185
|
+
data: defaultData,
|
|
1186
|
+
expandable: true,
|
|
1187
|
+
expandedIds: []
|
|
1188
|
+
}
|
|
1189
|
+
})
|
|
1190
|
+
const expandButton = wrapper.find('.ui-table__expand-button')
|
|
1191
|
+
expect(expandButton.attributes('aria-expanded')).toBe('false')
|
|
1192
|
+
expect(expandButton.attributes('aria-label')).toBe('Expand row')
|
|
1193
|
+
})
|
|
1194
|
+
|
|
1195
|
+
it('updates aria-expanded when row is expanded', () => {
|
|
1196
|
+
const wrapper = mount(DataTable, {
|
|
1197
|
+
props: {
|
|
1198
|
+
columns: defaultColumns,
|
|
1199
|
+
data: defaultData,
|
|
1200
|
+
expandable: true,
|
|
1201
|
+
expandedIds: [1]
|
|
1202
|
+
}
|
|
1203
|
+
})
|
|
1204
|
+
const expandButton = wrapper.find('.ui-table__expand-button')
|
|
1205
|
+
expect(expandButton.attributes('aria-expanded')).toBe('true')
|
|
1206
|
+
expect(expandButton.attributes('aria-label')).toBe('Collapse row')
|
|
1207
|
+
})
|
|
1208
|
+
|
|
1209
|
+
it('counts expand column in total columns', () => {
|
|
1210
|
+
const wrapper = mount(DataTable, {
|
|
1211
|
+
props: {
|
|
1212
|
+
columns: defaultColumns,
|
|
1213
|
+
data: [],
|
|
1214
|
+
expandable: true
|
|
1215
|
+
}
|
|
1216
|
+
})
|
|
1217
|
+
const emptyCell = wrapper.find('.ui-table__cell--empty')
|
|
1218
|
+
expect(emptyCell.attributes('colspan')).toBe('4')
|
|
1219
|
+
})
|
|
1220
|
+
})
|
|
1221
|
+
})
|