ticket-create-shared-drafts.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { getNode } from '@formkit/core'
  3. import { waitFor, within } from '@testing-library/vue'
  4. import ticketCustomerObjectAttributes from '#tests/graphql/factories/fixtures/ticket-customer-object-attributes.ts'
  5. import { visitView } from '#tests/support/components/visitView.ts'
  6. import { mockApplicationConfig } from '#tests/support/mock-applicationConfig.ts'
  7. import { mockPermissions } from '#tests/support/mock-permissions.ts'
  8. import { waitForNextTick } from '#tests/support/utils.ts'
  9. import { waitForFormUpdaterQueryCalls } from '#shared/components/Form/graphql/queries/formUpdater.mocks.ts'
  10. import { mockObjectManagerFrontendAttributesQuery } from '#shared/entities/object-attributes/graphql/queries/objectManagerFrontendAttributes.mocks.ts'
  11. import { waitForTicketCreateMutationCalls } from '#shared/entities/ticket/graphql/mutations/create.mocks.ts'
  12. import { waitForTicketSharedDraftStartCreateMutationCalls } from '#shared/entities/ticket-shared-draft-start/graphql/mutations/ticketSharedDraftStartCreate.mocks.ts'
  13. import { waitForTicketSharedDraftStartDeleteMutationCalls } from '#shared/entities/ticket-shared-draft-start/graphql/mutations/ticketSharedDraftStartDelete.mocks.ts'
  14. import { waitForTicketSharedDraftStartUpdateMutationCalls } from '#shared/entities/ticket-shared-draft-start/graphql/mutations/ticketSharedDraftStartUpdate.mocks.ts'
  15. import {
  16. mockTicketSharedDraftStartListQuery,
  17. waitForTicketSharedDraftStartListQueryCalls,
  18. } from '#shared/entities/ticket-shared-draft-start/graphql/queries/ticketSharedDraftStartList.mocks.ts'
  19. import {
  20. mockTicketSharedDraftStartSingleQuery,
  21. waitForTicketSharedDraftStartSingleQueryCalls,
  22. } from '#shared/entities/ticket-shared-draft-start/graphql/queries/ticketSharedDraftStartSingle.mocks.ts'
  23. import { getTicketSharedDraftStartUpdateByGroupSubscriptionHandler } from '#shared/entities/ticket-shared-draft-start/graphql/subscriptions/ticketSharedDraftStartUpdateByGroup.mocks.ts'
  24. import {
  25. convertToGraphQLId,
  26. getIdFromGraphQLId,
  27. } from '#shared/graphql/utils.ts'
  28. import { handleMockFormUpdaterQuery } from '#desktop/pages/ticket/__tests__/support/ticket-create-helpers.ts'
  29. vi.hoisted(() => {
  30. vi.setSystemTime('2024-07-03T13:48:09Z')
  31. })
  32. describe('ticket create view - shared drafts sidebar', async () => {
  33. describe('with agent permissions', async () => {
  34. beforeEach(() => {
  35. mockApplicationConfig({
  36. ui_ticket_create_available_types: [
  37. 'phone-in',
  38. 'phone-out',
  39. 'email-out',
  40. ],
  41. })
  42. mockPermissions(['ticket.agent'])
  43. handleMockFormUpdaterQuery()
  44. })
  45. it('supports creating shared drafts', async () => {
  46. const view = await visitView('/ticket/create')
  47. await view.events.type(
  48. await view.findByLabelText('Text'),
  49. 'foobar<div data-signature="true">Signature here</div>',
  50. )
  51. const formUpdaterCalls = await waitForFormUpdaterQueryCalls()
  52. await vi.waitUntil(() => formUpdaterCalls.length === 2)
  53. mockTicketSharedDraftStartListQuery({
  54. ticketSharedDraftStartList: [],
  55. })
  56. await view.events.click(view.getByLabelText('Group'))
  57. await view.events.click(view.getByRole('option', { name: 'Users' }))
  58. await waitForTicketSharedDraftStartListQueryCalls()
  59. const aside = within(
  60. view.getByRole('complementary', {
  61. name: 'Content sidebar',
  62. }),
  63. )
  64. await view.events.type(
  65. aside.getByLabelText('Create a shared draft'),
  66. 'Test shared draft 1',
  67. )
  68. await getNode('sharedDraftTitle')?.settled
  69. await view.events.click(
  70. aside.getByRole('link', { name: 'Create Shared Draft' }),
  71. )
  72. const calls = await waitForTicketSharedDraftStartCreateMutationCalls()
  73. expect(calls.at(-1)?.variables).toEqual({
  74. name: 'Test shared draft 1',
  75. input: expect.objectContaining({
  76. groupId: convertToGraphQLId('Group', 1),
  77. content: expect.objectContaining({
  78. body: 'foobar',
  79. }),
  80. }),
  81. })
  82. expect(view.getByRole('alert')).toHaveTextContent(
  83. 'Shared draft has been created successfully.',
  84. )
  85. await getTicketSharedDraftStartUpdateByGroupSubscriptionHandler().trigger(
  86. {
  87. ticketSharedDraftStartUpdateByGroup: {
  88. sharedDraftStarts: [
  89. {
  90. id: convertToGraphQLId('Ticket::SharedDraftStart', 1),
  91. name: 'Test shared draft 1',
  92. updatedAt: '2024-07-03T13:48:09Z',
  93. updatedBy: {
  94. fullname: 'Erika Mustermann',
  95. },
  96. },
  97. ],
  98. },
  99. },
  100. )
  101. await waitForNextTick()
  102. expect(
  103. aside.getByRole('link', { name: 'Test shared draft 1' }),
  104. ).toBeInTheDocument()
  105. })
  106. it('supports applying shared drafts', async () => {
  107. const view = await visitView('/ticket/create')
  108. const draftToMock = {
  109. id: convertToGraphQLId('Ticket::SharedDraftStart', 1),
  110. name: 'Test shared draft 1',
  111. content: {
  112. title: 'foobar',
  113. customer_id: 'test@example.com',
  114. body: 'body',
  115. },
  116. updatedAt: '2024-07-03T13:48:09Z',
  117. updatedBy: {
  118. fullname: 'Erika Mustermann',
  119. },
  120. }
  121. mockTicketSharedDraftStartListQuery({
  122. ticketSharedDraftStartList: [draftToMock],
  123. })
  124. await view.events.click(await view.findByLabelText('Group'))
  125. await view.events.click(view.getByRole('option', { name: 'Users' }))
  126. await waitForTicketSharedDraftStartListQueryCalls()
  127. const aside = within(
  128. view.getByRole('complementary', {
  129. name: 'Content sidebar',
  130. }),
  131. )
  132. mockTicketSharedDraftStartSingleQuery({
  133. ticketSharedDraftStartSingle: draftToMock,
  134. })
  135. await view.events.click(
  136. aside.getByRole('link', { name: draftToMock.name }),
  137. )
  138. await waitForTicketSharedDraftStartSingleQueryCalls()
  139. const flyout = within(
  140. view.getByRole('complementary', {
  141. name: 'Preview Shared Draft',
  142. }),
  143. )
  144. expect(
  145. flyout.getByText(draftToMock.updatedBy.fullname),
  146. ).toBeInTheDocument()
  147. expect(flyout.getByText('just now')).toBeInTheDocument()
  148. expect(flyout.getByText(draftToMock.content.body)).toBeInTheDocument()
  149. await view.events.click(flyout.getByRole('button', { name: 'Apply' }))
  150. expect(
  151. await view.findByRole('dialog', { name: 'Apply Draft' }),
  152. ).toBeInTheDocument()
  153. const dialog = within(
  154. view.getByRole('dialog', {
  155. name: 'Apply Draft',
  156. }),
  157. )
  158. handleMockFormUpdaterQuery({
  159. title: { value: draftToMock.content.title },
  160. customer_id: {
  161. value: draftToMock.content.customer_id,
  162. options: [{ value: draftToMock.content.customer_id }],
  163. },
  164. body: { value: draftToMock.content.body },
  165. pending_time: { show: false },
  166. shared_draft_id: { value: getIdFromGraphQLId(draftToMock.id) },
  167. })
  168. await view.events.click(
  169. dialog.getByRole('button', { name: 'Overwrite Content' }),
  170. )
  171. await waitFor(() => {
  172. expect(
  173. view.queryByRole('dialog', {
  174. name: 'Apply Draft',
  175. }),
  176. ).not.toBeInTheDocument()
  177. })
  178. const formUpdaterCalls = await waitForFormUpdaterQueryCalls()
  179. expect(formUpdaterCalls.at(-1)?.variables).toEqual(
  180. expect.objectContaining({
  181. meta: expect.objectContaining({
  182. additionalData: expect.objectContaining({
  183. sharedDraftId: draftToMock.id,
  184. draftType: 'start',
  185. }),
  186. }),
  187. }),
  188. )
  189. await waitForNextTick()
  190. expect(view.getByLabelText('Title')).toHaveValue(
  191. draftToMock.content.title,
  192. )
  193. await view.events.click(view.getByRole('button', { name: 'Create' }))
  194. const ticketCreateCalls = await waitForTicketCreateMutationCalls()
  195. expect(ticketCreateCalls.at(-1)?.variables).toEqual(
  196. expect.objectContaining({
  197. input: expect.objectContaining({
  198. title: draftToMock.content.title,
  199. sharedDraftId: draftToMock.id,
  200. }),
  201. }),
  202. )
  203. })
  204. it('supports updating shared drafts', async () => {
  205. const view = await visitView('/ticket/create')
  206. mockTicketSharedDraftStartListQuery({
  207. ticketSharedDraftStartList: [
  208. {
  209. id: convertToGraphQLId('Ticket::SharedDraftStart', 1),
  210. name: 'Test shared draft 1',
  211. updatedAt: '2024-07-03T13:48:09Z',
  212. updatedBy: {
  213. fullname: 'Erika Mustermann',
  214. },
  215. },
  216. ],
  217. })
  218. await view.events.click(await view.findByLabelText('Group'))
  219. await view.events.click(view.getByRole('option', { name: 'Users' }))
  220. await waitForTicketSharedDraftStartListQueryCalls()
  221. const aside = within(
  222. view.getByRole('complementary', {
  223. name: 'Content sidebar',
  224. }),
  225. )
  226. mockTicketSharedDraftStartSingleQuery({
  227. ticketSharedDraftStartSingle: {
  228. name: 'Test shared draft 1',
  229. content: {
  230. body: 'foobar',
  231. },
  232. updatedAt: '2024-07-03T13:48:09Z',
  233. updatedBy: {
  234. fullname: 'Erika Mustermann',
  235. },
  236. },
  237. })
  238. await view.events.click(
  239. aside.getByRole('link', { name: 'Test shared draft 1' }),
  240. )
  241. await waitForTicketSharedDraftStartSingleQueryCalls()
  242. const flyout = within(
  243. view.getByRole('complementary', {
  244. name: 'Preview Shared Draft',
  245. }),
  246. )
  247. await view.events.click(flyout.getByRole('button', { name: 'Apply' }))
  248. const dialog = within(
  249. await view.findByRole('dialog', { name: 'Apply Draft' }),
  250. )
  251. handleMockFormUpdaterQuery({
  252. shared_draft_id: {
  253. value: 1,
  254. },
  255. body: {
  256. value: 'foobar',
  257. },
  258. })
  259. await view.events.click(
  260. dialog.getByRole('button', { name: 'Overwrite Content' }),
  261. )
  262. await waitFor(() => {
  263. expect(
  264. view.queryByRole('complementary', {
  265. name: 'Preview Shared Draft',
  266. }),
  267. ).not.toBeInTheDocument()
  268. })
  269. await view.events.click(
  270. aside.getByRole('button', { name: 'Update Shared Draft' }),
  271. )
  272. const calls = await waitForTicketSharedDraftStartUpdateMutationCalls()
  273. expect(calls.at(-1)?.variables).toEqual({
  274. sharedDraftId: convertToGraphQLId('Ticket::SharedDraftStart', 1),
  275. input: expect.objectContaining({
  276. content: expect.objectContaining({
  277. body: 'foobar',
  278. }),
  279. }),
  280. })
  281. expect(view.getByRole('alert')).toHaveTextContent(
  282. 'Shared draft has been updated successfully.',
  283. )
  284. expect(
  285. aside.getByRole('button', { name: 'Update Shared Draft' }),
  286. ).toBeInTheDocument()
  287. })
  288. it('supports deleting shared drafts', async () => {
  289. const view = await visitView('/ticket/create')
  290. mockTicketSharedDraftStartListQuery({
  291. ticketSharedDraftStartList: [
  292. {
  293. id: convertToGraphQLId('Ticket::SharedDraftStart', 1),
  294. name: 'Test shared draft 1',
  295. updatedAt: '2024-07-03T13:48:09Z',
  296. updatedBy: {
  297. fullname: 'Erika Mustermann',
  298. },
  299. },
  300. ],
  301. })
  302. await view.events.click(await view.findByLabelText('Group'))
  303. await view.events.click(view.getByRole('option', { name: 'Users' }))
  304. await waitForTicketSharedDraftStartListQueryCalls()
  305. const aside = within(
  306. view.getByRole('complementary', {
  307. name: 'Content sidebar',
  308. }),
  309. )
  310. mockTicketSharedDraftStartSingleQuery({
  311. ticketSharedDraftStartSingle: {
  312. name: 'Test shared draft 1',
  313. content: {
  314. body: 'foobar',
  315. },
  316. updatedAt: '2024-07-03T13:48:09Z',
  317. updatedBy: {
  318. fullname: 'Erika Mustermann',
  319. },
  320. },
  321. })
  322. await view.events.click(
  323. aside.getByRole('link', { name: 'Test shared draft 1' }),
  324. )
  325. await waitForTicketSharedDraftStartSingleQueryCalls()
  326. const flyout = within(
  327. view.getByRole('complementary', {
  328. name: 'Preview Shared Draft',
  329. }),
  330. )
  331. await view.events.click(flyout.getByRole('button', { name: 'Delete' }))
  332. const dialog = within(
  333. await view.findByRole('dialog', { name: 'Delete Object' }),
  334. )
  335. await view.events.click(
  336. dialog.getByRole('button', { name: 'Delete Object' }),
  337. )
  338. const calls = await waitForTicketSharedDraftStartDeleteMutationCalls()
  339. expect(calls.at(-1)?.variables).toEqual({
  340. sharedDraftId: convertToGraphQLId('Ticket::SharedDraftStart', 1),
  341. })
  342. await waitFor(() => {
  343. expect(
  344. view.queryByRole('complementary', {
  345. name: 'Preview Shared Draft',
  346. }),
  347. ).not.toBeInTheDocument()
  348. })
  349. // FIXME: Check why returning an empty array triggers the following console error in test environment only.
  350. // Cache data may be lost when replacing the ticketSharedDraftStartList field of a Query object.
  351. await getTicketSharedDraftStartUpdateByGroupSubscriptionHandler().trigger(
  352. {
  353. ticketSharedDraftStartUpdateByGroup: {},
  354. },
  355. )
  356. expect(
  357. aside.queryByRole('link', { name: 'Test shared draft 1' }),
  358. ).not.toBeInTheDocument()
  359. })
  360. })
  361. describe('with customer permission', () => {
  362. beforeEach(() => {
  363. mockApplicationConfig({
  364. customer_ticket_create: true,
  365. })
  366. mockPermissions(['ticket.customer'])
  367. // Mock frontend attributes for customer context.
  368. mockObjectManagerFrontendAttributesQuery({
  369. objectManagerFrontendAttributes: ticketCustomerObjectAttributes(),
  370. })
  371. handleMockFormUpdaterQuery()
  372. })
  373. it('does not show', async () => {
  374. const view = await visitView('/ticket/create')
  375. await view.events.click(await view.findByLabelText('Group'))
  376. await view.events.click(view.getByRole('option', { name: 'Users' }))
  377. await waitForFormUpdaterQueryCalls()
  378. expect(
  379. view.queryByRole('complementary', {
  380. name: 'Content sidebar',
  381. }),
  382. ).not.toBeInTheDocument()
  383. })
  384. })
  385. })