ticket-create.spec.ts 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
  2. import {
  3. ticketObjectAttributes,
  4. ticketArticleObjectAttributes,
  5. ticketPayload,
  6. } from '@mobile/entities/ticket/__tests__/mocks/ticket-mocks'
  7. import { defaultOrganization } from '@mobile/entities/organization/__tests__/mocks/organization-mocks'
  8. import { FormUpdaterDocument } from '@shared/components/Form/graphql/queries/formUpdater.api'
  9. import { ObjectManagerFrontendAttributesDocument } from '@shared/entities/object-attributes/graphql/queries/objectManagerFrontendAttributes.api'
  10. import { visitView } from '@tests/support/components/visitView'
  11. import { mockGraphQLApi } from '@tests/support/mock-graphql-api'
  12. import { mockPermissions } from '@tests/support/mock-permissions'
  13. import { mockAccount } from '@tests/support/mock-account'
  14. import type { ExtendedRenderResult } from '@tests/support/components'
  15. import { mockApplicationConfig } from '@tests/support/mock-applicationConfig'
  16. import { flushPromises } from '@vue/test-utils'
  17. import { nullableMock, waitUntil } from '@tests/support/utils'
  18. import { getTestRouter } from '@tests/support/components/renderComponent'
  19. import { getNode } from '@formkit/core'
  20. import { AutocompleteSearchUserDocument } from '@shared/components/Form/fields/FieldCustomer/graphql/queries/autocompleteSearch/user.api'
  21. import { TicketCreateDocument } from '../graphql/mutations/create.api'
  22. const visitTicketCreate = async () => {
  23. const mockObjectAttributes = mockGraphQLApi(
  24. ObjectManagerFrontendAttributesDocument,
  25. ).willBehave(({ object }) => {
  26. if (object === 'Ticket') {
  27. return {
  28. data: {
  29. objectManagerFrontendAttributes: ticketObjectAttributes(),
  30. },
  31. }
  32. }
  33. return {
  34. data: {
  35. objectManagerFrontendAttributes: ticketArticleObjectAttributes(),
  36. },
  37. }
  38. })
  39. const mockFormUpdater = mockGraphQLApi(FormUpdaterDocument).willResolve({
  40. formUpdater: {
  41. group_id: {
  42. show: true,
  43. options: [
  44. {
  45. label: 'Users',
  46. value: 1,
  47. },
  48. ],
  49. clearable: true,
  50. },
  51. owner_id: {
  52. show: true,
  53. options: [{ value: 100, label: 'Max Mustermann' }],
  54. },
  55. priority_id: {
  56. show: true,
  57. options: [
  58. { value: 1, label: '1 low' },
  59. { value: 2, label: '2 normal' },
  60. { value: 3, label: '3 high' },
  61. ],
  62. clearable: true,
  63. },
  64. pending_time: {
  65. show: false,
  66. required: false,
  67. hidden: false,
  68. disabled: false,
  69. },
  70. state_id: {
  71. show: true,
  72. options: [
  73. { value: 4, label: 'closed' },
  74. { value: 2, label: 'open' },
  75. { value: 7, label: 'pending close' },
  76. { value: 3, label: 'pending reminder' },
  77. ],
  78. clearable: true,
  79. },
  80. },
  81. })
  82. const view = await visitView('/tickets/create')
  83. await flushPromises()
  84. return { mockFormUpdater, mockObjectAttributes, view }
  85. }
  86. const mockTicketCreate = () => {
  87. return mockGraphQLApi(TicketCreateDocument).willResolve({
  88. ticketCreate: {
  89. ticket: ticketPayload(),
  90. errors: null,
  91. __typename: 'TicketCreatePayload',
  92. },
  93. })
  94. }
  95. const mockCustomerQueryResult = () => {
  96. return mockGraphQLApi(AutocompleteSearchUserDocument).willResolve({
  97. autocompleteSearchUser: [
  98. nullableMock({
  99. value: '2',
  100. label: 'Nicole Braun',
  101. labelPlaceholder: null,
  102. heading: 'Zammad Foundation',
  103. headingPlaceholder: null,
  104. disabled: null,
  105. icon: null,
  106. user: {
  107. id: 'gid://zammad/User/2',
  108. internalId: 2,
  109. firstname: 'Nicole',
  110. lastname: 'Braun',
  111. fullname: 'Nicole Braun',
  112. image: null,
  113. objectAttributeValues: [],
  114. organization: {
  115. id: 'gid://zammad/Organization/1',
  116. internalId: 1,
  117. name: 'Zammad Foundation',
  118. active: true,
  119. objectAttributeValues: [],
  120. __typename: 'Organization',
  121. },
  122. hasSecondaryOrganizations: false,
  123. __typename: 'User',
  124. },
  125. __typename: 'AutocompleteSearchUserEntry',
  126. }),
  127. ],
  128. })
  129. }
  130. const nextStep = async (view: ExtendedRenderResult) => {
  131. await view.events.click(view.getByRole('button', { name: 'Continue' }))
  132. }
  133. describe('Creating new ticket as agent', () => {
  134. beforeEach(() => {
  135. mockPermissions(['ticket.agent'])
  136. mockApplicationConfig({
  137. customer_ticket_create: true,
  138. ui_ticket_create_available_types: ['phone-in', 'phone-out', 'email-out'],
  139. ui_ticket_create_default_type: 'phone-in',
  140. })
  141. })
  142. it('shows 4 steps for agents', async () => {
  143. const { view } = await visitTicketCreate()
  144. const steps = ['1', '2', '3', '4']
  145. steps.forEach((step) => {
  146. expect(view.getByRole('button', { name: step })).toBeInTheDocument()
  147. })
  148. })
  149. it('disables the submit button if required data is missing', async () => {
  150. const { view } = await visitTicketCreate()
  151. expect(view.getByRole('button', { name: 'Create ticket' })).toBeDisabled()
  152. })
  153. it('invalidates a single step if required data is missing', async () => {
  154. const { mockFormUpdater, view } = await visitTicketCreate()
  155. await view.events.type(view.getByLabelText('Title'), 'Foobar')
  156. await waitUntil(() => mockFormUpdater.calls.resolve)
  157. await nextStep(view)
  158. await nextStep(view)
  159. await nextStep(view)
  160. expect(
  161. view.getByRole('status', { name: 'Invalid values in step 3' }),
  162. ).toBeInTheDocument()
  163. })
  164. it('redirects to detail view after successful ticket creation', async () => {
  165. const mockCustomer = mockCustomerQueryResult()
  166. const mockTicket = mockTicketCreate()
  167. const { mockFormUpdater, view } = await visitTicketCreate()
  168. await view.events.type(view.getByLabelText('Title'), 'Ticket Title')
  169. await waitUntil(() => mockFormUpdater.calls.resolve === 2)
  170. await nextStep(view)
  171. await nextStep(view)
  172. // Customer selection.
  173. await view.events.click(view.getByLabelText('Customer'))
  174. await view.events.type(view.getByRole('searchbox'), 'nicole')
  175. await waitUntil(() => mockCustomer.calls.resolve)
  176. await view.events.click(view.getByText('Nicole Braun'))
  177. await waitUntil(() => mockFormUpdater.calls.resolve === 3)
  178. // Group selection.
  179. await view.events.click(view.getByLabelText('Group'))
  180. await view.events.click(view.getByText('Users'))
  181. await waitUntil(() => mockFormUpdater.calls.resolve === 4)
  182. await nextStep(view)
  183. // Text input.
  184. const editorNode = getNode('body')
  185. await editorNode?.input('Article body', false)
  186. await waitUntil(() => mockFormUpdater.calls.resolve === 5)
  187. const submitButton = view.getByRole('button', { name: 'Create ticket' })
  188. await waitUntil(() => !submitButton.hasAttribute('disabled'))
  189. expect(submitButton).not.toBeDisabled()
  190. await view.events.click(submitButton)
  191. await waitUntil(() => mockTicket.calls.resolve)
  192. await expect(view.findByRole('alert')).resolves.toHaveTextContent(
  193. 'Ticket has been created successfully.',
  194. )
  195. const router = getTestRouter()
  196. expect(router.replace).toHaveBeenCalledWith('/tickets/1')
  197. })
  198. it('shows confirm popup, when leaving', async () => {
  199. const { view } = await visitTicketCreate()
  200. await view.events.type(view.getByLabelText('Title'), 'Foobar')
  201. await getNode('ticket-create')?.settled
  202. await view.events.click(view.getByRole('button', { name: 'Go back' }))
  203. expect(view.queryByTestId('popupWindow')).toBeInTheDocument()
  204. await expect(
  205. view.findByRole('alert', { name: 'Confirm dialog' }),
  206. ).resolves.toBeInTheDocument()
  207. })
  208. it('shows the CC field for type "Email"', async () => {
  209. const { mockFormUpdater, view } = await visitTicketCreate()
  210. await view.events.type(view.getByLabelText('Title'), 'Foobar')
  211. await waitUntil(() => mockFormUpdater.calls.resolve)
  212. await nextStep(view)
  213. await view.events.click(view.getByLabelText('Send Email'))
  214. await nextStep(view)
  215. expect(view.getByLabelText('CC')).toBeInTheDocument()
  216. })
  217. // The rest of the test cases are covered by E2E test, due to limitations of JSDOM test environment.
  218. })
  219. describe('Creating new ticket as customer', () => {
  220. beforeEach(() => {
  221. mockPermissions(['ticket.customer'])
  222. mockApplicationConfig({
  223. customer_ticket_create: true,
  224. })
  225. })
  226. it('shows 3 steps for customers', async () => {
  227. const { view } = await visitTicketCreate()
  228. const steps = ['1', '2', '3']
  229. steps.forEach((step) => {
  230. expect(view.getByRole('button', { name: step })).toBeInTheDocument()
  231. })
  232. expect(view.queryByRole('button', { name: '4' })).not.toBeInTheDocument()
  233. })
  234. it('redirects to the error page if ticket creation is turned off', async () => {
  235. mockApplicationConfig({
  236. customer_ticket_create: false,
  237. })
  238. const { view } = await visitTicketCreate()
  239. expect(view.getByRole('main')).toHaveTextContent(
  240. 'Creating new tickets via web is disabled.',
  241. )
  242. })
  243. it('does not show the organization field without secondary organizations', async () => {
  244. mockAccount({
  245. lastname: 'Doe',
  246. firstname: 'John',
  247. organization: defaultOrganization(),
  248. hasSecondaryOrganizations: false,
  249. })
  250. const { mockFormUpdater, view } = await visitTicketCreate()
  251. await view.events.type(view.getByLabelText('Title'), 'Foobar')
  252. await waitUntil(() => mockFormUpdater.calls.resolve)
  253. await nextStep(view)
  254. expect(view.queryByLabelText('Organization')).not.toBeInTheDocument()
  255. })
  256. it('does show the organization field with secondary organizations', async () => {
  257. mockAccount({
  258. lastname: 'Doe',
  259. firstname: 'John',
  260. organization: defaultOrganization(),
  261. hasSecondaryOrganizations: true,
  262. })
  263. const { mockFormUpdater, view } = await visitTicketCreate()
  264. await view.events.type(view.getByLabelText('Title'), 'Foobar')
  265. await waitUntil(() => mockFormUpdater.calls.resolve)
  266. await nextStep(view)
  267. expect(view.queryByLabelText('Organization')).toBeInTheDocument()
  268. })
  269. })