ticket-detail-view.spec.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. // Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. const now = new Date(2022, 1, 1, 0, 0, 0, 0)
  3. vi.setSystemTime(now)
  4. import { ApolloError } from '@apollo/client/errors'
  5. import { TicketArticleRetrySecurityProcessDocument } from '@shared/entities/ticket-article/graphql/mutations/ticketArticleRetrySecurityProcess.api'
  6. import type { TicketArticleRetrySecurityProcessMutation } from '@shared/graphql/types'
  7. import { convertToGraphQLId } from '@shared/graphql/utils'
  8. import { getAllByTestId, getByLabelText } from '@testing-library/vue'
  9. import { getByIconName } from '@tests/support/components/iconQueries'
  10. import { getTestRouter } from '@tests/support/components/renderComponent'
  11. import { visitView } from '@tests/support/components/visitView'
  12. import createMockClient from '@tests/support/mock-apollo-client'
  13. import { mockAccount } from '@tests/support/mock-account'
  14. import { mockApplicationConfig } from '@tests/support/mock-applicationConfig'
  15. import {
  16. mockGraphQLApi,
  17. mockGraphQLSubscription,
  18. } from '@tests/support/mock-graphql-api'
  19. import { mockPermissions } from '@tests/support/mock-permissions'
  20. import { nullableMock, waitUntil } from '@tests/support/utils'
  21. import { flushPromises } from '@vue/test-utils'
  22. import { TicketDocument } from '../graphql/queries/ticket.api'
  23. import { TicketArticlesDocument } from '../graphql/queries/ticket/articles.api'
  24. import { TicketArticleUpdatesDocument } from '../graphql/subscriptions/ticketArticlesUpdates.api'
  25. import { TicketUpdatesDocument } from '../graphql/subscriptions/ticketUpdates.api'
  26. import {
  27. defaultArticles,
  28. defaultTicket,
  29. mockTicketDetailViewGql,
  30. mockTicketLiveUsersGql,
  31. } from './mocks/detail-view'
  32. beforeEach(() => {
  33. mockPermissions(['ticket.agent'])
  34. })
  35. test('statics inside ticket zoom view', async () => {
  36. const { waitUntilTicketLoaded } = mockTicketDetailViewGql()
  37. const view = await visitView('/tickets/1')
  38. expect(view.getByTestId('loader-list')).toBeInTheDocument()
  39. expect(view.getByTestId('loader-title')).toBeInTheDocument()
  40. expect(view.getByTestId('loader-header')).toBeInTheDocument()
  41. await waitUntilTicketLoaded()
  42. const header = view.getByTestId('header-content')
  43. expect(header).toHaveTextContent('#610001')
  44. expect(header).toHaveTextContent('created 3 days ago')
  45. const titleElement = view.getByTestId('title-content')
  46. expect(titleElement).toHaveTextContent('Test Ticket View')
  47. expect(titleElement, 'has customer avatar').toHaveTextContent('JD')
  48. const articlesElement = view.getByRole('group', { name: 'Articles' })
  49. const times = getAllByTestId(articlesElement, 'date-time-absolute')
  50. expect(times).toHaveLength(2)
  51. expect(times[0]).toHaveTextContent('2022-01-29')
  52. expect(times[1]).toHaveTextContent('2022-01-30')
  53. const comments = view.getAllByRole('comment')
  54. // everything else for article is testes inside ArticleBubble
  55. expect(comments).toHaveLength(3)
  56. // customer article
  57. expect(comments[0]).toHaveClass('flex-row-reverse')
  58. expect(comments[0]).toHaveTextContent('John')
  59. expect(comments[0]).toHaveTextContent('Body of a test ticket')
  60. // agent public comment
  61. expect(comments[1]).not.toHaveClass('flex-row-reverse')
  62. expect(comments[1]).toHaveTextContent('Albert')
  63. expect(comments[1]).toHaveTextContent('energy equals power times time')
  64. // agent internal comment
  65. expect(comments[2]).not.toHaveClass('flex-row-reverse')
  66. expect(comments[2]).toHaveTextContent('Monkey')
  67. expect(comments[2]).toHaveTextContent('only agents can see this haha')
  68. expect(view.getByRole('button', { name: 'Add reply' })).toBeInTheDocument()
  69. expect(
  70. view.queryByText('not-visible-attachment.png'),
  71. 'filters original-format attachments',
  72. ).not.toBeInTheDocument()
  73. })
  74. test('can refresh data by pulling up', async () => {
  75. const { waitUntilTicketLoaded } = mockTicketDetailViewGql()
  76. const view = await visitView('/tickets/1')
  77. await waitUntilTicketLoaded()
  78. const articlesElement = view.getByRole('group', { name: 'Articles' })
  79. const startEvent = new TouchEvent('touchstart', {
  80. touches: [{ clientY: 300 } as Touch],
  81. })
  82. articlesElement.dispatchEvent(startEvent)
  83. const moveEvent = new TouchEvent('touchmove', {
  84. touches: [{ clientY: 100 } as Touch],
  85. })
  86. Object.defineProperty(document.documentElement, 'scrollHeight', {
  87. value: 200,
  88. })
  89. Object.defineProperty(document.documentElement, 'scrollTop', {
  90. value: 0,
  91. })
  92. Object.defineProperty(document.documentElement, 'clientHeight', {
  93. value: 200,
  94. })
  95. articlesElement.dispatchEvent(moveEvent)
  96. await flushPromises()
  97. expect(view.getByIconName('mobile-arrow-down')).toHaveStyle({
  98. transform: 'translateY(22px) rotate(180deg)',
  99. })
  100. const touchEnd = new TouchEvent('touchend')
  101. articlesElement.dispatchEvent(touchEnd)
  102. await flushPromises()
  103. expect(view.getAllByIconName('mobile-loading')).not.toHaveLength(0)
  104. // TODO test api call
  105. })
  106. test("redirects to error page, if can't find ticket", async () => {
  107. const { calls } = mockGraphQLApi(TicketDocument).willFailWithError([
  108. { message: 'The ticket 9866 could not be found', extensions: {} },
  109. ])
  110. mockGraphQLApi(TicketArticlesDocument).willFailWithError([
  111. { message: 'The ticket 9866 could not be found', extensions: {} },
  112. ])
  113. mockGraphQLSubscription(TicketUpdatesDocument).error(
  114. new ApolloError({ errorMessage: "Couldn't find Ticket with 'id'=9866" }),
  115. )
  116. mockGraphQLSubscription(TicketArticleUpdatesDocument).error(
  117. new ApolloError({ errorMessage: "Couldn't find Ticket with 'id'=9866" }),
  118. )
  119. await visitView('/tickets/9866')
  120. await waitUntil(() => calls.error > 0)
  121. await flushPromises()
  122. const router = getTestRouter()
  123. expect(router.replace).toHaveBeenCalledWith({
  124. name: 'Error',
  125. query: {
  126. redirect: '1',
  127. },
  128. })
  129. })
  130. test('show article context on click', async () => {
  131. const { waitUntilTicketLoaded } = mockTicketDetailViewGql()
  132. const view = await visitView('/tickets/1')
  133. await waitUntilTicketLoaded()
  134. vi.useRealTimers()
  135. const contextTriggers = view.getAllByRole('button', {
  136. name: 'Article actions',
  137. })
  138. expect(contextTriggers).toHaveLength(3)
  139. await view.events.click(contextTriggers[0])
  140. expect(view.getByText('Set to internal')).toBeInTheDocument()
  141. expect(view.getByText('Split')).toBeInTheDocument()
  142. // expect(view.getByText('Reply')).toBeInTheDocument()
  143. // TODO actions itself should be tested when reply will be implemented
  144. })
  145. test('change content on subscription', async () => {
  146. const { waitUntilTicketLoaded, mockTicketSubscription, ticket } =
  147. mockTicketDetailViewGql()
  148. const view = await visitView('/tickets/1')
  149. await waitUntilTicketLoaded()
  150. expect(view.getByText(ticket.title)).toBeInTheDocument()
  151. await mockTicketSubscription.next({
  152. data: {
  153. ticketUpdates: {
  154. __typename: 'TicketUpdatesPayload',
  155. ticket: nullableMock({ ...ticket, title: 'Some New Title' }),
  156. ticketArticle: null,
  157. },
  158. },
  159. })
  160. expect(view.getByText('Some New Title')).toBeInTheDocument()
  161. })
  162. test('can load more articles', async () => {
  163. mockApplicationConfig({
  164. ticket_articles_min: 1,
  165. })
  166. const { description, articles } = defaultArticles()
  167. const [article1, article2] = articles.edges
  168. const articlesHandler = vi.fn(async (variables: any) => {
  169. if (!variables.loadDescription) {
  170. return {
  171. data: {
  172. description: null,
  173. articles: {
  174. __typename: 'TicketArticleConnection',
  175. totalCount: 3,
  176. edges: [article2],
  177. pageInfo: {
  178. __typename: 'PageInfo',
  179. hasPreviousPage: false,
  180. startCursor: '',
  181. endCursor: '',
  182. },
  183. },
  184. },
  185. }
  186. }
  187. return {
  188. data: {
  189. description,
  190. articles: {
  191. __typename: 'TicketArticleConnection',
  192. totalCount: 3,
  193. edges: [article1],
  194. pageInfo: {
  195. __typename: 'PageInfo',
  196. hasPreviousPage: true,
  197. startCursor: article1.cursor,
  198. endCursor: '',
  199. },
  200. },
  201. },
  202. }
  203. })
  204. mockTicketLiveUsersGql()
  205. mockGraphQLApi(TicketDocument).willResolve(defaultTicket())
  206. mockGraphQLSubscription(TicketUpdatesDocument)
  207. mockGraphQLSubscription(TicketArticleUpdatesDocument)
  208. createMockClient([
  209. {
  210. operationDocument: TicketArticlesDocument,
  211. handler: articlesHandler,
  212. },
  213. ])
  214. const view = await visitView('/tickets/1')
  215. const comments = await view.findAllByRole('comment')
  216. expect(comments).toHaveLength(2)
  217. vi.useRealTimers()
  218. await view.events.click(view.getByText('load 1 more'))
  219. expect(view.getAllByRole('comment')).toHaveLength(3)
  220. })
  221. describe('calling API to retry encryption', () => {
  222. it('updates ticket description', async () => {
  223. const articlesQuery = defaultArticles()
  224. const article = articlesQuery.description.edges[0].node
  225. article.securityState = {
  226. __typename: 'TicketArticleSecurityState',
  227. encryptionMessage: '',
  228. encryptionSuccess: false,
  229. signingMessage: 'Certificate for verification could not be found.',
  230. signingSuccess: false,
  231. }
  232. const { waitUntilTicketLoaded } = mockTicketDetailViewGql({
  233. articles: articlesQuery,
  234. })
  235. const view = await visitView('/tickets/1')
  236. await waitUntilTicketLoaded()
  237. const securityError = view.getByRole('button', { name: 'Security Error' })
  238. await view.events.click(securityError)
  239. const retryResult = {
  240. __typename: 'TicketArticleSecurityState',
  241. encryptionMessage: '',
  242. encryptionSuccess: false,
  243. signingMessage:
  244. '/emailAddress=smime1@example.com/C=DE/ST=Berlin/L=Berlin/O=Example Security/OU=IT Department/CN=example.com',
  245. signingSuccess: true,
  246. type: 'S/MIME',
  247. } as const
  248. const mutation = mockGraphQLApi(
  249. TicketArticleRetrySecurityProcessDocument,
  250. ).willResolve<TicketArticleRetrySecurityProcessMutation>({
  251. ticketArticleRetrySecurityProcess: {
  252. __typename: 'TicketArticleRetrySecurityProcessPayload',
  253. retryResult,
  254. article: {
  255. __typename: 'TicketArticle',
  256. id: article.id,
  257. securityState: { ...retryResult },
  258. },
  259. errors: null,
  260. },
  261. })
  262. await view.events.click(view.getByRole('button', { name: 'Try again' }))
  263. expect(mutation.spies.resolve).toHaveBeenCalled()
  264. expect(view.queryByTestId('popupWindow')).not.toBeInTheDocument()
  265. const [articlesElement] = view.getAllByRole('comment')
  266. expect(getByLabelText(articlesElement, 'Signed')).toBeInTheDocument()
  267. expect(getByIconName(articlesElement, 'mobile-signed')).toBeInTheDocument()
  268. })
  269. it('updates non-description article', async () => {
  270. const articlesQuery = defaultArticles()
  271. const article = articlesQuery.articles.edges[0].node
  272. article.securityState = {
  273. __typename: 'TicketArticleSecurityState',
  274. encryptionMessage: '',
  275. encryptionSuccess: false,
  276. signingMessage: 'Certificate for verification could not be found.',
  277. signingSuccess: false,
  278. }
  279. const { waitUntilTicketLoaded } = mockTicketDetailViewGql({
  280. articles: articlesQuery,
  281. })
  282. const view = await visitView('/tickets/1')
  283. await waitUntilTicketLoaded()
  284. const securityError = view.getByRole('button', { name: 'Security Error' })
  285. await view.events.click(securityError)
  286. const retryResult = {
  287. __typename: 'TicketArticleSecurityState',
  288. encryptionMessage: '',
  289. encryptionSuccess: false,
  290. signingMessage:
  291. '/emailAddress=smime1@example.com/C=DE/ST=Berlin/L=Berlin/O=Example Security/OU=IT Department/CN=example.com',
  292. signingSuccess: true,
  293. type: 'S/MIME',
  294. } as const
  295. const mutation = mockGraphQLApi(
  296. TicketArticleRetrySecurityProcessDocument,
  297. ).willResolve<TicketArticleRetrySecurityProcessMutation>({
  298. ticketArticleRetrySecurityProcess: {
  299. __typename: 'TicketArticleRetrySecurityProcessPayload',
  300. retryResult,
  301. article: {
  302. __typename: 'TicketArticle',
  303. id: article.id,
  304. securityState: { ...retryResult },
  305. },
  306. errors: null,
  307. },
  308. })
  309. await view.events.click(view.getByRole('button', { name: 'Try again' }))
  310. expect(mutation.spies.resolve).toHaveBeenCalled()
  311. expect(view.queryByTestId('popupWindow')).not.toBeInTheDocument()
  312. const [, firstCommentArticle] = view.getAllByRole('comment')
  313. expect(getByLabelText(firstCommentArticle, 'Signed')).toBeInTheDocument()
  314. expect(
  315. getByIconName(firstCommentArticle, 'mobile-signed'),
  316. ).toBeInTheDocument()
  317. })
  318. })
  319. describe('ticket viewers inside a ticket', () => {
  320. it('displays information with newer last interaction (and without own entry)', async () => {
  321. const { waitUntilTicketLoaded, mockTicketLiveUsersSubscription } =
  322. mockTicketDetailViewGql()
  323. mockAccount({
  324. lastname: 'Doe',
  325. firstname: 'John',
  326. fullname: 'John Doe',
  327. id: convertToGraphQLId('User', 4),
  328. })
  329. mockPermissions(['ticket.agent'])
  330. const view = await visitView('/tickets/1')
  331. await waitUntilTicketLoaded()
  332. await mockTicketLiveUsersSubscription.next({
  333. data: {
  334. ticketLiveUserUpdates: {
  335. liveUsers: [
  336. {
  337. user: {
  338. id: 'gid://zammad/User/4',
  339. firstname: 'Agent 1',
  340. lastname: 'Test',
  341. fullname: 'Agent 1 Test',
  342. __typename: 'User',
  343. },
  344. apps: [
  345. {
  346. name: 'mobile',
  347. editing: false,
  348. lastInteraction: '2022-02-01T10:55:26Z',
  349. __typename: 'TicketLiveUserApp',
  350. },
  351. ],
  352. __typename: 'TicketLiveUser',
  353. },
  354. {
  355. user: {
  356. id: 'gid://zammad/User/160',
  357. firstname: 'John',
  358. lastname: 'Doe',
  359. fullname: 'John Doe',
  360. __typename: 'User',
  361. },
  362. apps: [
  363. {
  364. name: 'desktop',
  365. editing: false,
  366. lastInteraction: '2022-01-31T10:30:24Z',
  367. __typename: 'TicketLiveUserApp',
  368. },
  369. {
  370. name: 'mobile',
  371. editing: false,
  372. lastInteraction: '2022-01-31T16:45:53Z',
  373. __typename: 'TicketLiveUserApp',
  374. },
  375. ],
  376. __typename: 'TicketLiveUser',
  377. },
  378. {
  379. user: {
  380. id: 'gid://zammad/User/165',
  381. firstname: 'Rose',
  382. lastname: 'Nylund',
  383. fullname: 'Rose Nylund',
  384. __typename: 'User',
  385. },
  386. apps: [
  387. {
  388. name: 'mobile',
  389. editing: false,
  390. lastInteraction: '2022-01-31T16:45:53Z',
  391. __typename: 'TicketLiveUserApp',
  392. },
  393. ],
  394. __typename: 'TicketLiveUser',
  395. },
  396. ],
  397. __typename: 'TicketLiveUserUpdatesPayload',
  398. },
  399. },
  400. })
  401. const counter = view.getByLabelText(/Ticket has 2 viewers/)
  402. expect(counter, 'has a counter').toBeInTheDocument()
  403. expect(counter).toHaveTextContent('+1')
  404. await view.events.click(view.getByTitle('Show ticket viewers'))
  405. await waitUntil(() =>
  406. view.queryByRole('dialog', { name: 'Ticket viewers' }),
  407. )
  408. expect(view.getByText('Opened in tabs')).toBeInTheDocument()
  409. expect(
  410. view.queryByRole('dialog', { name: 'Ticket viewers' }),
  411. ).toHaveTextContent('John Doe')
  412. expect(view.queryByIconName('mobile-desktop')).not.toBeInTheDocument()
  413. await mockTicketLiveUsersSubscription.next({
  414. data: {
  415. ticketLiveUserUpdates: {
  416. liveUsers: [
  417. {
  418. user: {
  419. id: 'gid://zammad/User/160',
  420. firstname: 'John',
  421. lastname: 'Doe',
  422. fullname: 'John Doe',
  423. __typename: 'User',
  424. },
  425. apps: [
  426. {
  427. name: 'desktop',
  428. editing: false,
  429. lastInteraction: '2022-01-31T18:30:24Z',
  430. __typename: 'TicketLiveUserApp',
  431. },
  432. {
  433. name: 'mobile',
  434. editing: false,
  435. lastInteraction: '2022-01-31T16:45:53Z',
  436. __typename: 'TicketLiveUserApp',
  437. },
  438. ],
  439. __typename: 'TicketLiveUser',
  440. },
  441. ],
  442. __typename: 'TicketLiveUserUpdatesPayload',
  443. },
  444. },
  445. })
  446. expect(view.queryByIconName('mobile-desktop')).toBeInTheDocument()
  447. })
  448. it('editing has always the highest priority', async () => {
  449. const { waitUntilTicketLoaded, mockTicketLiveUsersSubscription } =
  450. mockTicketDetailViewGql()
  451. mockAccount({
  452. lastname: 'Doe',
  453. firstname: 'John',
  454. fullname: 'John Doe',
  455. id: convertToGraphQLId('User', 4),
  456. })
  457. mockPermissions(['ticket.agent'])
  458. const view = await visitView('/tickets/1')
  459. await waitUntilTicketLoaded()
  460. await mockTicketLiveUsersSubscription.next({
  461. data: {
  462. ticketLiveUserUpdates: {
  463. liveUsers: [
  464. {
  465. user: {
  466. id: 'gid://zammad/User/160',
  467. firstname: 'John',
  468. lastname: 'Doe',
  469. fullname: 'John Doe',
  470. __typename: 'User',
  471. },
  472. apps: [
  473. {
  474. name: 'desktop',
  475. editing: true,
  476. lastInteraction: '2022-01-31T10:30:24Z',
  477. __typename: 'TicketLiveUserApp',
  478. },
  479. {
  480. name: 'mobile',
  481. editing: false,
  482. lastInteraction: '2022-01-31T16:45:53Z',
  483. __typename: 'TicketLiveUserApp',
  484. },
  485. ],
  486. __typename: 'TicketLiveUser',
  487. },
  488. ],
  489. __typename: 'TicketLiveUserUpdatesPayload',
  490. },
  491. },
  492. })
  493. await view.events.click(view.getByTitle('Show ticket viewers'))
  494. await waitUntil(() =>
  495. view.queryByRole('dialog', { name: 'Ticket viewers' }),
  496. )
  497. expect(
  498. view.queryByRole('dialog', { name: 'Ticket viewers' }),
  499. ).toHaveTextContent('John Doe')
  500. expect(view.queryByIconName('mobile-desktop-edit')).toBeInTheDocument()
  501. })
  502. it('show current user avatar when editing on other device', async () => {
  503. const { waitUntilTicketLoaded, mockTicketLiveUsersSubscription } =
  504. mockTicketDetailViewGql()
  505. mockAccount({
  506. lastname: 'Doe',
  507. firstname: 'John',
  508. fullname: 'John Doe',
  509. id: convertToGraphQLId('User', 4),
  510. })
  511. mockPermissions(['ticket.agent'])
  512. const view = await visitView('/tickets/1')
  513. await waitUntilTicketLoaded()
  514. await mockTicketLiveUsersSubscription.next({
  515. data: {
  516. ticketLiveUserUpdates: {
  517. liveUsers: [
  518. {
  519. user: {
  520. id: 'gid://zammad/User/4',
  521. firstname: 'Agent 1',
  522. lastname: 'Test',
  523. fullname: 'Agent 1 Test',
  524. __typename: 'User',
  525. },
  526. apps: [
  527. {
  528. name: 'mobile',
  529. editing: false,
  530. lastInteraction: '2022-02-01T10:55:26Z',
  531. __typename: 'TicketLiveUserApp',
  532. },
  533. {
  534. name: 'desktop',
  535. editing: true,
  536. lastInteraction: '2022-02-01T09:55:26Z',
  537. __typename: 'TicketLiveUserApp',
  538. },
  539. ],
  540. __typename: 'TicketLiveUser',
  541. },
  542. ],
  543. __typename: 'TicketLiveUserUpdatesPayload',
  544. },
  545. },
  546. })
  547. await view.events.click(view.getByTitle('Show ticket viewers'))
  548. await waitUntil(() =>
  549. view.queryByRole('dialog', { name: 'Ticket viewers' }),
  550. )
  551. expect(
  552. view.queryByRole('dialog', { name: 'Ticket viewers' }),
  553. ).toHaveTextContent('Agent 1 Test')
  554. expect(view.queryByIconName('mobile-desktop-edit')).toBeInTheDocument()
  555. })
  556. it('customer should only add live user entry but not subscribe', async () => {
  557. const {
  558. waitUntilTicketLoaded,
  559. mockTicketLiveUserUpsert,
  560. mockTicketLiveUsersSubscription,
  561. } = mockTicketDetailViewGql()
  562. mockAccount({
  563. lastname: 'Braun',
  564. firstname: 'Nicole',
  565. fullname: 'Nicole Braun',
  566. id: convertToGraphQLId('User', 3),
  567. })
  568. mockPermissions(['ticket.customer'])
  569. const view = await visitView('/tickets/1')
  570. await waitUntilTicketLoaded()
  571. await waitUntil(() => mockTicketLiveUserUpsert.calls.resolve === 1)
  572. await mockTicketLiveUsersSubscription.next({
  573. data: {
  574. ticketLiveUserUpdates: {
  575. liveUsers: [
  576. {
  577. user: {
  578. id: 'gid://zammad/User/160',
  579. firstname: 'John',
  580. lastname: 'Doe',
  581. fullname: 'John Doe',
  582. __typename: 'User',
  583. },
  584. apps: [
  585. {
  586. name: 'desktop',
  587. editing: false,
  588. lastInteraction: '2022-01-31T18:30:24Z',
  589. __typename: 'TicketLiveUserApp',
  590. },
  591. {
  592. name: 'mobile',
  593. editing: false,
  594. lastInteraction: '2022-01-31T16:45:53Z',
  595. __typename: 'TicketLiveUserApp',
  596. },
  597. ],
  598. __typename: 'TicketLiveUser',
  599. },
  600. ],
  601. __typename: 'TicketLiveUserUpdatesPayload',
  602. },
  603. },
  604. })
  605. expect(view.queryByTitle('Show ticket viewers')).not.toBeInTheDocument()
  606. })
  607. })