ticket-detail-view-edit.spec.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { getNode } from '@formkit/core'
  3. import { getByLabelText, getByRole, within } from '@testing-library/vue'
  4. import { expect } from 'vitest'
  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 {
  10. mockAutocompleteSearchRecipientQuery,
  11. waitForAutocompleteSearchRecipientQueryCalls,
  12. } from '#shared/components/Form/fields/FieldRecipient/graphql/queries/autocompleteSearch/recipient.mocks.ts'
  13. import { mockFormUpdaterQuery } from '#shared/components/Form/graphql/queries/formUpdater.mocks.ts'
  14. import { waitForTicketUpdateMutationCalls } from '#shared/entities/ticket/graphql/mutations/update.mocks.ts'
  15. import { mockTicketArticlesQuery } from '#shared/entities/ticket/graphql/queries/ticket/articles.mocks.ts'
  16. import { mockTicketQuery } from '#shared/entities/ticket/graphql/queries/ticket.mocks.ts'
  17. import { getTicketUpdatesSubscriptionHandler } from '#shared/entities/ticket/graphql/subscriptions/ticketUpdates.mocks.ts'
  18. import { createDummyArticle } from '#shared/entities/ticket-article/__tests__/mocks/ticket-articles.ts'
  19. import { createDummyTicket } from '#shared/entities/ticket-article/__tests__/mocks/ticket.ts'
  20. import { convertToGraphQLId } from '#shared/graphql/utils.ts'
  21. describe('Ticket detail view', () => {
  22. beforeEach(() => {
  23. mockPermissions(['ticket.agent'])
  24. })
  25. describe('Ticket attributes', () => {
  26. it('updates ticket state to closed', async () => {
  27. const ticket = createDummyTicket({
  28. state: {
  29. id: convertToGraphQLId('Ticket::State', 2),
  30. name: 'open',
  31. stateType: {
  32. id: convertToGraphQLId('TicketStateType', 2),
  33. name: 'open',
  34. },
  35. },
  36. defaultPolicy: {
  37. update: true,
  38. agentReadAccess: true,
  39. },
  40. })
  41. mockTicketQuery({
  42. ticket,
  43. })
  44. mockFormUpdaterQuery({
  45. formUpdater: {
  46. fields: {
  47. group_id: {
  48. options: [
  49. {
  50. value: 1,
  51. label: 'Users',
  52. },
  53. {
  54. value: 2,
  55. label: 'test group',
  56. },
  57. ],
  58. },
  59. owner_id: {
  60. options: [
  61. {
  62. value: 3,
  63. label: 'Test Admin Agent',
  64. },
  65. ],
  66. },
  67. state_id: {
  68. options: [
  69. {
  70. value: 4,
  71. label: 'closed',
  72. },
  73. {
  74. value: 2,
  75. label: 'open',
  76. },
  77. {
  78. value: 6,
  79. label: 'pending close',
  80. },
  81. {
  82. value: 3,
  83. label: 'pending reminder',
  84. },
  85. ],
  86. },
  87. pending_time: {
  88. show: false,
  89. },
  90. priority_id: {
  91. options: [
  92. {
  93. value: 1,
  94. label: '1 low',
  95. },
  96. {
  97. value: 2,
  98. label: '2 normal',
  99. },
  100. {
  101. value: 3,
  102. label: '3 high',
  103. },
  104. ],
  105. },
  106. },
  107. flags: {
  108. newArticlePresent: false,
  109. },
  110. },
  111. })
  112. const view = await visitView('/tickets/1')
  113. await getNode('form-ticket-edit')?.settled
  114. expect(
  115. view.findByRole('heading', {
  116. level: 2,
  117. name: 'Ticket',
  118. }),
  119. )
  120. const statusBadges = view.getAllByTestId('common-badge')
  121. const hasOpenTicketStatus = statusBadges.some((badge) =>
  122. within(badge).getByText('open'),
  123. )
  124. expect(hasOpenTicketStatus).toBe(true)
  125. const ticketMetaSidebar = within(view.getByLabelText('Content sidebar'))
  126. await view.events.click(await ticketMetaSidebar.findByLabelText('State'))
  127. expect(
  128. await view.findByRole('listbox', { name: 'Select…' }),
  129. ).toBeInTheDocument()
  130. mockFormUpdaterQuery({
  131. formUpdater: {
  132. fields: {
  133. state_id: { value: 4 },
  134. },
  135. },
  136. })
  137. await view.events.click(view.getByRole('option', { name: 'closed' }))
  138. await getNode('form-ticket-edit')?.settled
  139. await view.events.click(view.getByRole('button', { name: 'Update' }))
  140. const calls = await waitForTicketUpdateMutationCalls()
  141. expect(calls?.at(-1)?.variables).toEqual({
  142. input: {
  143. article: null,
  144. groupId: convertToGraphQLId('Group', 2),
  145. objectAttributeValues: [],
  146. ownerId: convertToGraphQLId('User', 1),
  147. priorityId: convertToGraphQLId('Ticket::Priority', 2),
  148. stateId: convertToGraphQLId('Ticket::State', 4), // Updates from open to closed 2 -> 4
  149. },
  150. meta: {
  151. skipValidators: [],
  152. macroId: undefined,
  153. },
  154. ticketId: convertToGraphQLId('Ticket', 1),
  155. })
  156. await getTicketUpdatesSubscriptionHandler().trigger({
  157. ticketUpdates: {
  158. ticket: {
  159. ...ticket,
  160. state: {
  161. ...ticket.state,
  162. id: convertToGraphQLId('Ticket::State', 4),
  163. name: 'closed',
  164. stateType: {
  165. ...ticket.state.stateType,
  166. id: convertToGraphQLId('Ticket::StateType', 5),
  167. name: 'closed',
  168. },
  169. },
  170. },
  171. },
  172. })
  173. await waitForNextTick()
  174. const hasClosedTicketStatus = statusBadges.some((badge) =>
  175. within(badge).getByText('closed'),
  176. )
  177. expect(hasClosedTicketStatus).toBe(true)
  178. })
  179. })
  180. describe('Article actions', () => {
  181. it('adds an internal note', async () => {
  182. mockApplicationConfig({
  183. ui_ticket_zoom_article_note_new_internal: true,
  184. })
  185. mockTicketQuery({
  186. ticket: createDummyTicket({
  187. articleType: 'phone',
  188. defaultPolicy: {
  189. update: true,
  190. agentReadAccess: true,
  191. },
  192. }),
  193. })
  194. mockTicketArticlesQuery({
  195. articles: {
  196. totalCount: 1,
  197. edges: [
  198. {
  199. node: createDummyArticle({
  200. articleType: 'phone',
  201. internal: false,
  202. }),
  203. },
  204. ],
  205. },
  206. })
  207. mockFormUpdaterQuery({
  208. formUpdater: {
  209. fields: {
  210. group_id: {
  211. options: [
  212. {
  213. value: 1,
  214. label: 'Users',
  215. },
  216. {
  217. value: 2,
  218. label: 'test group',
  219. },
  220. ],
  221. },
  222. owner_id: {
  223. options: [
  224. {
  225. value: 3,
  226. label: 'Test Admin Agent',
  227. },
  228. ],
  229. },
  230. state_id: {
  231. options: [
  232. {
  233. value: 4,
  234. label: 'closed',
  235. },
  236. {
  237. value: 2,
  238. label: 'open',
  239. },
  240. {
  241. value: 6,
  242. label: 'pending close',
  243. },
  244. {
  245. value: 3,
  246. label: 'pending reminder',
  247. },
  248. ],
  249. },
  250. pending_time: {
  251. show: false,
  252. },
  253. priority_id: {
  254. options: [
  255. {
  256. value: 1,
  257. label: '1 low',
  258. },
  259. {
  260. value: 2,
  261. label: '2 normal',
  262. },
  263. {
  264. value: 3,
  265. label: '3 high',
  266. },
  267. ],
  268. },
  269. },
  270. flags: {
  271. newArticlePresent: false,
  272. },
  273. },
  274. })
  275. const view = await visitView('/tickets/1')
  276. await view.events.click(
  277. await view.findByRole('button', { name: 'Add internal note' }),
  278. )
  279. const complementary = await view.findByRole('complementary', {
  280. name: 'Reply',
  281. })
  282. expect(
  283. getByRole(complementary, 'heading', { level: 2, name: 'Reply' }),
  284. ).toBeInTheDocument()
  285. await getNode('form-ticket-edit')?.settled
  286. expect(getByLabelText(complementary, 'Visibility')).toHaveTextContent(
  287. 'Internal',
  288. )
  289. expect(complementary.firstChild).toHaveClass('bg-stripes')
  290. const editor = view.getByRole('textbox', { name: 'Text' })
  291. // FIXME: This is not possible to test ATM, due to TipTap editor not being supported in JSDOM.
  292. // expect(editor).toHaveFocus()
  293. await view.events.type(editor, 'Foo note')
  294. await getNode('form-ticket-edit')?.settled
  295. await view.events.click(view.getByRole('button', { name: 'Update' }))
  296. const calls = await waitForTicketUpdateMutationCalls()
  297. expect(calls?.at(-1)?.variables).toEqual(
  298. expect.objectContaining({
  299. input: expect.objectContaining({
  300. article: expect.objectContaining({ body: 'Foo note' }),
  301. }),
  302. }),
  303. )
  304. })
  305. it('replies to an article', async () => {
  306. mockTicketQuery({
  307. ticket: createDummyTicket({
  308. group: {
  309. id: convertToGraphQLId('Group', 1),
  310. emailAddress: {
  311. name: 'Zammad Helpdesk',
  312. emailAddress: 'zammad@localhost',
  313. },
  314. },
  315. articleType: 'email',
  316. defaultPolicy: {
  317. update: true,
  318. agentReadAccess: true,
  319. },
  320. }),
  321. })
  322. mockTicketArticlesQuery({
  323. articles: {
  324. totalCount: 1,
  325. edges: [
  326. {
  327. node: createDummyArticle({
  328. articleType: 'email',
  329. internal: false,
  330. }),
  331. },
  332. ],
  333. },
  334. })
  335. mockFormUpdaterQuery({
  336. formUpdater: {
  337. fields: {
  338. group_id: {
  339. options: [
  340. {
  341. value: 1,
  342. label: 'Users',
  343. },
  344. {
  345. value: 2,
  346. label: 'test group',
  347. },
  348. ],
  349. },
  350. owner_id: {
  351. options: [
  352. {
  353. value: 3,
  354. label: 'Test Admin Agent',
  355. },
  356. ],
  357. },
  358. state_id: {
  359. options: [
  360. {
  361. value: 4,
  362. label: 'closed',
  363. },
  364. {
  365. value: 2,
  366. label: 'open',
  367. },
  368. {
  369. value: 6,
  370. label: 'pending close',
  371. },
  372. {
  373. value: 3,
  374. label: 'pending reminder',
  375. },
  376. ],
  377. },
  378. pending_time: {
  379. show: false,
  380. },
  381. priority_id: {
  382. options: [
  383. {
  384. value: 1,
  385. label: '1 low',
  386. },
  387. {
  388. value: 2,
  389. label: '2 normal',
  390. },
  391. {
  392. value: 3,
  393. label: '3 high',
  394. },
  395. ],
  396. },
  397. },
  398. flags: {
  399. newArticlePresent: false,
  400. },
  401. },
  402. })
  403. const view = await visitView('/tickets/1')
  404. const articles = await view.findAllByRole('article')
  405. await view.events.click(
  406. await within(articles[0]).findByRole('button', { name: 'Reply' }),
  407. )
  408. await view.events.type(
  409. view.getByRole('textbox', { name: 'Text' }),
  410. 'Foo email',
  411. )
  412. await getNode('form-ticket-edit')?.settled
  413. await view.events.click(view.getByRole('button', { name: 'Update' }))
  414. const calls = await waitForTicketUpdateMutationCalls()
  415. expect(calls?.at(-1)?.variables).toEqual(
  416. expect.objectContaining({
  417. input: expect.objectContaining({
  418. article: expect.objectContaining({ body: 'Foo email' }),
  419. }),
  420. }),
  421. )
  422. })
  423. it('forwards to an article', async () => {
  424. mockTicketQuery({
  425. ticket: createDummyTicket({
  426. group: {
  427. id: convertToGraphQLId('Group', 1),
  428. emailAddress: {
  429. name: 'Zammad Helpdesk',
  430. emailAddress: 'zammad@localhost',
  431. },
  432. },
  433. articleType: 'email',
  434. defaultPolicy: {
  435. update: true,
  436. agentReadAccess: true,
  437. },
  438. }),
  439. })
  440. mockTicketArticlesQuery({
  441. articles: {
  442. totalCount: 1,
  443. edges: [
  444. {
  445. node: createDummyArticle({
  446. articleType: 'email',
  447. internal: false,
  448. }),
  449. },
  450. ],
  451. },
  452. })
  453. mockFormUpdaterQuery({
  454. formUpdater: {
  455. fields: {
  456. group_id: {
  457. options: [
  458. {
  459. value: 1,
  460. label: 'Users',
  461. },
  462. {
  463. value: 2,
  464. label: 'test group',
  465. },
  466. ],
  467. },
  468. owner_id: {
  469. options: [
  470. {
  471. value: 3,
  472. label: 'Test Admin Agent',
  473. },
  474. ],
  475. },
  476. state_id: {
  477. options: [
  478. {
  479. value: 4,
  480. label: 'closed',
  481. },
  482. {
  483. value: 2,
  484. label: 'open',
  485. },
  486. {
  487. value: 6,
  488. label: 'pending close',
  489. },
  490. {
  491. value: 3,
  492. label: 'pending reminder',
  493. },
  494. ],
  495. },
  496. pending_time: {
  497. show: false,
  498. },
  499. priority_id: {
  500. options: [
  501. {
  502. value: 1,
  503. label: '1 low',
  504. },
  505. {
  506. value: 2,
  507. label: '2 normal',
  508. },
  509. {
  510. value: 3,
  511. label: '3 high',
  512. },
  513. ],
  514. },
  515. },
  516. flags: {
  517. newArticlePresent: false,
  518. },
  519. },
  520. })
  521. const view = await visitView('/tickets/1')
  522. const articles = await view.findAllByRole('article')
  523. await view.events.click(
  524. await within(articles[0]).findByRole('button', {
  525. name: 'Action menu button',
  526. }),
  527. )
  528. await view.events.click(
  529. await view.findByRole('button', {
  530. name: 'Forward',
  531. }),
  532. )
  533. const to = view.getByLabelText('To')
  534. await view.events.click(to)
  535. mockAutocompleteSearchRecipientQuery({
  536. autocompleteSearchRecipient: [],
  537. })
  538. await view.events.type(
  539. within(to).getByRole('searchbox'),
  540. 'nicole.braun@zammad.org',
  541. )
  542. await waitForAutocompleteSearchRecipientQueryCalls()
  543. await view.events.click(
  544. view.getByRole('button', { name: 'add new email address' }),
  545. )
  546. await getNode('form-ticket-edit')?.settled
  547. await view.events.click(view.getByRole('button', { name: 'Update' }))
  548. const calls = await waitForTicketUpdateMutationCalls()
  549. expect(calls?.at(-1)?.variables).toEqual(
  550. expect.objectContaining({
  551. input: expect.objectContaining({
  552. article: expect.objectContaining({
  553. body: expect.stringContaining('---Begin forwarded message:---'),
  554. }),
  555. }),
  556. }),
  557. )
  558. })
  559. it('discards unsaved changes', async () => {
  560. mockApplicationConfig({
  561. ui_ticket_zoom_article_note_new_internal: true,
  562. })
  563. mockTicketQuery({
  564. ticket: createDummyTicket({
  565. articleType: 'phone',
  566. defaultPolicy: {
  567. update: true,
  568. agentReadAccess: true,
  569. },
  570. }),
  571. })
  572. const view = await visitView('/tickets/1')
  573. await view.events.click(
  574. view.getByRole('button', { name: 'Add phone call' }),
  575. )
  576. expect(
  577. await view.findByRole('heading', { level: 2, name: 'Reply' }),
  578. ).toBeInTheDocument()
  579. await view.events.type(
  580. view.getByRole('textbox', { name: 'Text' }),
  581. 'Foo note',
  582. )
  583. await view.events.click(
  584. view.getByRole('button', { name: 'Discard your unsaved changes' }),
  585. )
  586. const confirmDialog = await view.findByRole('dialog')
  587. expect(confirmDialog).toBeInTheDocument()
  588. await view.events.click(
  589. within(confirmDialog).getByRole('button', { name: 'Discard Changes' }),
  590. )
  591. expect(
  592. view.queryByRole('textbox', { name: 'Text' }),
  593. ).not.toBeInTheDocument()
  594. })
  595. it('discards reply form and it keeps the ticket attribute fields state', async () => {
  596. mockTicketQuery({
  597. ticket: createDummyTicket({
  598. articleType: 'phone',
  599. defaultPolicy: {
  600. update: true,
  601. agentReadAccess: true,
  602. },
  603. }),
  604. })
  605. mockTicketArticlesQuery({
  606. articles: {
  607. totalCount: 1,
  608. edges: [
  609. {
  610. node: createDummyArticle({
  611. articleType: 'phone',
  612. internal: false,
  613. }),
  614. },
  615. ],
  616. },
  617. })
  618. mockFormUpdaterQuery({
  619. formUpdater: {
  620. fields: {
  621. group_id: {
  622. options: [
  623. {
  624. value: 1,
  625. label: 'Users',
  626. },
  627. {
  628. value: 2,
  629. label: 'test group',
  630. },
  631. ],
  632. },
  633. owner_id: {
  634. options: [
  635. {
  636. value: 3,
  637. label: 'Test Admin Agent',
  638. },
  639. ],
  640. },
  641. state_id: {
  642. options: [
  643. {
  644. value: 4,
  645. label: 'closed',
  646. },
  647. {
  648. value: 2,
  649. label: 'open',
  650. },
  651. {
  652. value: 6,
  653. label: 'pending close',
  654. },
  655. {
  656. value: 3,
  657. label: 'pending reminder',
  658. },
  659. ],
  660. },
  661. pending_time: {
  662. show: false,
  663. },
  664. priority_id: {
  665. options: [
  666. {
  667. value: 1,
  668. label: '1 low',
  669. },
  670. {
  671. value: 2,
  672. label: '2 normal',
  673. },
  674. {
  675. value: 3,
  676. label: '3 high',
  677. },
  678. ],
  679. },
  680. },
  681. flags: {
  682. newArticlePresent: false,
  683. },
  684. },
  685. })
  686. const view = await visitView('/tickets/1')
  687. // Discard changes inside the reply form
  688. await view.events.click(
  689. view.getByRole('button', { name: 'Add phone call' }),
  690. )
  691. expect(
  692. await view.findByRole('heading', { level: 2, name: 'Reply' }),
  693. ).toBeInTheDocument()
  694. // Sets dirty set for a ticket attribute
  695. await view.events.click(view.getByLabelText('State'))
  696. await view.events.click(
  697. await view.findByRole('option', { name: 'closed' }),
  698. )
  699. await view.events.click(
  700. view.getByRole('button', { name: 'Discard unsaved reply' }),
  701. )
  702. expect(
  703. await view.findByRole('dialog', { name: 'Unsaved Changes' }),
  704. ).toBeInTheDocument()
  705. await view.events.click(
  706. view.getByRole('button', { name: 'Discard Changes' }),
  707. )
  708. // Verify that ticket attributes state is not lost
  709. expect(view.getByLabelText('State')).toHaveTextContent('closed')
  710. })
  711. // TODO: Currently we have a problem in our resetForm-Function but also Formkit has an bug inside the own reset handling
  712. // (null / false will currently ignored when setting back the initial value).
  713. // So we will improve our own reset function and create an issue on FormKit side to fix this.
  714. it.skip('discards complete form with an reply and afterwards only the reply directly', async () => {
  715. mockTicketQuery({
  716. ticket: createDummyTicket({
  717. group: {
  718. id: convertToGraphQLId('Group', 1),
  719. emailAddress: {
  720. name: 'Zammad Helpdesk',
  721. emailAddress: 'zammad@localhost',
  722. },
  723. },
  724. defaultPolicy: {
  725. update: true,
  726. agentReadAccess: true,
  727. },
  728. }),
  729. })
  730. mockTicketArticlesQuery({
  731. articles: {
  732. totalCount: 1,
  733. edges: [
  734. {
  735. node: createDummyArticle({
  736. articleType: 'phone',
  737. internal: false,
  738. }),
  739. },
  740. ],
  741. },
  742. })
  743. mockFormUpdaterQuery({
  744. formUpdater: {
  745. fields: {
  746. group_id: {
  747. options: [
  748. {
  749. value: 1,
  750. label: 'Users',
  751. },
  752. {
  753. value: 2,
  754. label: 'test group',
  755. },
  756. ],
  757. },
  758. owner_id: {
  759. options: [
  760. {
  761. value: 3,
  762. label: 'Test Admin Agent',
  763. },
  764. ],
  765. },
  766. state_id: {
  767. options: [
  768. {
  769. value: 4,
  770. label: 'closed',
  771. },
  772. {
  773. value: 2,
  774. label: 'open',
  775. },
  776. {
  777. value: 6,
  778. label: 'pending close',
  779. },
  780. {
  781. value: 3,
  782. label: 'pending reminder',
  783. },
  784. ],
  785. },
  786. pending_time: {
  787. show: false,
  788. },
  789. priority_id: {
  790. options: [
  791. {
  792. value: 1,
  793. label: '1 low',
  794. },
  795. {
  796. value: 2,
  797. label: '2 normal',
  798. },
  799. {
  800. value: 3,
  801. label: '3 high',
  802. },
  803. ],
  804. },
  805. },
  806. flags: {
  807. newArticlePresent: false,
  808. },
  809. },
  810. })
  811. const view = await visitView('/tickets/1')
  812. // Discard changes inside the reply form
  813. await view.events.click(view.getByRole('button', { name: 'Add reply' }))
  814. await view.events.click(
  815. await view.findByRole('button', {
  816. name: 'Discard your unsaved changes',
  817. }),
  818. )
  819. expect(
  820. await view.findByRole('dialog', { name: 'Unsaved Changes' }),
  821. ).toBeInTheDocument()
  822. await view.events.click(
  823. view.getByRole('button', { name: 'Discard Changes' }),
  824. )
  825. expect(
  826. view.queryByRole('button', {
  827. name: 'Discard your unsaved changes',
  828. }),
  829. ).not.toBeInTheDocument()
  830. await view.events.click(view.getByRole('button', { name: 'Add reply' }))
  831. await view.events.click(
  832. view.getByRole('button', { name: 'Discard unsaved reply' }),
  833. )
  834. expect(
  835. await view.findByRole('dialog', { name: 'Unsaved Changes' }),
  836. ).toBeInTheDocument()
  837. await view.events.click(
  838. view.getByRole('button', { name: 'Discard Changes' }),
  839. )
  840. expect(
  841. view.queryByRole('button', {
  842. name: 'Discard your unsaved changes',
  843. }),
  844. ).not.toBeInTheDocument()
  845. })
  846. })
  847. })