ticket-detail-view.spec.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { within } from '@testing-library/vue'
  3. import { beforeEach, expect } from 'vitest'
  4. import createArticle from '#tests/graphql/factories/TicketArticle.ts'
  5. import { getTestRouter } from '#tests/support/components/renderComponent.ts'
  6. import { visitView } from '#tests/support/components/visitView.ts'
  7. import { mockPermissions } from '#tests/support/mock-permissions.ts'
  8. import { mockTicketArticlesQuery } from '#shared/entities/ticket/graphql/queries/ticket/articles.mocks.ts'
  9. import { mockTicketQuery } from '#shared/entities/ticket/graphql/queries/ticket.mocks.ts'
  10. import { createDummyArticle } from '#shared/entities/ticket-article/__tests__/mocks/ticket-articles.ts'
  11. import { createDummyTicket } from '#shared/entities/ticket-article/__tests__/mocks/ticket.ts'
  12. import { type TicketArticleEdge } from '#shared/graphql/types.ts'
  13. import { mockLinkListQuery } from '../graphql/queries/linkList.mocks.ts'
  14. describe('Ticket detail view', () => {
  15. beforeEach(() => {
  16. mockPermissions(['ticket.agent'])
  17. mockLinkListQuery({
  18. linkList: [],
  19. })
  20. })
  21. describe('Error handling', () => {
  22. it.todo('redirects if ticket id is not found', async () => {
  23. // :TODO Test as soon as the bug for the Query has complexity of 19726, has been resolved,
  24. // and specific ticket error handling is in place.
  25. mockTicketQuery({
  26. ticket: null,
  27. })
  28. await visitView('/tickets/232')
  29. const router = getTestRouter()
  30. expect(router.currentRoute.value.name).toEqual('Error')
  31. })
  32. })
  33. describe('Article list', () => {
  34. it('shows see more button', async () => {
  35. mockTicketQuery({
  36. ticket: createDummyTicket(),
  37. })
  38. const firstArticleEdges: TicketArticleEdge[] = []
  39. const articlesEdges: TicketArticleEdge[] = []
  40. let count = 27
  41. while (count > 0) {
  42. const first = createArticle()
  43. const article = createArticle()
  44. if (count <= 5)
  45. firstArticleEdges.push(<TicketArticleEdge>{
  46. cursor: Buffer.from(count.toString()).toString('base64'),
  47. node: { ...first, internalId: count, sender: { name: 'Agent' } },
  48. })
  49. if (count > 5)
  50. articlesEdges.push(<TicketArticleEdge>{
  51. cursor: Buffer.from(count.toString()).toString('base64'),
  52. node: {
  53. ...article,
  54. internalId: 50 - count,
  55. sender: { name: 'Customer' },
  56. },
  57. })
  58. // eslint-disable-next-line no-plusplus
  59. count--
  60. }
  61. mockTicketArticlesQuery({
  62. articles: {
  63. totalCount: 50,
  64. edges: articlesEdges,
  65. pageInfo: {
  66. hasPreviousPage: articlesEdges.length > 0,
  67. startCursor:
  68. articlesEdges.length > 0 ? articlesEdges[0].cursor : null,
  69. endCursor: btoa('50'),
  70. },
  71. },
  72. firstArticles: {
  73. edges: firstArticleEdges,
  74. },
  75. })
  76. const view = await visitView('/tickets/1')
  77. const feed = view.getByRole('feed')
  78. const articles = within(feed).getAllByRole('article')
  79. expect(articles).toHaveLength(26) // 20 articles from end && 5 articles from the beginning 1 more button
  80. expect(
  81. within(articles.at(6) as HTMLElement).getByRole('button', {
  82. name: 'See more',
  83. }),
  84. ).toBeInTheDocument()
  85. })
  86. it('shows meta information if article is clicked', async () => {
  87. mockTicketQuery({
  88. ticket: createDummyTicket(),
  89. })
  90. const testArticle = createDummyArticle({
  91. bodyWithUrls: 'foobar',
  92. })
  93. mockTicketArticlesQuery({
  94. articles: {
  95. totalCount: 1,
  96. edges: [{ node: testArticle }],
  97. },
  98. firstArticles: {
  99. edges: [{ node: testArticle }],
  100. },
  101. })
  102. const view = await visitView('/tickets/1')
  103. expect(
  104. view.getByRole('heading', { name: 'Test Ticket', level: 2 }),
  105. ).toBeInTheDocument()
  106. const ticketDetailHeader = view.getByTestId(
  107. 'visible-ticket-detail-top-bar',
  108. )
  109. expect(
  110. within(ticketDetailHeader).getByLabelText('Breadcrumb navigation'),
  111. ).toBeInTheDocument()
  112. expect(view.getByTestId('article-content')).toHaveTextContent('foobar')
  113. await view.events.click(view.getByTestId('article-bubble-body-1'))
  114. expect(
  115. await view.findByLabelText('Article meta information'),
  116. ).toBeInTheDocument()
  117. vi.useFakeTimers()
  118. await view.events.click(view.getByTestId('article-bubble-body-1'))
  119. // NB: Click handler has a built-in timeout (200ms) in order to catch double click behavior.
  120. // Advance the timer manually so we speed up the test a bit.
  121. await vi.runAllTimersAsync()
  122. vi.useRealTimers()
  123. expect(
  124. view.queryByLabelText('Article meta information'),
  125. ).not.toBeInTheDocument()
  126. })
  127. })
  128. it('has invisible top bar and hides it from screen reader', async () => {
  129. mockTicketQuery({
  130. ticket: createDummyTicket(),
  131. })
  132. const testArticle = createDummyArticle({
  133. bodyWithUrls: 'foobar',
  134. })
  135. mockTicketArticlesQuery({
  136. articles: {
  137. totalCount: 1,
  138. edges: [{ node: testArticle }],
  139. },
  140. firstArticles: {
  141. edges: [{ node: testArticle }],
  142. },
  143. })
  144. const view = await visitView('/tickets/1')
  145. expect(view.getByTestId('invisible-ticket-detail-top-bar')).toHaveAttribute(
  146. 'aria-hidden',
  147. 'true',
  148. )
  149. expect(view.getByTestId('invisible-ticket-detail-top-bar')).toHaveClass(
  150. 'invisible',
  151. )
  152. })
  153. })