UserTaskbarTabs.spec.ts 18 KB

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