ticket-create.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { waitFor, within } from '@testing-library/vue'
  3. import ticketCustomerObjectAttributes from '#tests/graphql/factories/fixtures/ticket-customer-object-attributes.ts'
  4. import { visitView } from '#tests/support/components/visitView.ts'
  5. import { mockApplicationConfig } from '#tests/support/mock-applicationConfig.ts'
  6. import { mockPermissions } from '#tests/support/mock-permissions.ts'
  7. import { mockObjectManagerFrontendAttributesQuery } from '#shared/entities/object-attributes/graphql/queries/objectManagerFrontendAttributes.mocks.ts'
  8. import { waitForTicketCreateMutationCalls } from '#shared/entities/ticket/graphql/mutations/create.mocks.ts'
  9. import {
  10. EnumTaskbarEntity,
  11. EnumTaskbarEntityAccess,
  12. } from '#shared/graphql/types.ts'
  13. import { convertToGraphQLId } from '#shared/graphql/utils.ts'
  14. import getUuid from '#shared/utils/getUuid.ts'
  15. import { waitForUserCurrentTaskbarItemUpdateMutationCalls } from '#desktop/entities/user/current/graphql/mutations/userCurrentTaskbarItemUpdate.mocks.ts'
  16. import { mockUserCurrentTaskbarItemListQuery } from '#desktop/entities/user/current/graphql/queries/userCurrentTaskbarItemList.mocks.ts'
  17. import {
  18. handleMockUserQuery,
  19. handleCustomerMock,
  20. handleMockFormUpdaterQuery,
  21. rendersFields,
  22. handleMockOrganizationQuery,
  23. } from '#desktop/pages/ticket/__tests__/support/ticket-create-helpers.ts'
  24. vi.hoisted(() => {
  25. vi.setSystemTime('2024-11-11T00:00:00Z')
  26. })
  27. describe('ticket create view', async () => {
  28. describe('view with granted access', async () => {
  29. beforeEach(() => {
  30. mockApplicationConfig({
  31. ui_task_mananger_max_task_count: 30,
  32. ui_ticket_create_available_types: [
  33. 'phone-in',
  34. 'phone-out',
  35. 'email-out',
  36. ],
  37. })
  38. mockPermissions(['ticket.agent'])
  39. })
  40. // FIXME: This test example has to be first, otherwise it will start to fail.
  41. // This is probably due to the leftover router instance, which has to be set up in a different way for this test.
  42. it('supports updating dirty flag in the associated taskbar tab', async () => {
  43. const uid = getUuid()
  44. mockUserCurrentTaskbarItemListQuery({
  45. userCurrentTaskbarItemList: [
  46. {
  47. __typename: 'UserTaskbarItem',
  48. id: convertToGraphQLId('Taskbar', 1),
  49. key: `TicketCreateScreen-${uid}`,
  50. callback: EnumTaskbarEntity.TicketCreate,
  51. entityAccess: EnumTaskbarEntityAccess.Granted,
  52. entity: {
  53. __typename: 'UserTaskbarItemEntityTicketCreate',
  54. uid,
  55. title: '',
  56. createArticleTypeKey: 'phone-in',
  57. },
  58. dirty: false,
  59. },
  60. ],
  61. })
  62. handleMockFormUpdaterQuery()
  63. const view = await visitView(`/ticket/create/${uid}`)
  64. expect(
  65. await view.findByRole('button', { name: 'Cancel & Go Back' }),
  66. ).toBeInTheDocument()
  67. await view.events.type(view.getByLabelText('Title'), 'Test Ticket')
  68. const calls = await waitForUserCurrentTaskbarItemUpdateMutationCalls()
  69. expect(calls.at(-1)?.variables).toEqual(
  70. expect.objectContaining({
  71. input: expect.objectContaining({
  72. dirty: true,
  73. }),
  74. }),
  75. )
  76. })
  77. it('renders view correctly', async () => {
  78. const view = await visitView('/ticket/create')
  79. expect(
  80. await view.findByRole('heading', { level: 1, name: 'New Ticket' }),
  81. ).toBeInTheDocument()
  82. expect(view.getByRole('tablist')).toBeInTheDocument()
  83. // Default tab is the first one
  84. expect(
  85. view.getByRole('tab', { selected: true, name: 'Received Call' }),
  86. ).toBeInTheDocument()
  87. rendersFields(view)
  88. })
  89. it('cancels ticket creation', async () => {
  90. const view = await visitView('/ticket/create')
  91. expect(
  92. await view.findByRole('heading', { level: 1, name: 'New Ticket' }),
  93. ).toBeInTheDocument()
  94. await view.events.click(
  95. view.getByRole('button', { name: 'Cancel & Go Back' }),
  96. )
  97. await waitFor(() =>
  98. expect(
  99. view.queryByRole('heading', { level: 1, name: 'New Ticket' }),
  100. ).not.toBeInTheDocument(),
  101. )
  102. })
  103. it('shows send email article type', async () => {
  104. const view = await visitView('/ticket/create')
  105. await view.events.click(await view.findByText('Send Email'))
  106. expect(
  107. view.getByRole('tab', { selected: true, name: 'Send Email' }),
  108. ).toBeInTheDocument()
  109. expect(view.getByLabelText('CC')).toBeInTheDocument()
  110. rendersFields(view)
  111. })
  112. it('shows outbound call article type', async () => {
  113. const view = await visitView('/ticket/create')
  114. await view.events.click(await view.findByText('Outbound Call'))
  115. expect(
  116. view.getByRole('tab', { selected: true, name: 'Outbound Call' }),
  117. ).toBeInTheDocument()
  118. rendersFields(view)
  119. })
  120. it('detects duplicate ticket', async () => {
  121. await mockApplicationConfig({
  122. ticket_duplicate_detection: true,
  123. ticket_duplicate_detection_title: 'Similar tickets found',
  124. ticket_duplicate_detection_body:
  125. 'Tickets with the same attributes were found.',
  126. })
  127. handleMockFormUpdaterQuery({
  128. ticket_duplicate_detection: {
  129. show: true,
  130. hidden: false,
  131. value: { count: 1, items: [[1, '123,', 'foo title']] },
  132. },
  133. })
  134. const view = await visitView('/ticket/create')
  135. await view.events.type(await view.findByLabelText('Title'), 'foo title')
  136. await waitFor(() =>
  137. expect(view.getByText('Similar tickets found')).toBeInTheDocument(),
  138. )
  139. expect(view.getByTestId('common-alert')).toHaveTextContent('foo title')
  140. expect(view.getByIconName('exclamation-triangle')).toBeInTheDocument()
  141. expect(
  142. view.getByText('Tickets with the same attributes were found.'),
  143. ).toBeInTheDocument()
  144. })
  145. it('prevents submission on incomplete form', async () => {
  146. handleMockFormUpdaterQuery()
  147. const view = await visitView('/ticket/create')
  148. await view.events.type(await view.findByLabelText('Title'), 'Test Ticket')
  149. await view.events.click(view.getByRole('button', { name: 'Create' }))
  150. expect(await view.findAllByText('This field is required.')).toHaveLength(
  151. 4,
  152. )
  153. })
  154. it('discards unsaved changes', async () => {
  155. handleMockFormUpdaterQuery()
  156. const view = await visitView('/ticket/create')
  157. expect(
  158. await view.findByRole('button', { name: 'Cancel & Go Back' }),
  159. ).toBeInTheDocument()
  160. await view.events.type(view.getByLabelText('Title'), 'Test Ticket')
  161. await waitFor(() =>
  162. expect(
  163. view.queryByRole('button', { name: 'Cancel & Go Back' }),
  164. ).not.toBeInTheDocument(),
  165. )
  166. await view.events.click(
  167. await view.findByRole('button', { name: 'Discard Changes' }),
  168. )
  169. const dialog = await view.findByRole('dialog', {
  170. name: 'Unsaved Changes',
  171. })
  172. expect(dialog).toBeInTheDocument()
  173. const dialogView = within(dialog)
  174. expect(
  175. await dialogView.findByText(
  176. 'Are you sure? You have unsaved changes that will get lost.',
  177. ),
  178. )
  179. await view.events.click(
  180. dialogView.getByRole('button', { name: 'Discard Changes' }),
  181. )
  182. // should not be in the document anymore
  183. await waitFor(() =>
  184. expect(view.getByLabelText('Title')).not.toHaveValue('Test Ticket'),
  185. )
  186. })
  187. it('keeps form values on cancel unsaved changes', async () => {
  188. handleMockFormUpdaterQuery()
  189. const view = await visitView('/ticket/create')
  190. await view.events.type(await view.findByLabelText('Title'), 'Test Ticket')
  191. await view.events.click(
  192. await view.findByRole('button', { name: 'Discard Changes' }),
  193. )
  194. const dialog = await view.findByRole('dialog', {
  195. name: 'Unsaved Changes',
  196. })
  197. const dialogView = within(dialog)
  198. await view.events.click(
  199. dialogView.getByRole('button', { name: 'Cancel & Go Back' }),
  200. )
  201. expect(view.getByText('Test Ticket')).toBeInTheDocument()
  202. })
  203. it('creates a new ticket', async () => {
  204. handleMockFormUpdaterQuery()
  205. const view = await visitView('/ticket/create')
  206. await view.events.type(await view.findByLabelText('Title'), 'Test Ticket')
  207. // Page title updates when title is set
  208. expect(
  209. await view.findByRole('heading', { level: 1, name: 'Test Ticket' }),
  210. ).toBeInTheDocument()
  211. // Page title defaults back when title is cleared
  212. await view.events.clear(view.getByLabelText('Title'))
  213. await waitFor(() =>
  214. expect(
  215. view.getByRole('heading', { level: 1, name: 'New Ticket' }),
  216. ).toBeInTheDocument(),
  217. )
  218. await view.events.type(view.getByLabelText('Title'), 'Test Ticket')
  219. // Customer field
  220. await handleCustomerMock(view)
  221. handleMockUserQuery()
  222. await view.events.click(
  223. view.getByRole('option', {
  224. name: 'Avatar (Nicole Braun) Nicole Braun – Zammad Foundation',
  225. }),
  226. )
  227. // Sidebar CUSTOMER
  228. expect(view.getByLabelText('Avatar (Nicole Braun)')).toBeInTheDocument()
  229. expect(view.getByText('Zammad Foundation')).toBeInTheDocument()
  230. expect(view.getByText('open tickets')).toBeInTheDocument()
  231. expect(view.getByText('nicole.braun@zammad.org')).toBeInTheDocument()
  232. expect(view.getByText('closed tickets')).toBeInTheDocument()
  233. expect(view.getByLabelText('Open tickets')).toHaveTextContent('17')
  234. // Sidebar Organization
  235. handleMockOrganizationQuery()
  236. await view.events.click(view.getByLabelText('Organization'))
  237. expect(view.getByText('Organization')).toBeInTheDocument()
  238. expect(view.getByText('Members')).toBeInTheDocument()
  239. expect(view.getByLabelText('Avatar (Nicole Braun)')).toBeInTheDocument()
  240. // Text field
  241. await view.events.type(
  242. view.getByRole('textbox', { name: 'Text' }),
  243. 'Test ticket text',
  244. )
  245. // Group field
  246. await view.events.click(view.getByLabelText('Group'))
  247. await view.events.click(view.getByRole('option', { name: 'Users' }))
  248. // State field
  249. await view.events.click(view.getByLabelText('Priority'))
  250. await view.events.click(view.getByRole('option', { name: '2 normal' }))
  251. // Priority Field
  252. await view.events.click(view.getByLabelText('State'))
  253. await view.events.click(
  254. view.getByRole('option', { name: 'pending reminder' }),
  255. )
  256. // Date selection Field on pending reminder
  257. await view.events.click(view.getByText('Pending till'))
  258. await waitFor(() => expect(view.getByRole('dialog')).toBeInTheDocument())
  259. const dateCells: Element[] = view.getAllByRole('gridcell', { name: '29' })
  260. await view.events.click(<Element>dateCells.at(-1))
  261. // Submission
  262. await view.events.click(view.getByRole('button', { name: 'Create' }))
  263. const calls = await waitForTicketCreateMutationCalls()
  264. expect(calls.at(-1)?.variables).toEqual({
  265. input: {
  266. article: {
  267. body: 'Test ticket text',
  268. cc: undefined,
  269. contentType: 'text/html',
  270. security: undefined,
  271. sender: 'Customer',
  272. type: 'phone',
  273. },
  274. customer: {
  275. id: 'gid://zammad/User/2',
  276. },
  277. groupId: 'gid://zammad/Group/1',
  278. objectAttributeValues: [],
  279. pendingTime: '2024-11-29T00:00:00.000Z',
  280. priorityId: 'gid://zammad/Ticket::Priority/2',
  281. stateId: 'gid://zammad/Ticket::State/3',
  282. title: 'Test Ticket',
  283. },
  284. })
  285. await waitFor(() =>
  286. expect(
  287. view.getByText('Ticket has been created successfully.'),
  288. ).toBeInTheDocument(),
  289. )
  290. })
  291. })
  292. describe('with customer permission', () => {
  293. beforeEach(() => {
  294. mockPermissions(['ticket.customer'])
  295. })
  296. describe('view disabled customer ticket create', async () => {
  297. beforeEach(() => {
  298. mockApplicationConfig({
  299. customer_ticket_create: false,
  300. })
  301. mockPermissions(['ticket.customer'])
  302. })
  303. it('redirects to error page', async () => {
  304. const view = await visitView('/ticket/create')
  305. expect(view.getByText('Not Found')).toBeInTheDocument()
  306. expect(view.getByText("This page doesn't exist.")).toBeInTheDocument()
  307. })
  308. })
  309. describe('view enabled customer ticket create', async () => {
  310. beforeEach(() => {
  311. mockApplicationConfig({
  312. customer_ticket_create: true,
  313. })
  314. })
  315. it('creates a new ticket', async () => {
  316. // Mock frontend attributes for customer context.
  317. // TODO: check if we can mock the query twice based on the variable?
  318. mockObjectManagerFrontendAttributesQuery({
  319. objectManagerFrontendAttributes: ticketCustomerObjectAttributes(),
  320. })
  321. handleMockFormUpdaterQuery()
  322. const view = await visitView('/ticket/create')
  323. await view.events.type(
  324. await view.findByLabelText('Title'),
  325. 'Test Customer Ticket',
  326. )
  327. // Text field
  328. await view.events.type(
  329. view.getByRole('textbox', { name: 'Text' }),
  330. 'Test customer ticket text',
  331. )
  332. await view.events.click(view.getByLabelText('Group'))
  333. await view.events.click(view.getByRole('option', { name: 'Users' }))
  334. // Submission
  335. await view.events.click(view.getByRole('button', { name: 'Create' }))
  336. const calls = await waitForTicketCreateMutationCalls()
  337. expect(calls.at(-1)?.variables).toEqual({
  338. input: {
  339. article: {
  340. body: 'Test customer ticket text',
  341. cc: undefined,
  342. contentType: 'text/html',
  343. security: undefined,
  344. sender: 'Customer',
  345. type: 'web',
  346. },
  347. customer: undefined,
  348. groupId: 'gid://zammad/Group/1',
  349. objectAttributeValues: [],
  350. stateId: 'gid://zammad/Ticket::State/2',
  351. title: 'Test Customer Ticket',
  352. },
  353. })
  354. await waitFor(() =>
  355. expect(
  356. view.getByText('Ticket has been created successfully.'),
  357. ).toBeInTheDocument(),
  358. )
  359. })
  360. })
  361. })
  362. })