UserTaskbarTabs.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { getByRole, queryByRole } from '@testing-library/vue'
  3. import { type RouteRecordRaw } from 'vue-router'
  4. import {
  5. getAllByIconName,
  6. getByIconName,
  7. } from '#tests/support/components/iconQueries.ts'
  8. import { renderComponent } from '#tests/support/components/index.ts'
  9. import { getTestRouter } from '#tests/support/components/renderComponent.ts'
  10. import { mockApplicationConfig } from '#tests/support/mock-applicationConfig.ts'
  11. import { waitForNextTick } from '#tests/support/utils.ts'
  12. import {
  13. EnumTaskbarEntity,
  14. EnumTaskbarEntityAccess,
  15. EnumTicketStateColorCode,
  16. } from '#shared/graphql/types.ts'
  17. import { convertToGraphQLId } from '#shared/graphql/utils.ts'
  18. import { waitForUserCurrentTaskbarItemDeleteMutationCalls } from '#desktop/entities/user/current/graphql/mutations/userCurrentTaskbarItemDelete.mocks.ts'
  19. import {
  20. mockUserCurrentTaskbarItemListQuery,
  21. waitForUserCurrentTaskbarItemListQueryCalls,
  22. } from '#desktop/entities/user/current/graphql/queries/userCurrentTaskbarItemList.mocks.ts'
  23. import { getUserCurrentTaskbarItemListUpdatesSubscriptionHandler } from '#desktop/entities/user/current/graphql/subscriptions/userCurrentTaskbarItemListUpdates.mocks.ts'
  24. import { getUserCurrentTaskbarItemUpdatesSubscriptionHandler } from '#desktop/entities/user/current/graphql/subscriptions/userCurrentTaskbarItemUpdates.mocks.ts'
  25. import UserTaskbarTabs, { type Props } from '../UserTaskbarTabs.vue'
  26. import '#tests/graphql/builders/mocks.ts'
  27. const waitForVariantConfirmationMock = vi
  28. .fn()
  29. .mockImplementation((variant) => variant === 'unsaved')
  30. vi.mock('#shared/composables/useConfirmation.ts', async () => ({
  31. useConfirmation: () => ({
  32. waitForVariantConfirmation: waitForVariantConfirmationMock,
  33. }),
  34. }))
  35. const renderUserTaskbarTabs = async (
  36. options: {
  37. props?: Partial<Props>
  38. routerRoutes?: RouteRecordRaw[]
  39. } = {},
  40. ) => {
  41. const wrapper = renderComponent(UserTaskbarTabs, {
  42. router: true,
  43. store: true,
  44. dialog: true,
  45. ...options,
  46. })
  47. await waitForUserCurrentTaskbarItemListQueryCalls()
  48. await waitForNextTick()
  49. return wrapper
  50. }
  51. describe('UserTaskbarTabs.vue', () => {
  52. beforeAll(() => {
  53. mockApplicationConfig({
  54. ticket_hook: 'Ticket#',
  55. ui_task_mananger_max_task_count: 30,
  56. })
  57. })
  58. it.each([{ collapsed: false }, { collapsed: true }])(
  59. 'does not render anything in case taskbar is empty (collapsed: $collapsed)',
  60. async ({ collapsed }) => {
  61. mockUserCurrentTaskbarItemListQuery({
  62. userCurrentTaskbarItemList: [],
  63. })
  64. const wrapper = await renderUserTaskbarTabs({
  65. props: {
  66. collapsed,
  67. },
  68. })
  69. expect(wrapper.queryByText('Tabs')).not.toBeInTheDocument()
  70. expect(
  71. wrapper.queryByRole('button', {
  72. name: 'List of all user taskbar tabs',
  73. }),
  74. ).not.toBeInTheDocument()
  75. },
  76. )
  77. it('renders ticket tab', async () => {
  78. // Rely on the default ticket tab from the `UserTaskbarItem` factory.
  79. const wrapper = await renderUserTaskbarTabs({
  80. routerRoutes: [
  81. {
  82. path: '/',
  83. name: 'Main',
  84. component: { template: '<div></div>' },
  85. },
  86. {
  87. path: '/tickets/1',
  88. name: 'Test',
  89. component: { template: '<div></div>' },
  90. },
  91. ],
  92. })
  93. expect(wrapper.getByText('Tabs')).toBeInTheDocument()
  94. const tab = wrapper.getByRole('listitem')
  95. expect(getByIconName(tab, 'check-circle-no')).toBeInTheDocument()
  96. expect(tab).toHaveTextContent('Welcome to Zammad!')
  97. expect(tab).toHaveAccessibleDescription(
  98. 'Drag and drop to reorder your tabs.',
  99. )
  100. const link = getByRole(tab, 'link')
  101. expect(link).toHaveAttribute('href', '/desktop/tickets/1')
  102. expect(link).toHaveAccessibleName('Ticket#53001 - Welcome to Zammad!')
  103. expect(link).not.toHaveClass('!bg-yellow-500')
  104. // Test active background based on the open state.
  105. const router = getTestRouter()
  106. await router.push('/tickets/1')
  107. expect(link).toHaveClass('!bg-yellow-500')
  108. expect(
  109. getByRole(tab, 'button', { name: 'Close this tab' }),
  110. ).toBeInTheDocument()
  111. })
  112. it('renders ticket create tab', async () => {
  113. mockUserCurrentTaskbarItemListQuery({
  114. userCurrentTaskbarItemList: [
  115. {
  116. __typename: 'UserTaskbarItem',
  117. id: convertToGraphQLId('Taskbar', 1),
  118. key: 'TicketCreateScreen-999',
  119. callback: EnumTaskbarEntity.TicketCreate,
  120. entityAccess: EnumTaskbarEntityAccess.Granted,
  121. entity: {
  122. __typename: 'UserTaskbarItemEntityTicketCreate',
  123. uid: '999',
  124. title: 'Test title',
  125. createArticleTypeKey: 'phone-in',
  126. },
  127. },
  128. ],
  129. })
  130. const wrapper = await renderUserTaskbarTabs()
  131. expect(wrapper.getByText('Tabs')).toBeInTheDocument()
  132. const tab = wrapper.getByRole('listitem')
  133. expect(getByIconName(tab, 'pencil')).toBeInTheDocument()
  134. expect(tab).toHaveTextContent('Received Call: Test title')
  135. expect(tab).toHaveAccessibleDescription(
  136. 'Drag and drop to reorder your tabs.',
  137. )
  138. const link = getByRole(tab, 'link')
  139. expect(link).toHaveAttribute('href', '/desktop/tickets/create/999')
  140. expect(link).toHaveAccessibleName('Received Call: Test title')
  141. expect(
  142. getByRole(tab, 'button', { name: 'Close this tab' }),
  143. ).toBeInTheDocument()
  144. })
  145. it('renders forbidden tab', async () => {
  146. mockUserCurrentTaskbarItemListQuery({
  147. userCurrentTaskbarItemList: [
  148. {
  149. __typename: 'UserTaskbarItem',
  150. id: convertToGraphQLId('Taskbar', 999),
  151. key: 'Ticket-999',
  152. callback: EnumTaskbarEntity.TicketZoom,
  153. entityAccess: EnumTaskbarEntityAccess.Forbidden,
  154. },
  155. ],
  156. })
  157. const wrapper = await renderUserTaskbarTabs()
  158. expect(wrapper.getByText('Tabs')).toBeInTheDocument()
  159. const tab = wrapper.getByRole('listitem')
  160. expect(getAllByIconName(tab, 'x-lg')[0]).toHaveClass('text-red-500')
  161. expect(tab).toHaveTextContent('Access denied')
  162. expect(tab).toHaveAccessibleDescription(
  163. 'Drag and drop to reorder your tabs.',
  164. )
  165. expect(queryByRole(tab, 'link')).not.toBeInTheDocument()
  166. expect(
  167. getByRole(tab, 'button', { name: 'Close this tab' }),
  168. ).toBeInTheDocument()
  169. })
  170. it('renders not found tab', async () => {
  171. mockUserCurrentTaskbarItemListQuery({
  172. userCurrentTaskbarItemList: [
  173. {
  174. __typename: 'UserTaskbarItem',
  175. id: convertToGraphQLId('Taskbar', 999),
  176. key: 'Ticket-999',
  177. callback: EnumTaskbarEntity.TicketZoom,
  178. entityAccess: EnumTaskbarEntityAccess.NotFound,
  179. },
  180. ],
  181. })
  182. const wrapper = await renderUserTaskbarTabs()
  183. expect(wrapper.getByText('Tabs')).toBeInTheDocument()
  184. const tab = wrapper.getByRole('listitem')
  185. expect(getAllByIconName(tab, 'x-lg')[0]).toHaveClass('text-red-500')
  186. expect(tab).toHaveTextContent('Not found')
  187. expect(tab).toHaveAccessibleDescription(
  188. 'Drag and drop to reorder your tabs.',
  189. )
  190. expect(queryByRole(tab, 'link')).not.toBeInTheDocument()
  191. expect(
  192. getByRole(tab, 'button', { name: 'Close this tab' }),
  193. ).toBeInTheDocument()
  194. })
  195. it('renders popover button in collapsed mode', async () => {
  196. // Rely on the default ticket tab from the `UserTaskbarItem` factory.
  197. const wrapper = await renderUserTaskbarTabs({
  198. props: {
  199. collapsed: true,
  200. },
  201. })
  202. expect(wrapper.queryByText('Tabs')).not.toBeInTheDocument()
  203. const popoverButton = wrapper.getByRole('button', {
  204. name: 'List of all user taskbar tabs',
  205. })
  206. expect(getByIconName(popoverButton, 'card-list')).toBeInTheDocument()
  207. await wrapper.events.click(popoverButton)
  208. const popover = wrapper.getByRole('region', {
  209. name: 'List of all user taskbar tabs',
  210. })
  211. const tab = getByRole(popover, 'listitem')
  212. expect(getByIconName(tab, 'check-circle-no')).toBeInTheDocument()
  213. expect(tab).toHaveTextContent('Welcome to Zammad!')
  214. expect(tab).not.toHaveAccessibleDescription(
  215. 'Drag and drop to reorder your tabs.',
  216. )
  217. const link = getByRole(tab, 'link')
  218. expect(link).toHaveAttribute('href', '/desktop/tickets/1')
  219. expect(link).toHaveAccessibleName('Ticket#53001 - Welcome to Zammad!')
  220. expect(link).toHaveClass('!bg-yellow-500')
  221. expect(
  222. getByRole(tab, 'button', { name: 'Close this tab' }),
  223. ).toBeInTheDocument()
  224. })
  225. it('implements tab updates subscription', async () => {
  226. mockUserCurrentTaskbarItemListQuery({
  227. userCurrentTaskbarItemList: [],
  228. })
  229. const wrapper = await renderUserTaskbarTabs()
  230. expect(wrapper.queryByText('Tabs')).not.toBeInTheDocument()
  231. // Add item.
  232. await getUserCurrentTaskbarItemUpdatesSubscriptionHandler().trigger({
  233. userCurrentTaskbarItemUpdates: {
  234. addItem: {
  235. __typename: 'UserTaskbarItem',
  236. id: convertToGraphQLId('Taskbar', 42),
  237. key: 'Ticket-42',
  238. callback: EnumTaskbarEntity.TicketZoom,
  239. entityAccess: EnumTaskbarEntityAccess.Granted,
  240. entity: {
  241. __typename: 'Ticket',
  242. id: convertToGraphQLId('Ticket', 42),
  243. internalId: 42,
  244. number: '53042',
  245. title: 'Test ticket title',
  246. stateColorCode: EnumTicketStateColorCode.Pending,
  247. state: {
  248. __typename: 'TicketState',
  249. name: 'pending reminder',
  250. },
  251. },
  252. },
  253. updateItem: null,
  254. removeItem: null,
  255. },
  256. })
  257. expect(wrapper.queryByText('Tabs')).toBeInTheDocument()
  258. expect(wrapper.getByText('Test ticket title')).toBeInTheDocument()
  259. // Update item.
  260. await getUserCurrentTaskbarItemUpdatesSubscriptionHandler().trigger({
  261. userCurrentTaskbarItemUpdates: {
  262. addItem: null,
  263. updateItem: {
  264. __typename: 'UserTaskbarItem',
  265. id: convertToGraphQLId('Taskbar', 42),
  266. key: 'Ticket-42',
  267. callback: EnumTaskbarEntity.TicketZoom,
  268. entityAccess: EnumTaskbarEntityAccess.Granted,
  269. entity: {
  270. __typename: 'Ticket',
  271. id: convertToGraphQLId('Ticket', 42),
  272. internalId: 42,
  273. number: '53042',
  274. title: 'New ticket title',
  275. stateColorCode: EnumTicketStateColorCode.Open,
  276. state: {
  277. __typename: 'TicketState',
  278. name: 'open',
  279. },
  280. },
  281. },
  282. removeItem: null,
  283. },
  284. })
  285. expect(wrapper.queryByText('Test ticket title')).not.toBeInTheDocument()
  286. expect(wrapper.getByText('New ticket title')).toBeInTheDocument()
  287. // Remove item.
  288. await getUserCurrentTaskbarItemUpdatesSubscriptionHandler().trigger({
  289. userCurrentTaskbarItemUpdates: {
  290. addItem: null,
  291. updateItem: null,
  292. removeItem: convertToGraphQLId('Taskbar', 42),
  293. },
  294. })
  295. expect(wrapper.queryByText('New ticket title')).not.toBeInTheDocument()
  296. })
  297. it('supports updating the tabs after they got reordered', async () => {
  298. mockUserCurrentTaskbarItemListQuery({
  299. userCurrentTaskbarItemList: [
  300. {
  301. __typename: 'UserTaskbarItem',
  302. id: convertToGraphQLId('Taskbar', 1),
  303. key: 'TicketCreateScreen-999',
  304. callback: EnumTaskbarEntity.TicketCreate,
  305. entityAccess: EnumTaskbarEntityAccess.Granted,
  306. entity: {
  307. __typename: 'UserTaskbarItemEntityTicketCreate',
  308. uid: '999',
  309. title: 'First ticket',
  310. createArticleTypeKey: 'phone-in',
  311. },
  312. prio: 1,
  313. formId: 'foo',
  314. changed: false,
  315. dirty: false,
  316. notify: false,
  317. updatedAt: '2024-07-24T15:42:28.212Z',
  318. },
  319. {
  320. __typename: 'UserTaskbarItem',
  321. id: convertToGraphQLId('Taskbar', 2),
  322. key: 'Ticket-42',
  323. callback: EnumTaskbarEntity.TicketZoom,
  324. entityAccess: EnumTaskbarEntityAccess.Granted,
  325. entity: {
  326. __typename: 'Ticket',
  327. id: convertToGraphQLId('Ticket', 42),
  328. internalId: 42,
  329. number: '53042',
  330. title: 'Second ticket',
  331. stateColorCode: EnumTicketStateColorCode.Pending,
  332. state: {
  333. __typename: 'TicketState',
  334. name: 'pending reminder',
  335. },
  336. },
  337. prio: 2,
  338. formId: 'bar',
  339. changed: false,
  340. dirty: false,
  341. notify: false,
  342. updatedAt: '2024-07-24T15:42:28.212Z',
  343. },
  344. ],
  345. })
  346. const wrapper = await renderUserTaskbarTabs()
  347. expect(wrapper.getByText('Tabs')).toBeInTheDocument()
  348. let tabs = wrapper.getAllByRole('listitem')
  349. expect(tabs).toHaveLength(2)
  350. expect(tabs[0]).toHaveTextContent('First ticket')
  351. expect(tabs[1]).toHaveTextContent('Second ticket')
  352. // Reorder tabs.
  353. await getUserCurrentTaskbarItemListUpdatesSubscriptionHandler().trigger({
  354. userCurrentTaskbarItemListUpdates: {
  355. __typename: 'UserCurrentTaskbarItemListUpdatesPayload',
  356. taskbarItemList: [
  357. {
  358. __typename: 'UserTaskbarItem',
  359. id: convertToGraphQLId('Taskbar', 2),
  360. prio: 1,
  361. },
  362. {
  363. __typename: 'UserTaskbarItem',
  364. id: convertToGraphQLId('Taskbar', 1),
  365. prio: 2,
  366. },
  367. ],
  368. },
  369. })
  370. tabs = wrapper.getAllByRole('listitem')
  371. expect(tabs).toHaveLength(2)
  372. expect(tabs[0]).toHaveTextContent('Second ticket')
  373. expect(tabs[1]).toHaveTextContent('First ticket')
  374. })
  375. it('supports closing tabs', async () => {
  376. // Rely on the default ticket tab from the `UserTaskbarItem` factory.
  377. const wrapper = await renderUserTaskbarTabs()
  378. expect(wrapper.getByText('Tabs')).toBeInTheDocument()
  379. const tab = wrapper.getByRole('listitem')
  380. await wrapper.events.click(
  381. getByRole(tab, 'button', { name: 'Close this tab' }),
  382. )
  383. const calls = await waitForUserCurrentTaskbarItemDeleteMutationCalls()
  384. expect(waitForVariantConfirmationMock).toHaveBeenCalled()
  385. expect(calls.at(-1)?.variables).toEqual({
  386. id: convertToGraphQLId('Taskbar', 1),
  387. })
  388. // TODO: Check for correct redirect when implemented.
  389. await vi.waitFor(() => {
  390. expect(
  391. wrapper,
  392. 'correctly redirects to dashboard screen',
  393. ).toHaveCurrentUrl('/dashboard')
  394. })
  395. })
  396. })