ticket-overviews.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. // Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. import { waitFor, within } from '@testing-library/vue'
  3. import { afterEach, beforeEach } from 'vitest'
  4. import { generateObjectData } from '#tests/graphql/builders/index.ts'
  5. import { getTestRouter } from '#tests/support/components/renderComponent.ts'
  6. import { visitView } from '#tests/support/components/visitView.ts'
  7. import { mockApplicationConfig } from '#tests/support/mock-applicationConfig.ts'
  8. import { mockPermissions } from '#tests/support/mock-permissions.ts'
  9. import { waitForNextTick } from '#tests/support/utils.ts'
  10. import { mockCurrentUserQuery } from '#shared/graphql/queries/currentUser.mocks.ts'
  11. import { EnumOrderDirection } from '#shared/graphql/types.ts'
  12. import { convertToGraphQLId } from '#shared/graphql/utils.ts'
  13. import { mockTicketsCachedByOverviewQuery } from '#desktop/entities/ticket/graphql/queries/ticketsCachedByOverview.mocks.ts'
  14. import {
  15. mockUserCurrentTicketOverviewsQuery,
  16. waitForUserCurrentTicketOverviewsQueryCalls,
  17. } from '#desktop/entities/ticket/graphql/queries/userCurrentTicketOverviews.mocks.ts'
  18. import { getUserCurrentOverviewOrderingFullAttributesUpdatesSubscriptionHandler } from '#desktop/entities/ticket/graphql/subscriptions/useCurrentOverviewOrderingFullAttributesUpdates.mocks.ts'
  19. import { getUserCurrentTicketOverviewFullAttributesUpdatesSubscriptionHandler } from '#desktop/entities/ticket/graphql/subscriptions/userCurrentTicketOverviewFullAttributesUpdates.mocks.ts'
  20. const mockDefaultOverviewQueries = () =>
  21. mockUserCurrentTicketOverviewsQuery({
  22. userCurrentTicketOverviews: [
  23. {
  24. id: convertToGraphQLId('Overview', 1),
  25. name: 'My Assigned Tickets',
  26. link: 'my_assigned',
  27. prio: 1000,
  28. orderBy: 'created_at',
  29. orderDirection: EnumOrderDirection.Ascending,
  30. active: true,
  31. },
  32. ],
  33. })
  34. const getDefaultOverviews = () => [
  35. {
  36. id: convertToGraphQLId('Overview', 1),
  37. name: 'My Assigned Tickets',
  38. link: 'my_assigned',
  39. prio: 1000,
  40. orderBy: 'created_at',
  41. orderDirection: EnumOrderDirection.Ascending,
  42. viewColumns: [],
  43. orderColumns: [],
  44. active: true,
  45. },
  46. {
  47. id: convertToGraphQLId('Overview', 2),
  48. name: 'New Tickets',
  49. link: 'new_tickets',
  50. prio: 2000,
  51. orderBy: 'created_at',
  52. orderDirection: EnumOrderDirection.Ascending,
  53. viewColumns: [],
  54. orderColumns: [],
  55. active: true,
  56. },
  57. ]
  58. describe('TicketOverviews', async () => {
  59. it('redirects when overview does not exist', async () => {
  60. mockDefaultOverviewQueries()
  61. mockTicketsCachedByOverviewQuery({
  62. ticketsCachedByOverview: generateObjectData('TicketConnection', {
  63. totalCount: 0,
  64. edges: [],
  65. pageInfo: {
  66. endCursor: '',
  67. hasNextPage: false,
  68. },
  69. }),
  70. })
  71. await visitView('tickets/view/does_not_exist')
  72. const router = getTestRouter()
  73. await waitFor(() =>
  74. expect(router.currentRoute.value.path).toBe('/tickets/view/my_assigned'),
  75. )
  76. })
  77. it('displays overviews correctly', async () => {
  78. mockDefaultOverviewQueries()
  79. const view = await visitView('tickets/view/my_assigned')
  80. const primaryNavigationSidebar = view.getByRole('complementary', {
  81. name: 'Main sidebar',
  82. })
  83. expect(
  84. within(primaryNavigationSidebar).getByRole('link', {
  85. name: 'Overviews',
  86. }),
  87. ).toHaveAttribute('href', '/desktop/tickets/view')
  88. const secondaryNavigationSidebar = await view.findByRole('complementary', {
  89. name: 'second level navigation sidebar',
  90. })
  91. expect(secondaryNavigationSidebar).toHaveTextContent('My Assigned Tickets')
  92. expect(
  93. await view.findByRole('table', { name: 'Overview: My Assigned Tickets' }),
  94. ).toHaveTextContent('My Assigned TicketsState Icon') // deeper test is in TicketList
  95. })
  96. it('reorders overviews when subscription comes in', async () => {
  97. const overviews = getDefaultOverviews()
  98. mockCurrentUserQuery({
  99. currentUser: {
  100. preferences: {
  101. overviews_last_used: {
  102. '1': '2021-06-01T00:00:00.000Z',
  103. '2': '2021-06-01T00:00:00.000Z',
  104. },
  105. },
  106. },
  107. })
  108. mockUserCurrentTicketOverviewsQuery({
  109. userCurrentTicketOverviews: overviews,
  110. })
  111. const view = await visitView('tickets/view/my_assigned')
  112. const secondaryNavigationSidebar = await view.findByRole('complementary', {
  113. name: 'second level navigation sidebar',
  114. })
  115. let currentOverviews = within(secondaryNavigationSidebar).getAllByRole(
  116. 'link',
  117. )
  118. expect(currentOverviews[0]).toHaveTextContent('My Assigned Tickets')
  119. expect(currentOverviews[1]).toHaveTextContent('New Tickets')
  120. await getUserCurrentOverviewOrderingFullAttributesUpdatesSubscriptionHandler().trigger(
  121. {
  122. userCurrentOverviewOrderingUpdates: generateObjectData(
  123. 'UserCurrentOverviewOrderingUpdatesPayload',
  124. {
  125. overviews: overviews.reverse(),
  126. },
  127. ),
  128. },
  129. )
  130. await waitForNextTick()
  131. currentOverviews = within(secondaryNavigationSidebar).getAllByRole('link')
  132. expect(currentOverviews[0]).toHaveTextContent('New Tickets')
  133. expect(currentOverviews[1]).toHaveTextContent('My Assigned Tickets')
  134. })
  135. it('updates overviews when subscription comes in', async () => {
  136. // ticketsCachedCountByOverview will be removed soon
  137. const overviews = getDefaultOverviews()
  138. mockUserCurrentTicketOverviewsQuery({
  139. userCurrentTicketOverviews: overviews,
  140. })
  141. const view = await visitView('tickets/view/my_assigned')
  142. const secondaryNavigationSidebar = await view.findByRole('complementary', {
  143. name: 'second level navigation sidebar',
  144. })
  145. expect(
  146. within(secondaryNavigationSidebar).getAllByRole('link'),
  147. ).toHaveLength(2)
  148. await getUserCurrentTicketOverviewFullAttributesUpdatesSubscriptionHandler().trigger(
  149. {
  150. userCurrentTicketOverviewUpdates: generateObjectData(
  151. 'UserCurrentTicketOverviewUpdatesPayload',
  152. {
  153. ticketOverviews: [
  154. ...overviews,
  155. {
  156. id: convertToGraphQLId('Overview', 3),
  157. name: 'Foo Tickets',
  158. link: 'foo_tickets',
  159. prio: 2000,
  160. orderBy: 'created_at',
  161. orderDirection: EnumOrderDirection.Ascending,
  162. active: true,
  163. },
  164. ],
  165. },
  166. ),
  167. },
  168. )
  169. expect(
  170. within(secondaryNavigationSidebar).getAllByRole('link'),
  171. ).toHaveLength(3)
  172. })
  173. describe('empty states', () => {
  174. it('displays a message to the agent when no overviews are available.', async () => {
  175. mockUserCurrentTicketOverviewsQuery({
  176. userCurrentTicketOverviews: [],
  177. })
  178. const view = await visitView('tickets/view')
  179. expect(
  180. await view.findByText(
  181. 'Currently, no overviews are assigned to your roles. Please contact your administrator.',
  182. ),
  183. ).toBeInTheDocument()
  184. expect(view.getByRole('heading', { level: 2 })).toHaveTextContent(
  185. 'No Overviews',
  186. )
  187. expect(view.getByIconName('exclamation-triangle')).toBeInTheDocument()
  188. expect(
  189. view.queryByLabelText('second level navigation sidebar'),
  190. ).not.toBeInTheDocument()
  191. })
  192. })
  193. it('displays a ticket create message to the customer when no tickets are available and no ticket history', async () => {
  194. mockUserCurrentTicketOverviewsQuery({
  195. userCurrentTicketOverviews: [
  196. generateObjectData('Overview', {
  197. id: convertToGraphQLId('Overview', 1),
  198. name: 'My Tickets',
  199. link: 'my_tickets',
  200. prio: 9,
  201. orderBy: 'created_at',
  202. orderDirection: EnumOrderDirection.Ascending,
  203. organizationShared: false,
  204. outOfOffice: false,
  205. active: true,
  206. }),
  207. ],
  208. })
  209. mockCurrentUserQuery({
  210. currentUser: {
  211. preferences: {
  212. tickets_closed: 0,
  213. tickets_open: 0,
  214. },
  215. },
  216. })
  217. mockTicketsCachedByOverviewQuery({
  218. ticketsCachedByOverview: generateObjectData('TicketConnection', {
  219. totalCount: 0,
  220. edges: [],
  221. pageInfo: {
  222. endCursor: '',
  223. hasNextPage: false,
  224. },
  225. }),
  226. })
  227. mockPermissions(['ticket.customer'])
  228. await mockApplicationConfig({ customer_ticket_create: true })
  229. const view = await visitView('tickets/view')
  230. const secondaryNavigationSidebar = await view.findByRole('complementary', {
  231. name: 'second level navigation sidebar',
  232. })
  233. expect(
  234. within(secondaryNavigationSidebar).getByRole('link', {
  235. name: 'My Tickets 0', // 0 comes from the ticket count
  236. }),
  237. ).toBeInTheDocument()
  238. expect(await view.findByRole('heading', { level: 2 })).toHaveTextContent(
  239. 'Welcome!',
  240. )
  241. expect(
  242. view.getByText('You have not created a ticket yet.'),
  243. ).toBeInTheDocument()
  244. expect(
  245. view.getByText(
  246. 'The way to communicate with us is this thing called "ticket".',
  247. ),
  248. ).toBeInTheDocument()
  249. expect(
  250. view.getByText(
  251. 'Please click on the button below to create your first one.',
  252. ),
  253. ).toBeInTheDocument()
  254. await view.events.click(
  255. view.getByRole('button', { name: 'Create your first ticket' }),
  256. )
  257. const router = getTestRouter()
  258. await waitFor(() =>
  259. expect(router.currentRoute.value.name).toBe('TicketCreate'),
  260. )
  261. })
  262. it('displays a message indicating no tickets are available when the overview is empty', async () => {
  263. mockUserCurrentTicketOverviewsQuery({
  264. userCurrentTicketOverviews: [
  265. generateObjectData('Overview', {
  266. id: convertToGraphQLId('Overview', 1),
  267. name: 'My Assigned Tickets',
  268. link: 'my_assigned',
  269. prio: 4,
  270. orderBy: 'created_at',
  271. orderDirection: EnumOrderDirection.Ascending,
  272. organizationShared: false,
  273. outOfOffice: false,
  274. active: true,
  275. }),
  276. ],
  277. })
  278. mockCurrentUserQuery({
  279. currentUser: {
  280. preferences: {
  281. tickets_closed: 1,
  282. tickets_open: 2,
  283. },
  284. },
  285. })
  286. mockTicketsCachedByOverviewQuery({
  287. ticketsCachedByOverview: generateObjectData('TicketConnection', {
  288. totalCount: 0,
  289. edges: [],
  290. pageInfo: {
  291. endCursor: '',
  292. hasNextPage: false,
  293. },
  294. }),
  295. })
  296. mockPermissions(['ticket.agent'])
  297. const view = await visitView('tickets/view')
  298. expect(await view.findByRole('heading', { level: 2 })).toHaveTextContent(
  299. 'Empty Overview',
  300. )
  301. expect(view.getByText('No tickets in this state.')).toBeInTheDocument()
  302. })
  303. describe('polling overviews', () => {
  304. beforeEach(vi.useFakeTimers)
  305. afterEach(vi.useRealTimers)
  306. it.todo('polls for the active overviews', async () => {
  307. mockDefaultOverviewQueries()
  308. await visitView('tickets/view/my_assigned')
  309. const mocks = await waitForUserCurrentTicketOverviewsQueryCalls()
  310. expect(mocks).toHaveLength(1)
  311. const config = {
  312. foreground: {
  313. interval_sec: 5,
  314. cache_ttl_sec: 5,
  315. },
  316. }
  317. setQueryPollingConfig(config)
  318. await vi.advanceTimersByTimeAsync(config.foreground.interval_sec * 1000)
  319. await vi.advanceTimersToNextTimerAsync()
  320. expect(mocks).toHaveLength(2)
  321. // await waitFor(() => expect(mocks).toHaveLength(2))
  322. })
  323. it.todo('polls for the background overviews', async () => {
  324. mockDefaultOverviewQueries()
  325. mockCurrentUserQuery({
  326. currentUser: {
  327. preferences: {
  328. overviews_last_used: {
  329. '2': '2021-06-01T00:00:00.000Z',
  330. },
  331. },
  332. },
  333. })
  334. await visitView('tickets/view/my_assigned')
  335. // const mocks = await waitForUserCurrentTicketOverviewsQueryCalls()
  336. })
  337. it('disables polling when the config is disabled', async () => {
  338. mockDefaultOverviewQueries()
  339. await visitView('tickets/view/my_assigned')
  340. setQueryPollingConfig({
  341. disabled: true,
  342. })
  343. const mocks = await waitForUserCurrentTicketOverviewsQueryCalls()
  344. expect(mocks).toHaveLength(1)
  345. // :TODO fix this
  346. await vi.advanceTimersByTimeAsync(5 * 1000)
  347. await vi.advanceTimersToNextTimerAsync()
  348. expect(mocks).toHaveLength(1)
  349. })
  350. })
  351. })