CommonSimpleTable.spec.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { waitFor, within } from '@testing-library/vue'
  3. import { vi } from 'vitest'
  4. import { ref } from 'vue'
  5. import {
  6. type ExtendedMountingOptions,
  7. renderComponent,
  8. } from '#tests/support/components/index.ts'
  9. import { i18n } from '#shared/i18n.ts'
  10. import type { MenuItem } from '#desktop/components/CommonPopoverMenu/types.ts'
  11. import CommonSimpleTable, { type Props } from '../CommonSimpleTable.vue'
  12. const tableHeaders = [
  13. {
  14. key: 'name',
  15. label: 'User name',
  16. },
  17. {
  18. key: 'role',
  19. label: 'Role',
  20. },
  21. ]
  22. const tableItems = [
  23. {
  24. id: 1,
  25. name: 'Lindsay Walton',
  26. role: 'Member',
  27. },
  28. ]
  29. const tableActions: MenuItem[] = [
  30. {
  31. key: 'download',
  32. label: 'Download this row',
  33. icon: 'download',
  34. },
  35. {
  36. key: 'delete',
  37. label: 'Delete this row',
  38. icon: 'trash3',
  39. },
  40. ]
  41. const renderTable = (
  42. props: Props,
  43. options: ExtendedMountingOptions<Props> = { form: true },
  44. ) => {
  45. return renderComponent(CommonSimpleTable, {
  46. ...options,
  47. props,
  48. })
  49. }
  50. beforeEach(() => {
  51. i18n.setTranslationMap(new Map([['Role', 'Rolle']]))
  52. })
  53. describe('CommonSimpleTable', () => {
  54. it('displays the table without actions', async () => {
  55. const wrapper = renderTable({
  56. headers: tableHeaders,
  57. items: tableItems,
  58. })
  59. expect(wrapper.getByText('User name')).toBeInTheDocument()
  60. expect(wrapper.getByText('Rolle')).toBeInTheDocument()
  61. expect(wrapper.getByText('Lindsay Walton')).toBeInTheDocument()
  62. expect(wrapper.getByText('Member')).toBeInTheDocument()
  63. expect(wrapper.queryByText('Actions')).toBeNull()
  64. })
  65. it('displays the table with actions', async () => {
  66. const wrapper = renderTable(
  67. {
  68. headers: tableHeaders,
  69. items: tableItems,
  70. actions: tableActions,
  71. },
  72. { router: true },
  73. )
  74. expect(wrapper.getByText('Actions')).toBeInTheDocument()
  75. expect(wrapper.getByLabelText('Action menu button')).toBeInTheDocument()
  76. })
  77. it('displays the additional data with the item suffix slot', async () => {
  78. const wrapper = renderTable(
  79. {
  80. headers: tableHeaders,
  81. items: tableItems,
  82. actions: tableActions,
  83. },
  84. {
  85. router: true,
  86. slots: {
  87. 'item-suffix-role': '<span>Additional Example</span>',
  88. },
  89. },
  90. )
  91. expect(wrapper.getByText('Additional Example')).toBeInTheDocument()
  92. })
  93. it('generates expected DOM', async () => {
  94. // TODO: check if such snapshot test is really the way we want to go.
  95. const view = renderTable(
  96. {
  97. headers: tableHeaders,
  98. items: tableItems,
  99. actions: tableActions,
  100. },
  101. // NB: Please don't remove this, otherwise snapshot would contain markup of many more components other than the
  102. // one under the test, which can lead to false positives.
  103. {
  104. shallow: true,
  105. },
  106. )
  107. expect(view.baseElement.querySelector('table')).toMatchFileSnapshot(
  108. `${__filename}.snapshot.txt`,
  109. )
  110. })
  111. it('supports text truncation in cell content', async () => {
  112. const wrapper = renderTable({
  113. headers: [
  114. ...tableHeaders,
  115. {
  116. key: 'truncated',
  117. label: 'Truncated',
  118. truncate: true,
  119. },
  120. ],
  121. items: [
  122. ...tableItems,
  123. {
  124. id: 2,
  125. name: 'Max Mustermann',
  126. role: 'Admin',
  127. truncated: 'Some text to be truncated',
  128. },
  129. ],
  130. })
  131. const truncatedText = wrapper.getByText('Some text to be truncated')
  132. expect(truncatedText.parentElement).toHaveClass('truncate')
  133. })
  134. it('supports tooltip on truncated cell content', async () => {
  135. const wrapper = renderTable({
  136. headers: [
  137. ...tableHeaders,
  138. {
  139. key: 'truncated',
  140. label: 'Truncated',
  141. truncate: true,
  142. },
  143. ],
  144. items: [
  145. ...tableItems,
  146. {
  147. id: 2,
  148. name: 'Max Mustermann',
  149. role: 'Admin',
  150. truncated: 'Some text to be truncated',
  151. },
  152. ],
  153. })
  154. await wrapper.events.hover(wrapper.getByText('Max Mustermann'))
  155. await waitFor(() => {
  156. expect(wrapper.getByText('Some text to be truncated')).toBeInTheDocument()
  157. expect(
  158. wrapper.getByLabelText('Some text to be truncated'),
  159. ).toBeInTheDocument()
  160. })
  161. })
  162. it('supports header slot', () => {
  163. const wrapper = renderTable(
  164. {
  165. headers: tableHeaders,
  166. items: tableItems,
  167. actions: tableActions,
  168. },
  169. {
  170. slots: {
  171. 'column-header-name': '<div>Custom header</div>',
  172. },
  173. },
  174. )
  175. expect(wrapper.getByText('Custom header')).toBeInTheDocument()
  176. })
  177. it('supports listening for row click events', async () => {
  178. const mockedCallback = vi.fn()
  179. const item = tableItems[0]
  180. const wrapper = renderComponent({
  181. components: { CommonSimpleTable },
  182. setup() {
  183. return {
  184. mockedCallback,
  185. tableHeaders,
  186. items: [item],
  187. }
  188. },
  189. template: `<CommonSimpleTable @click-row="mockedCallback" :headers="tableHeaders" :items="items"/>`,
  190. })
  191. expect(
  192. wrapper.getByRole('button', { name: 'Select table row' }),
  193. ).toBeInTheDocument()
  194. await wrapper.events.click(wrapper.getByText('Lindsay Walton'))
  195. expect(mockedCallback).toHaveBeenCalledWith(item)
  196. wrapper.getByRole('button', { name: 'Select table row' }).focus()
  197. await wrapper.events.keyboard('{enter}')
  198. expect(mockedCallback).toHaveBeenCalledWith(item)
  199. })
  200. it('supports marking row in active color', () => {
  201. const wrapper = renderTable({
  202. headers: [
  203. ...tableHeaders,
  204. {
  205. key: 'name',
  206. label: 'name',
  207. },
  208. ],
  209. selectedRowId: '2',
  210. items: [
  211. {
  212. id: '2',
  213. name: 'foo',
  214. },
  215. ],
  216. })
  217. const row = wrapper.getByTestId('simple-table-row')
  218. expect(row).toHaveClass('!bg-blue-800')
  219. })
  220. it('supports marking row in active color', () => {
  221. const wrapper = renderTable({
  222. headers: [
  223. {
  224. key: 'name',
  225. label: 'name',
  226. },
  227. ],
  228. selectedRowId: '2',
  229. items: [
  230. {
  231. id: '2',
  232. name: 'foo cell',
  233. },
  234. ],
  235. })
  236. const row = wrapper.getByTestId('simple-table-row')
  237. expect(row).toHaveClass('!bg-blue-800')
  238. expect(within(row).getByText('foo cell')).toHaveClass(
  239. 'text-black dark:text-white',
  240. )
  241. })
  242. it('supports adding class to table header', () => {
  243. const wrapper = renderTable({
  244. headers: [
  245. {
  246. key: 'name',
  247. label: 'Awesome Cell Header',
  248. labelClass: 'text-red-500 font-bold',
  249. },
  250. ],
  251. items: [
  252. {
  253. id: '2',
  254. name: 'foo cell',
  255. },
  256. ],
  257. })
  258. expect(wrapper.getByText('Awesome Cell Header')).toHaveClass(
  259. 'text-red-500 font-bold',
  260. )
  261. })
  262. it('supports adding a link to a cell', () => {
  263. const wrapper = renderTable(
  264. {
  265. headers: [
  266. {
  267. key: 'urlTest',
  268. label: 'Link Row',
  269. type: 'link',
  270. },
  271. ],
  272. items: [
  273. {
  274. id: 1,
  275. urlTest: {
  276. label: 'Example',
  277. link: 'https://example.com',
  278. openInNewTab: true,
  279. external: true,
  280. },
  281. },
  282. ],
  283. },
  284. { router: true },
  285. )
  286. const linkCell = wrapper.getByRole('link')
  287. expect(linkCell).toHaveTextContent('Example')
  288. expect(linkCell).toHaveAttribute('href', 'https://example.com')
  289. expect(linkCell).toHaveAttribute('target', '_blank')
  290. })
  291. it('adds hover a')
  292. it('supports row selection', async () => {
  293. const checkedRows = ref([])
  294. const items = [
  295. {
  296. id: 1,
  297. label: 'selection data 1',
  298. },
  299. {
  300. id: 2,
  301. label: 'selection data 2',
  302. },
  303. ]
  304. const wrapper = renderTable(
  305. {
  306. headers: [
  307. {
  308. key: 'urlTest',
  309. label: 'Link Row',
  310. },
  311. ],
  312. items,
  313. hasCheckboxColumn: true,
  314. },
  315. { form: true, vModel: { checkedRows } },
  316. )
  317. expect(wrapper.getAllByRole('checkbox')).toHaveLength(3)
  318. const selectAllCheckbox = wrapper.getByLabelText('Select all entries')
  319. expect(selectAllCheckbox).not.toHaveAttribute('checked')
  320. const rowCheckboxes = wrapper.getAllByRole('checkbox', {
  321. name: 'Select this entry',
  322. })
  323. await wrapper.events.click(rowCheckboxes[0])
  324. expect(rowCheckboxes[0]).toHaveAttribute('checked')
  325. await wrapper.events.click(rowCheckboxes[1])
  326. await waitFor(() => expect(checkedRows.value).toEqual(items))
  327. await waitFor(() => expect(selectAllCheckbox).toHaveAttribute('checked'))
  328. await wrapper.events.click(wrapper.getByLabelText('Deselect all entries'))
  329. await waitFor(() => expect(rowCheckboxes[0]).not.toHaveAttribute('checked'))
  330. expect(rowCheckboxes[1]).not.toHaveAttribute('checked')
  331. await wrapper.events.click(rowCheckboxes[1])
  332. expect(
  333. await wrapper.findByLabelText('Deselect this entry'),
  334. ).toBeInTheDocument()
  335. })
  336. it('supports disabling checkbox item for specific rows', async () => {
  337. const checkedRows = ref([])
  338. const items = [
  339. {
  340. id: 1,
  341. checked: false,
  342. disabled: true,
  343. label: 'selection data 1',
  344. },
  345. {
  346. id: 1,
  347. checked: true,
  348. disabled: true,
  349. label: 'selection data 1',
  350. },
  351. ]
  352. const wrapper = renderTable(
  353. {
  354. headers: [
  355. {
  356. key: 'urlTest',
  357. label: 'Link Row',
  358. },
  359. ],
  360. items,
  361. hasCheckboxColumn: true,
  362. },
  363. { form: true, vModel: { checkedRows } },
  364. )
  365. const checkboxes = wrapper.getAllByRole('checkbox')
  366. expect(checkboxes).toHaveLength(3)
  367. expect(checkboxes[1]).toBeDisabled()
  368. expect(checkboxes[1]).not.toBeChecked()
  369. expect(checkboxes[2]).toHaveAttribute('value', 'true')
  370. await wrapper.events.click(checkboxes[1])
  371. expect(checkedRows.value).toEqual([])
  372. await wrapper.events.click(checkboxes[0])
  373. expect(checkedRows.value).toEqual([])
  374. })
  375. })