zoom_spec.rb 86 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. require 'system/examples/core_workflow_examples'
  4. RSpec.describe 'Ticket zoom', type: :system do
  5. context 'when ticket has an attachment' do
  6. let(:group) { Group.find_by(name: 'Users') }
  7. let(:ticket) { create(:ticket, group: group) }
  8. let(:article) { create(:ticket_article, ticket: ticket) }
  9. let(:attachment_name) { 'some_file.txt' }
  10. before do
  11. create(:store,
  12. object: 'Ticket::Article',
  13. o_id: article.id,
  14. data: 'some content',
  15. filename: attachment_name,
  16. preferences: {
  17. 'Content-Type' => 'text/plain',
  18. })
  19. end
  20. context 'article was already forwarded once' do
  21. before do
  22. visit "#ticket/zoom/#{ticket.id}"
  23. within(:active_content) do
  24. find('a[data-type=emailForward]').click
  25. click('.js-reset')
  26. have_no_css('.js-reset')
  27. end
  28. end
  29. it 'adds attachments when forwarding multiple times' do
  30. within(:active_content) do
  31. find('a[data-type=emailForward]').click
  32. end
  33. within('.js-writeArea') do
  34. expect(page).to have_text attachment_name
  35. end
  36. end
  37. end
  38. end
  39. context 'when using the sidebar' do
  40. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users'), customer: create(:customer, :with_org)) }
  41. before do
  42. Setting.set("#{service_name}_integration", true) if defined? service_name
  43. visit "#ticket/zoom/#{ticket.id}"
  44. end
  45. it 'does show the edit link for the customer' do
  46. click '.tabsSidebar-tab[data-tab=customer]'
  47. click '#userAction'
  48. click_on 'Edit Customer'
  49. modal_ready
  50. end
  51. it 'does show the edit link for the organization' do
  52. click '.tabsSidebar-tab[data-tab=organization]'
  53. click '#userAction'
  54. click_on 'Edit Organization'
  55. modal_ready
  56. end
  57. %w[idoit gitlab github].each do |service_name|
  58. it "#{service_name} tab is hidden" do
  59. expect(page).to have_no_css(".tabsSidebar-tab[data-tab=#{service_name}]")
  60. end
  61. context "when #{service_name} is enabled" do
  62. let(:service_name) { service_name }
  63. context 'when agent' do
  64. it "#{service_name} tab is visible" do
  65. expect(page).to have_css(".tabsSidebar-tab[data-tab=#{service_name}]")
  66. end
  67. end
  68. context 'when customer', authenticated_as: :customer do
  69. let(:customer) { create(:customer) }
  70. it "#{service_name} tab is hidden" do
  71. expect(page).to have_no_css(".tabsSidebar-tab[data-tab=#{service_name}]")
  72. end
  73. end
  74. end
  75. end
  76. end
  77. context 'when ticket has a calendar attachment' do
  78. let(:group) { Group.find_by(name: 'Users') }
  79. let(:store_file_content_name) do
  80. Rails.root.join('spec/fixtures/files/calendar/basic.ics').read
  81. end
  82. let(:store_file_name) { 'basic.ics' }
  83. let(:expected_event) do
  84. {
  85. 'title' => 'Test Summary',
  86. 'location' => 'https://us.zoom.us/j/example?pwd=test',
  87. 'attendees' => ['M.bob@example.com', 'J.doe@example.com'],
  88. 'organizer' => 'f.sample@example.com',
  89. 'description' => 'Test description'
  90. }
  91. end
  92. let(:ticket) { create(:ticket, group: group) }
  93. let(:article) { create(:ticket_article, ticket: ticket) }
  94. before do
  95. create(:store,
  96. object: 'Ticket::Article',
  97. o_id: article.id,
  98. data: store_file_content_name,
  99. filename: store_file_name,
  100. preferences: {
  101. 'Content-Type' => 'text/calendar',
  102. })
  103. visit "#ticket/zoom/#{ticket.id}"
  104. end
  105. it 'has an attached calendar file' do
  106. within :active_ticket_article, article do
  107. within '.attachment.file-calendar' do
  108. expect(page).to have_text(store_file_name)
  109. end
  110. end
  111. end
  112. it 'shows a preview button for the calendar file' do
  113. within :active_ticket_article, article do
  114. within '.attachment.file-calendar' do
  115. expect(page).to have_button('Preview')
  116. end
  117. end
  118. end
  119. context 'when calendar preview button is clicked' do
  120. before do
  121. within :active_ticket_article, article do
  122. within '.attachment.file-calendar' do
  123. click_on 'Preview'
  124. end
  125. end
  126. end
  127. it 'shows calender data in the model' do
  128. in_modal do
  129. expect(page).to have_text expected_event['title']
  130. expect(page).to have_text expected_event['location']
  131. expected_event['attendees'].each { |attendee| expect(page).to have_text attendee }
  132. expect(page).to have_text expected_event['organizer']
  133. expect(page).to have_text expected_event['description']
  134. end
  135. click '.js-cancel'
  136. end
  137. end
  138. end
  139. context 'replying' do
  140. context 'Group without signature' do
  141. let(:ticket) { create(:ticket) }
  142. let(:current_user) { create(:agent, password: 'test', groups: [ticket.group]) }
  143. before do
  144. # initial article to reply to
  145. create(:ticket_article, ticket: ticket)
  146. end
  147. it 'ensures that text input opens on multiple replies', authenticated_as: :current_user do
  148. visit "ticket/zoom/#{ticket.id}"
  149. 2.times do |article_offset|
  150. articles_existing = 1
  151. articles_expected = articles_existing + (article_offset + 1)
  152. all('a[data-type=emailReply]').last.click
  153. # wait till input box expands completely
  154. find('.attachmentPlaceholder-label').in_fixed_position
  155. expect(page).to have_no_css('.attachmentPlaceholder-hint')
  156. find('.articleNewEdit-body').send_keys('Some reply')
  157. click '.js-submit'
  158. expect(page).to have_css('.ticket-article-item', count: articles_expected)
  159. end
  160. end
  161. end
  162. context 'Group with signature', authenticated_as: :user do
  163. let(:signature_body) { 'Sample signature here' }
  164. let(:signature) { create(:signature, body: signature_body) }
  165. let(:group) { create(:group, signature: signature) }
  166. let(:ticket) { create(:ticket, group: group) }
  167. let(:user) { create(:agent, groups: [group]) }
  168. before do
  169. visit "ticket/zoom/#{ticket.id}"
  170. click '.attachmentPlaceholder'
  171. end
  172. it 'removes signature when switching from email reply to phone' do
  173. click '.js-selectableTypes'
  174. click '.js-articleTypeItem[data-value=email]'
  175. within :richtext do
  176. expect(page).to have_text(signature_body)
  177. end
  178. click '.js-selectableTypes'
  179. click '.js-articleTypeItem[data-value=phone]'
  180. within :richtext do
  181. expect(page).to have_no_text(signature_body)
  182. end
  183. end
  184. it 'adds signature when switching from phone to email reply' do
  185. within :richtext do
  186. expect(page).to have_no_text(signature_body)
  187. end
  188. click '.js-selectableTypes'
  189. click '.js-articleTypeItem[data-value=email]'
  190. within :richtext do
  191. expect(page).to have_text(signature_body)
  192. end
  193. end
  194. end
  195. context 'to inbound phone call', authenticated_as: -> { agent }, current_user_id: -> { agent.id } do
  196. let(:agent) { create(:agent, groups: [Group.first]) }
  197. let(:customer) { create(:customer) }
  198. let(:ticket) { create(:ticket, customer: customer, group: agent.groups.first) }
  199. let!(:article) { create(:ticket_article, :inbound_phone, ticket: ticket) }
  200. before do
  201. create(:customer, active: false)
  202. end
  203. it 'goes to customer email' do
  204. visit "ticket/zoom/#{ticket.id}"
  205. within :active_ticket_article, article do
  206. click '.js-ArticleAction[data-type=emailReply]'
  207. end
  208. within :active_content do
  209. within '.article-new' do
  210. expect(find('[name=to]', visible: :all).value).to eq customer.email
  211. end
  212. end
  213. end
  214. it 'check active and inactive user in TO-field' do
  215. visit "ticket/zoom/#{ticket.id}"
  216. within :active_ticket_article, article do
  217. click '.js-ArticleAction[data-type=emailReply]'
  218. end
  219. within :active_content do
  220. within '.article-new' do
  221. find('[name=to] ~ .ui-autocomplete-input').fill_in with: '**'
  222. end
  223. end
  224. expect(page).to have_css('ul.ui-autocomplete > li.ui-menu-item', minimum: 2)
  225. expect(page).to have_css('ul.ui-autocomplete > li.ui-menu-item.is-inactive', count: 1)
  226. end
  227. end
  228. context 'to outbound phone call', authenticated_as: -> { agent }, current_user_id: -> { agent.id } do
  229. let(:agent) { create(:agent, groups: [Group.first]) }
  230. let(:customer) { create(:customer) }
  231. let(:ticket) { create(:ticket, customer: customer, group: agent.groups.first) }
  232. let!(:article) { create(:ticket_article, :outbound_phone, ticket: ticket) }
  233. it 'goes to customer email' do
  234. visit "ticket/zoom/#{ticket.id}"
  235. within :active_ticket_article, article do
  236. click '.js-ArticleAction[data-type=emailReply]'
  237. end
  238. within :active_content do
  239. within '.article-new' do
  240. expect(find('[name=to]', visible: :all).value).to eq customer.email
  241. end
  242. end
  243. end
  244. end
  245. context 'scrollPageHeader disappears when answering via email #3736' do
  246. let(:ticket) do
  247. ticket = create(:ticket, group: Group.first)
  248. create_list(:ticket_article, 15, ticket: ticket)
  249. ticket
  250. end
  251. before do
  252. visit "ticket/zoom/#{ticket.id}"
  253. end
  254. it 'does reset the scrollPageHeader on rerender of the ticket' do
  255. select User.find_by(email: 'admin@example.com').fullname, from: 'Owner'
  256. find('.js-textarea').send_keys('test 1234')
  257. find('.js-submit').click
  258. expect(page).to have_css('div.scrollPageHeader .js-ticketTitleContainer')
  259. end
  260. end
  261. end
  262. describe 'delete article', authenticated_as: :authenticate do
  263. let(:group) { Group.first }
  264. let(:admin) { create(:admin, groups: [group]) }
  265. let(:agent) { create(:agent, groups: [group]) }
  266. let(:other_agent) { create(:agent, groups: [group]) }
  267. let(:customer) { create(:customer) }
  268. let(:article) { send(item) }
  269. def authenticate
  270. Setting.set('ui_ticket_zoom_article_delete_timeframe', setting_delete_timeframe) if defined?(setting_delete_timeframe)
  271. article
  272. user
  273. end
  274. def article_communication
  275. create_ticket_article(sender_name: 'Agent', internal: false, type_name: 'email', updated_by: customer)
  276. end
  277. def article_note_self
  278. create_ticket_article(sender_name: 'Agent', internal: true, type_name: 'note', updated_by: user)
  279. end
  280. def article_note_other
  281. create_ticket_article(sender_name: 'Agent', internal: true, type_name: 'note', updated_by: other_agent)
  282. end
  283. def article_note_customer
  284. create_ticket_article(sender_name: 'Customer', internal: false, type_name: 'note', updated_by: customer)
  285. end
  286. def article_note_communication_self
  287. create(:ticket_article_type, name: 'note_communication', communication: true)
  288. create_ticket_article(sender_name: 'Agent', internal: true, type_name: 'note_communication', updated_by: user)
  289. end
  290. def article_note_communication_other
  291. create(:ticket_article_type, name: 'note_communication', communication: true)
  292. create_ticket_article(sender_name: 'Agent', internal: true, type_name: 'note_communication', updated_by: other_agent)
  293. end
  294. def create_ticket_article(sender_name:, internal:, type_name:, updated_by:)
  295. UserInfo.current_user_id = updated_by.id
  296. ticket = create(:ticket, group: group, customer: customer)
  297. create(:ticket_article,
  298. sender_name: sender_name, internal: internal, type_name: type_name, ticket: ticket,
  299. body: "to be deleted #{offset} #{item}",
  300. created_at: offset.ago, updated_at: offset.ago)
  301. end
  302. context 'going through full stack' do
  303. context 'as admin' do
  304. let(:user) { admin }
  305. let(:item) { 'article_note_self' }
  306. let(:offset) { 0.minutes }
  307. it 'succeeds' do
  308. ensure_websocket do
  309. visit "ticket/zoom/#{article.ticket.id}"
  310. end
  311. within :active_ticket_article, article do
  312. click '.js-ArticleAction[data-type=delete]'
  313. end
  314. in_modal do
  315. click '.js-submit'
  316. end
  317. wait.until_disappears { find :active_ticket_article, article, wait: false }
  318. end
  319. end
  320. end
  321. context 'verifying permissions matrix' do
  322. shared_examples 'according to permission matrix' do |item:, expects_visible:, offset:, description:|
  323. context "looking at #{description} #{item}" do
  324. let(:item) { item }
  325. let(:offset) { offset }
  326. let(:matcher) { expects_visible ? :have_css : :have_no_css }
  327. it expects_visible ? 'delete button is visible' : 'delete button is not visible' do
  328. visit "ticket/zoom/#{article.ticket.id}"
  329. find("#article-#{article.id}")
  330. within :active_ticket_article, article do
  331. expect(page).to send(matcher, '.js-ArticleAction[data-type=delete]', wait: 0)
  332. end
  333. end
  334. end
  335. end
  336. shared_examples 'deleting ticket article' do |item:, now:, later:, much_later:|
  337. include_examples 'according to permission matrix', item: item, expects_visible: now, offset: 0.minutes, description: 'just created'
  338. include_examples 'according to permission matrix', item: item, expects_visible: later, offset: 6.minutes, description: 'few minutes old'
  339. include_examples 'according to permission matrix', item: item, expects_visible: much_later, offset: 11.minutes, description: 'very old'
  340. end
  341. context 'as admin' do
  342. let(:user) { admin }
  343. include_examples 'deleting ticket article',
  344. item: 'article_communication',
  345. now: false, later: false, much_later: false
  346. include_examples 'deleting ticket article',
  347. item: 'article_note_self',
  348. now: true, later: true, much_later: false
  349. include_examples 'deleting ticket article',
  350. item: 'article_note_other',
  351. now: false, later: false, much_later: false
  352. include_examples 'deleting ticket article',
  353. item: 'article_note_customer',
  354. now: false, later: false, much_later: false
  355. include_examples 'deleting ticket article',
  356. item: 'article_note_communication_self',
  357. now: true, later: true, much_later: false
  358. include_examples 'deleting ticket article',
  359. item: 'article_note_communication_other',
  360. now: false, later: false, much_later: false
  361. end
  362. context 'as agent' do
  363. let(:user) { agent }
  364. include_examples 'deleting ticket article',
  365. item: 'article_communication',
  366. now: false, later: false, much_later: false
  367. include_examples 'deleting ticket article',
  368. item: 'article_note_self',
  369. now: true, later: true, much_later: false
  370. include_examples 'deleting ticket article',
  371. item: 'article_note_other',
  372. now: false, later: false, much_later: false
  373. include_examples 'deleting ticket article',
  374. item: 'article_note_customer',
  375. now: false, later: false, much_later: false
  376. include_examples 'deleting ticket article',
  377. item: 'article_note_communication_self',
  378. now: true, later: true, much_later: false
  379. include_examples 'deleting ticket article',
  380. item: 'article_note_communication_other',
  381. now: false, later: false, much_later: false
  382. end
  383. context 'as customer' do
  384. let(:user) { customer }
  385. include_examples 'deleting ticket article',
  386. item: 'article_communication',
  387. now: false, later: false, much_later: false
  388. include_examples 'deleting ticket article',
  389. item: 'article_note_customer',
  390. now: false, later: false, much_later: false
  391. end
  392. context 'with custom offset' do
  393. let(:setting_delete_timeframe) { 6_000 }
  394. context 'as admin' do
  395. let(:user) { admin }
  396. include_examples 'according to permission matrix', item: 'article_note_self', expects_visible: true, offset: 5000.seconds, description: 'outside of delete timeframe'
  397. include_examples 'according to permission matrix', item: 'article_note_self', expects_visible: false, offset: 8000.seconds, description: 'outside of delete timeframe'
  398. end
  399. context 'as agent' do
  400. let(:user) { agent }
  401. include_examples 'according to permission matrix', item: 'article_note_self', expects_visible: true, offset: 5000.seconds, description: 'outside of delete timeframe'
  402. include_examples 'according to permission matrix', item: 'article_note_self', expects_visible: false, offset: 8000.seconds, description: 'outside of delete timeframe'
  403. end
  404. end
  405. context 'with timeframe as 0' do
  406. let(:setting_delete_timeframe) { 0 }
  407. context 'as agent' do
  408. let(:user) { agent }
  409. include_examples 'according to permission matrix', item: 'article_note_self', expects_visible: true, offset: 99.days, description: 'long after'
  410. end
  411. end
  412. end
  413. context 'button is hidden on the go' do
  414. let(:setting_delete_timeframe) { 10 }
  415. let(:user) { agent }
  416. let(:item) { 'article_note_self' }
  417. let!(:article) { send(item) }
  418. let(:offset) { 0.seconds }
  419. it 'successfully' do
  420. visit "ticket/zoom/#{article.ticket.id}"
  421. within :active_ticket_article, article do
  422. find '.js-ArticleAction[data-type=delete]' # make sure delete button did show up
  423. expect(page).to have_no_css('.js-ArticleAction[data-type=delete]')
  424. end
  425. end
  426. end
  427. end
  428. describe 'forwarding article with an image' do
  429. let(:ticket_article_body) do
  430. filename = 'squares.png'
  431. file = Rails.root.join("spec/fixtures/files/image/#{filename}").binread
  432. ext = File.extname(filename)[1...]
  433. base64 = Base64.encode64(file).delete("\n")
  434. "<img style='width: 1004px; max-width: 100%;' src=\\\"data:image/#{ext};base64,#{base64}\\\"><br>"
  435. end
  436. def current_ticket
  437. Ticket.find current_url.split('/').last
  438. end
  439. def create_ticket
  440. visit '#ticket/create'
  441. within :active_content do
  442. find('[data-type=email-out]').click
  443. find('[name=title]').fill_in with: 'Title'
  444. find('[name=customer_id_completion]').fill_in with: 'customer@example.com'
  445. set_tree_select_value('group_id', Group.first.name)
  446. find(:richtext).execute_script "this.innerHTML = \"#{ticket_article_body}\""
  447. find('.js-submit').click
  448. end
  449. end
  450. def forward
  451. within :active_content do
  452. find('.textBubble-content .richtext-content')
  453. click '.js-ArticleAction[data-type=emailForward]'
  454. fill_in 'To', with: 'customer@example.com'
  455. find('.js-submit').click
  456. end
  457. end
  458. def images_identical?(image_a, image_b)
  459. return false if image_a.height != image_b.height
  460. return false if image_a.width != image_b.width
  461. image_a.height.times do |y|
  462. image_a.row(y).each_with_index do |pixel, x|
  463. return false if pixel != image_b[x, y]
  464. end
  465. end
  466. true
  467. end
  468. it 'keeps image intact' do
  469. create_ticket
  470. forward
  471. images = current_ticket.articles.map do |article|
  472. ChunkyPNG::Image.from_string article.attachments.first.content
  473. end
  474. expect(images_identical?(images.first, images.second)).to be(true)
  475. end
  476. end
  477. # https://github.com/zammad/zammad/issues/3335
  478. context 'ticket state sort order maintained when locale is de-de', authenticated_as: :user do
  479. context 'when existing ticket is open' do
  480. let(:user) { create(:customer, preferences: { locale: 'de-de' }) }
  481. let(:ticket) { create(:ticket, customer: user) }
  482. it 'shows ticket state dropdown options in sorted translated alphabetically order' do
  483. visit "ticket/zoom/#{ticket.id}"
  484. within :active_content, '.tabsSidebar' do
  485. expect(all('select[name=state_id] option').map(&:text)).to eq(%w[geschlossen neu offen])
  486. end
  487. end
  488. end
  489. context 'when a new ticket is created' do
  490. let(:user) { create(:agent, preferences: { locale: 'de-de' }, groups: [permitted_group]) }
  491. let(:permitted_group) { create(:group) }
  492. it 'shows ticket state dropdown options in sorted order' do
  493. visit 'ticket/create'
  494. expect(all('select[name=state_id] option').map(&:text)).to eq ['-', 'geschlossen', 'neu', 'offen', 'warten auf Erinnerung', 'warten auf Schließen']
  495. end
  496. end
  497. end
  498. context 'object manager attribute permission view' do
  499. let!(:group_users) { Group.find_by(name: 'Users') }
  500. shared_examples 'shows attributes and values for agent view and editable' do
  501. it 'shows attributes and values for agent view and editable', authenticated_as: :current_user do
  502. visit "ticket/zoom/#{ticket.id}"
  503. refresh # refresh to have assets generated for ticket
  504. expect(page).to have_select('state_id', options: ['new', 'open', 'pending reminder', 'pending close', 'closed'])
  505. expect(page).to have_select('priority_id')
  506. expect(page).to have_select('owner_id')
  507. expect(page).to have_css('div.tabsSidebar-tab[data-tab=customer]')
  508. end
  509. end
  510. shared_examples 'shows attributes and values for agent view but disabled' do
  511. it 'shows attributes and values for agent view but disabled', authenticated_as: :current_user do
  512. visit "ticket/zoom/#{ticket.id}"
  513. refresh # refresh to have assets generated for ticket
  514. expect(page).to have_select('state_id', disabled: true)
  515. expect(page).to have_select('priority_id', disabled: true)
  516. expect(page).to have_select('owner_id', disabled: true)
  517. expect(page).to have_css('div.tabsSidebar-tab[data-tab=customer]')
  518. end
  519. end
  520. shared_examples 'shows attributes and values for customer view' do
  521. it 'shows attributes and values for customer view', authenticated_as: :current_user do
  522. visit "ticket/zoom/#{ticket.id}"
  523. refresh # refresh to have assets generated for ticket
  524. expect(page).to have_select('state_id', options: %w[new open closed])
  525. expect(page).to have_no_select('priority_id')
  526. expect(page).to have_no_select('owner_id')
  527. expect(page).to have_no_css('div.tabsSidebar-tab[data-tab=customer]')
  528. end
  529. end
  530. context 'as customer' do
  531. let!(:current_user) { create(:customer) }
  532. let(:ticket) { create(:ticket, customer: current_user) }
  533. include_examples 'shows attributes and values for customer view'
  534. end
  535. context 'as agent with full permissions' do
  536. let(:current_user) { create(:agent, groups: [ group_users ]) }
  537. let(:ticket) { create(:ticket, group: group_users) }
  538. include_examples 'shows attributes and values for agent view and editable'
  539. end
  540. context 'as agent with change permissions' do
  541. let!(:current_user) { create(:agent) }
  542. let(:ticket) { create(:ticket, group: group_users) }
  543. before do
  544. current_user.group_names_access_map = {
  545. group_users.name => %w[read change],
  546. }
  547. end
  548. include_examples 'shows attributes and values for agent view and editable'
  549. end
  550. context 'as agent with read permissions' do
  551. let!(:current_user) { create(:agent) }
  552. let(:ticket) { create(:ticket, group: group_users) }
  553. before do
  554. current_user.group_names_access_map = {
  555. group_users.name => 'read',
  556. }
  557. end
  558. include_examples 'shows attributes and values for agent view but disabled'
  559. end
  560. context 'as agent+customer with full permissions' do
  561. let!(:current_user) { create(:agent_and_customer, groups: [ group_users ]) }
  562. context 'normal ticket' do
  563. let(:ticket) { create(:ticket, group: group_users) }
  564. include_examples 'shows attributes and values for agent view and editable'
  565. end
  566. context 'ticket where current_user is also customer' do
  567. let(:ticket) { create(:ticket, customer: current_user, group: group_users) }
  568. include_examples 'shows attributes and values for agent view and editable'
  569. end
  570. end
  571. context 'as agent+customer with change permissions' do
  572. let!(:current_user) { create(:agent_and_customer) }
  573. before do
  574. current_user.group_names_access_map = {
  575. group_users.name => %w[read change],
  576. }
  577. end
  578. context 'normal ticket' do
  579. let(:ticket) { create(:ticket, group: group_users) }
  580. include_examples 'shows attributes and values for agent view and editable'
  581. end
  582. context 'ticket where current_user is also customer' do
  583. let(:ticket) { create(:ticket, customer: current_user, group: group_users) }
  584. include_examples 'shows attributes and values for agent view and editable'
  585. end
  586. end
  587. context 'as agent+customer with read permissions' do
  588. let!(:current_user) { create(:agent_and_customer) }
  589. before do
  590. current_user.group_names_access_map = {
  591. group_users.name => 'read',
  592. }
  593. end
  594. context 'normal ticket' do
  595. let(:ticket) { create(:ticket, group: group_users) }
  596. include_examples 'shows attributes and values for agent view but disabled'
  597. end
  598. context 'ticket where current_user is also customer' do
  599. let(:ticket) { create(:ticket, customer: current_user, group: group_users) }
  600. include_examples 'shows attributes and values for agent view but disabled'
  601. end
  602. end
  603. context 'as agent+customer but only customer for the ticket (no agent access)' do
  604. let!(:current_user) { create(:agent_and_customer) }
  605. let(:ticket) { create(:ticket, customer: current_user) }
  606. include_examples 'shows attributes and values for customer view'
  607. end
  608. end
  609. describe 'note visibility', authenticated_as: :customer do
  610. context 'when logged in as a customer' do
  611. let(:customer) { create(:customer) }
  612. let(:ticket) { create(:ticket, customer: customer) }
  613. let!(:ticket_article) { create(:ticket_article, ticket: ticket) }
  614. let!(:ticket_note) { create(:ticket_article, ticket: ticket, internal: true, type_name: 'note') }
  615. it 'previously created private note is not visible' do
  616. visit "ticket/zoom/#{ticket_article.ticket.id}"
  617. expect(page).to have_no_selector(:active_ticket_article, ticket_note)
  618. end
  619. it 'previously created private note shows up via WS push' do
  620. visit "ticket/zoom/#{ticket_article.ticket.id}"
  621. ensure_websocket
  622. # make sure ticket is done loading and change will be pushed via WS
  623. find(:active_ticket_article, ticket_article)
  624. ticket_note.update!(internal: false)
  625. expect(page).to have_selector(:active_ticket_article, ticket_note)
  626. end
  627. end
  628. end
  629. # https://github.com/zammad/zammad/issues/3012
  630. describe 'article type selection' do
  631. context 'when logged in as a customer', authenticated_as: :customer do
  632. let(:customer) { create(:customer) }
  633. let(:ticket) { create(:ticket, customer: customer) }
  634. it 'hides button for single choice' do
  635. visit "ticket/zoom/#{ticket.id}"
  636. find('.articleNewEdit-body').send_keys('Some reply')
  637. expect(page).to have_no_selector('.js-selectedArticleType')
  638. end
  639. end
  640. context 'when logged in as an agent' do
  641. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  642. it 'shows button for multiple choices' do
  643. visit "ticket/zoom/#{ticket.id}"
  644. find('.articleNewEdit-body').send_keys('Some reply')
  645. expect(page).to have_css('.js-selectedArticleType')
  646. end
  647. end
  648. end
  649. # https://github.com/zammad/zammad/issues/3260
  650. describe 'next in overview macro changes URL', authenticated_as: :authenticate do
  651. let(:next_ticket) { create(:ticket, title: 'next Ticket', group: Group.first) }
  652. let(:macro) { create(:macro, name: 'next macro', ux_flow_next_up: 'next_from_overview') }
  653. def authenticate
  654. next_ticket && macro
  655. true
  656. end
  657. it 'to next Ticket ID' do
  658. visit 'ticket/view/all_unassigned'
  659. click_on 'Welcome to Zammad!'
  660. click '.js-openDropdownMacro'
  661. find(:macro, macro.id).click
  662. wait(5, interval: 1).until_constant { current_url }
  663. expect(current_url).to include("ticket/zoom/#{next_ticket.id}")
  664. end
  665. end
  666. # https://github.com/zammad/zammad/issues/3279
  667. describe 'previous/next clickability when at last or first ticket' do
  668. let(:ticket_a) { create(:ticket, title: 'ticket a', group: Group.first) }
  669. let(:ticket_b) { create(:ticket, title: 'ticket b', group: Group.first) }
  670. before do
  671. ticket_a && ticket_b
  672. visit 'ticket/view/all_unassigned'
  673. end
  674. it 'previous is not clickable for the first item' do
  675. open_nth_item(0)
  676. expect(page).to have_css('.pagination .btn--split--first.is-disabled')
  677. end
  678. it 'next is clickable for the first item' do
  679. open_nth_item(0)
  680. expect { click '.pagination .btn--split--last' }.to change { page.find('.content.active')[:id] }
  681. end
  682. it 'previous is clickable for the middle item' do
  683. open_nth_item(1)
  684. expect { click '.pagination .btn--split--first' }.to change { page.find('.content.active')[:id] }
  685. end
  686. it 'next is clickable for the middle item' do
  687. open_nth_item(1)
  688. expect { click '.pagination .btn--split--last' }.to change { page.find('.content.active')[:id] }
  689. end
  690. it 'previous is clickable for the last item' do
  691. open_nth_item(2)
  692. expect { click '.pagination .btn--split--first' }.to change { page.find('.content.active')[:id] }
  693. end
  694. it 'next is not clickable for the last item' do
  695. open_nth_item(2)
  696. expect(page).to have_css('.pagination .btn--split--last.is-disabled')
  697. end
  698. def open_nth_item(nth)
  699. within :active_content do
  700. find_all('.table tr.item a[href^="#ticket/zoom"]')[nth].click
  701. end
  702. await_empty_ajax_queue
  703. end
  704. end
  705. # https://github.com/zammad/zammad/issues/3267
  706. describe 'previous/next buttons are added when open ticket is opened from overview' do
  707. let(:ticket_a) { create(:ticket, title: 'ticket a', group: Group.first) }
  708. let(:ticket_b) { create(:ticket, title: 'ticket b', group: Group.first) }
  709. # prepare an opened ticket and go to overview
  710. before do
  711. ticket_a && ticket_b
  712. visit "ticket/zoom/#{ticket_a.id}"
  713. visit 'ticket/view/all_unassigned'
  714. end
  715. it 'adds previous/next buttons to existing ticket' do
  716. within :active_content do
  717. click_on ticket_a.title
  718. expect(page).to have_css('.pagination-counter')
  719. end
  720. end
  721. it 'keeps previous/next buttons when navigating to overview ticket from elsewhere' do
  722. within :active_content do
  723. click_on ticket_a.title
  724. visit 'dashboard'
  725. visit "ticket/zoom/#{ticket_a.id}"
  726. expect(page).to have_css('.pagination-counter')
  727. end
  728. end
  729. end
  730. # https://github.com/zammad/zammad/issues/2942
  731. describe 'attachments are lost in specific conditions' do
  732. let(:ticket) { create(:ticket, group: Group.first) }
  733. it 'attachment is retained when forwarding a fresh article' do
  734. ensure_websocket do
  735. visit "ticket/zoom/#{ticket.id}"
  736. end
  737. # add an article, forcing reset of form_id
  738. # click in the upper most upper left corner of the article create textbox
  739. # (that works for both Firefox and Chrome)
  740. # to avoid clicking on attachment upload
  741. find('.js-writeArea').click(x: 5, y: 5)
  742. # wait for propagateOpenTextarea to be completed
  743. find('.attachmentPlaceholder-label').in_fixed_position
  744. expect(page).to have_no_css('.attachmentPlaceholder-hint')
  745. # write article content
  746. find('.articleNewEdit-body').send_keys('Some reply')
  747. click '.js-submit'
  748. # wait for article to be added to the page
  749. expect(page).to have_css('.ticket-article-item', count: 1)
  750. # create a on-the-fly article with attachment that will get pushed to open browser
  751. article1 = create(:ticket_article, ticket: ticket)
  752. create(:store,
  753. object: 'Ticket::Article',
  754. o_id: article1.id,
  755. data: 'some content',
  756. filename: 'some_file.txt',
  757. preferences: {
  758. 'Content-Type' => 'text/plain',
  759. })
  760. # wait for article to be added to the page
  761. expect(page).to have_css('.ticket-article-item', count: 2)
  762. # click on forward of created article
  763. within :active_ticket_article, article1 do
  764. find('a[data-type=emailForward]').click
  765. end
  766. # wait for propagateOpenTextarea to be completed
  767. find('.attachmentPlaceholder-label').in_fixed_position
  768. expect(page).to have_no_css('.attachmentPlaceholder-hint')
  769. # fill forward information and create article
  770. fill_in 'To', with: 'forward@example.org'
  771. find('.articleNewEdit-body').send_keys('Forwarding with the attachment')
  772. click '.js-submit'
  773. # wait for article to be added to the page
  774. expect(page).to have_css('.ticket-article-item', count: 3)
  775. # check if attachment was forwarded successfully
  776. within :active_ticket_article, ticket.reload.articles.last do
  777. within '.attachments--list' do
  778. expect(page).to have_text('some_file.txt')
  779. end
  780. end
  781. end
  782. end
  783. describe 'mentions' do
  784. context 'when logged in as agent' do
  785. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  786. let!(:other_agent) { create(:agent, groups: [Group.find_by(name: 'Users')]) }
  787. let!(:admin) { User.find_by(email: 'admin@example.com') }
  788. it 'can subscribe and unsubscribe' do
  789. ensure_websocket do
  790. visit "ticket/zoom/#{ticket.id}"
  791. click '.js-subscriptions .js-subscribe input'
  792. expect(page).to have_css('.js-subscriptions .js-unsubscribe input')
  793. expect(page).to have_css('.js-subscriptions span.avatar')
  794. click '.js-subscriptions .js-unsubscribe input'
  795. expect(page).to have_css('.js-subscriptions .js-subscribe input')
  796. expect(page).to have_no_selector('.js-subscriptions span.avatar')
  797. create(:mention, mentionable: ticket, user: other_agent)
  798. expect(page).to have_css('.js-subscriptions span.avatar')
  799. # check history for mention entries
  800. click 'h2.sidebar-header-headline.js-headline'
  801. click 'li[data-type=ticket-history] a'
  802. expect(page).to have_text("created Mention → '#{admin.firstname} #{admin.lastname}'")
  803. expect(page).to have_text("removed Mention → '#{admin.firstname} #{admin.lastname}'")
  804. expect(page).to have_text("created Mention → '#{other_agent.firstname} #{other_agent.lastname}'")
  805. end
  806. end
  807. end
  808. end
  809. # https://github.com/zammad/zammad/issues/2671
  810. describe 'Pending time field in ticket sidebar', authenticated_as: :customer do
  811. let(:customer) { create(:customer) }
  812. let(:ticket) { create(:ticket, customer: customer, pending_time: 1.day.from_now, state: Ticket::State.lookup(name: 'pending reminder')) }
  813. it 'not shown to customer' do
  814. visit "ticket/zoom/#{ticket.id}"
  815. within :active_content do
  816. expect(page).to have_no_css('.controls[data-name=pending_time]')
  817. end
  818. end
  819. end
  820. describe 'Pending time field in ticket sidebar as agent' do
  821. before do
  822. ticket.update(pending_time: 1.day.from_now, state: Ticket::State.lookup(name: 'pending reminder'))
  823. visit "ticket/zoom/#{ticket.id}"
  824. end
  825. let(:ticket) { Ticket.first }
  826. # has to run asynchronously to keep both Firefox and Safari
  827. # https://github.com/zammad/zammad/issues/3414
  828. # https://github.com/zammad/zammad/issues/2887
  829. context 'when clicking timepicker component' do
  830. it 'in the first half, hours selected' do
  831. within :active_content do
  832. # timepicker messes with the dom, so don't cache the element and wait a bit.
  833. sleep 1
  834. find('.js-timepicker').click(x: -10, y: 20)
  835. sleep 0.5
  836. expect(find('.js-timepicker')).to have_selection(0..2)
  837. end
  838. end
  839. it 'in the second half, minutes selected' do
  840. within :active_content do
  841. sleep 1
  842. find('.js-timepicker').click(x: 10, y: 20)
  843. sleep 0.5
  844. expect(find('.js-timepicker')).to have_selection(3..5)
  845. end
  846. end
  847. end
  848. matcher :have_selection do
  849. match { starts_at == expected.begin && ends_at == expected.end }
  850. def starts_at
  851. actual.evaluate_script 'this.selectionStart'
  852. end
  853. def ends_at
  854. actual.evaluate_script 'this.selectionEnd'
  855. end
  856. end
  857. end
  858. describe 'Article ID URL / link' do
  859. let(:ticket) { create(:ticket, group: Group.first) }
  860. let!(:article) { create(:'ticket/article', ticket: ticket) }
  861. it 'shows Article direct link' do
  862. ensure_websocket do
  863. visit "ticket/zoom/#{ticket.id}"
  864. end
  865. url = "#{Setting.get('http_type')}://#{Setting.get('fqdn')}/#ticket/zoom/#{ticket.id}/#{article.id}"
  866. within :active_ticket_article, article do
  867. expect(page).to have_css(%(a[href="#{url}"]))
  868. end
  869. end
  870. context 'when multiple Articles are present' do
  871. let(:article_count) { 20 }
  872. let(:article_top) { ticket.articles.second }
  873. let(:article_middle) { ticket.articles[ article_count / 2 ] }
  874. let(:article_bottom) { ticket.articles.last }
  875. before do
  876. article_count.times do
  877. create(:'ticket/article', ticket: ticket, body: SecureRandom.uuid)
  878. end
  879. visit "ticket/zoom/#{ticket.id}"
  880. end
  881. def wait_for_scroll
  882. wait(5, interval: 0.2).until_constant do
  883. find('.ticketZoom').native.location.y
  884. end
  885. end
  886. def check_shown(top: false, middle: false, bottom: false)
  887. wait_for_scroll
  888. expect(page).to have_css("div#article-content-#{article_top.id} .richtext-content", obscured: !top)
  889. .and(have_css("div#article-content-#{article_middle.id} .richtext-content", obscured: !middle, wait: 0))
  890. .and(have_css("div#article-content-#{article_bottom.id} .richtext-content", obscured: !bottom, wait: 0))
  891. end
  892. it 'scrolls to top article ID' do
  893. visit "ticket/zoom/#{ticket.id}/#{article_top.id}"
  894. check_shown(top: true)
  895. end
  896. it 'scrolls to middle article ID' do
  897. visit "ticket/zoom/#{ticket.id}/#{article_middle.id}"
  898. check_shown(middle: true)
  899. end
  900. it 'scrolls to bottom article ID' do
  901. visit "ticket/zoom/#{ticket.id}/#{article_top.id}"
  902. wait_for_scroll
  903. visit "ticket/zoom/#{ticket.id}/#{article_bottom.id}"
  904. check_shown(bottom: true)
  905. end
  906. end
  907. context 'when long articles are present' do
  908. it 'shows the "See more" link if you switch between the ticket and the dashboard on new articles' do
  909. ensure_websocket do
  910. # prerender ticket
  911. visit "ticket/zoom/#{ticket.id}"
  912. # ticket tab becomes background
  913. visit 'dashboard'
  914. end
  915. # create a new article
  916. article_id = create(:'ticket/article', ticket: ticket, body: "#{SecureRandom.uuid} #{"lorem ipsum\n" * 200}")
  917. wait(30).until { has_css?('div.tasks a.is-modified') }
  918. visit "ticket/zoom/#{ticket.id}"
  919. within :active_content do
  920. expect(find("div#article-content-#{article_id.id}")).to have_text('See more')
  921. end
  922. end
  923. end
  924. end
  925. describe 'Macros', authenticated_as: :authenticate do
  926. let(:macro_body) { 'macro <b>body</b>' }
  927. let(:macro) { create(:macro, perform: { 'article.note' => { 'body' => macro_body, 'internal' => 'true', 'subject' => 'macro note' } }) }
  928. let!(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  929. def authenticate
  930. macro
  931. true
  932. end
  933. it 'does html macro by default' do
  934. visit "ticket/zoom/#{ticket.id}"
  935. find('.js-openDropdownMacro').click
  936. find(:macro, macro.id).click
  937. expect(ticket.reload.articles.last.body).to eq(macro_body)
  938. expect(ticket.reload.articles.last.content_type).to eq('text/html')
  939. end
  940. end
  941. describe 'object manager attributes maxlength', authenticated_as: :authenticate, db_strategy: :reset do
  942. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  943. def authenticate
  944. ticket
  945. create(:object_manager_attribute_text, :required_screen, name: 'maxtest', display: 'maxtest', data_option: {
  946. 'type' => 'text',
  947. 'maxlength' => 3,
  948. 'null' => true,
  949. 'translate' => false,
  950. 'default' => '',
  951. 'options' => {},
  952. 'relation' => '',
  953. })
  954. ObjectManager::Attribute.migration_execute
  955. true
  956. end
  957. it 'checks ticket zoom' do
  958. visit "ticket/zoom/#{ticket.id}"
  959. within(:active_content) do
  960. fill_in 'maxtest', with: 'hellu'
  961. expect(page.find_field('maxtest').value).to eq('hel')
  962. end
  963. end
  964. end
  965. describe 'Update of ticket links', authenticated_as: :authenticate do
  966. let(:ticket1) { create(:ticket, group: Group.find_by(name: 'Users')) }
  967. let(:ticket2) { create(:ticket, group: Group.find_by(name: 'Users')) }
  968. def authenticate
  969. ticket1
  970. ticket2
  971. create(:link, from: ticket1, to: ticket2)
  972. true
  973. end
  974. it 'does update the state of the ticket links' do
  975. visit "ticket/zoom/#{ticket1.id}"
  976. # check title changes
  977. expect(page).to have_text(ticket2.title)
  978. ticket2.update(title: 'super new title')
  979. expect(page).to have_text(ticket2.reload.title)
  980. # check state changes
  981. expect(page).to have_css('div.links .tasks svg.open')
  982. ticket2.update(state: Ticket::State.find_by(name: 'closed'))
  983. expect(page).to have_css('div.links .tasks svg.closed')
  984. end
  985. end
  986. describe 'GitLab Integration', :integration, authenticated_as: :authenticate, required_envs: %w[GITLAB_ENDPOINT GITLAB_APITOKEN] do
  987. let!(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  988. def authenticate
  989. Setting.set('gitlab_integration', true)
  990. Setting.set('gitlab_config', {
  991. api_token: ENV['GITLAB_APITOKEN'],
  992. endpoint: ENV['GITLAB_ENDPOINT'],
  993. })
  994. true
  995. end
  996. it 'creates links and removes them' do
  997. visit "#ticket/zoom/#{ticket.id}"
  998. within(:active_content) do
  999. # switch to GitLab sidebar
  1000. click('.tabsSidebar-tab[data-tab=gitlab]')
  1001. click('.sidebar-header-headline.js-headline')
  1002. # add issue
  1003. click_on 'Link issue'
  1004. fill_in 'link', with: ENV['GITLAB_ISSUE_LINK']
  1005. click_on 'Submit'
  1006. # verify issue
  1007. content = find('.sidebar-git-issue-content')
  1008. expect(content).to have_text('#1 Example issue')
  1009. expect(content).to have_text('critical')
  1010. expect(content).to have_text('special')
  1011. expect(content).to have_text('important milestone')
  1012. expect(content).to have_text('zammad-robot')
  1013. expect(ticket.reload.preferences[:gitlab][:issue_links][0]).to eq(ENV['GITLAB_ISSUE_LINK'])
  1014. # check sidebar counter increased to 1
  1015. expect(find('.tabsSidebar-tab[data-tab=gitlab] .js-tabCounter')).to have_text('1')
  1016. # delete issue
  1017. click(".sidebar-git-issue-delete span[data-issue-id='#{ENV['GITLAB_ISSUE_LINK']}']")
  1018. content = find('.sidebar[data-tab=gitlab] .sidebar-content')
  1019. expect(content).to have_text('No linked issues')
  1020. expect(ticket.reload.preferences[:gitlab][:issue_links][0]).to be_nil
  1021. # check that counter got removed
  1022. expect(page).to have_no_selector('.tabsSidebar-tab[data-tab=gitlab] .js-tabCounter')
  1023. end
  1024. end
  1025. end
  1026. describe 'GitHub Integration', :integration, authenticated_as: :authenticate, required_envs: %w[GITHUB_ENDPOINT GITHUB_APITOKEN] do
  1027. let!(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1028. def authenticate
  1029. Setting.set('github_integration', true)
  1030. Setting.set('github_config', {
  1031. api_token: ENV['GITHUB_APITOKEN'],
  1032. endpoint: ENV['GITHUB_ENDPOINT'],
  1033. })
  1034. true
  1035. end
  1036. it 'creates links and removes them' do
  1037. visit "#ticket/zoom/#{ticket.id}"
  1038. within(:active_content) do
  1039. # switch to GitHub sidebar
  1040. click('.tabsSidebar-tab[data-tab=github]')
  1041. click('.sidebar-header-headline.js-headline')
  1042. # add issue
  1043. click_on 'Link issue'
  1044. fill_in 'link', with: ENV['GITHUB_ISSUE_LINK']
  1045. click_on 'Submit'
  1046. # verify issue
  1047. content = find('.sidebar-git-issue-content')
  1048. expect(content).to have_text('#1575 GitHub integration')
  1049. expect(content).to have_text('enhancement')
  1050. expect(content).to have_text('integration')
  1051. expect(content).to have_text('4.0')
  1052. expect(content).to have_text('Thorsten')
  1053. expect(ticket.reload.preferences[:github][:issue_links][0]).to eq(ENV['GITHUB_ISSUE_LINK'])
  1054. # check sidebar counter increased to 1
  1055. expect(find('.tabsSidebar-tab[data-tab=github] .js-tabCounter')).to have_text('1')
  1056. # delete issue
  1057. click(".sidebar-git-issue-delete span[data-issue-id='#{ENV['GITHUB_ISSUE_LINK']}']")
  1058. content = find('.sidebar[data-tab=github] .sidebar-content')
  1059. expect(content).to have_text('No linked issues')
  1060. expect(ticket.reload.preferences[:github][:issue_links][0]).to be_nil
  1061. # check that counter got removed
  1062. expect(page).to have_no_selector('.tabsSidebar-tab[data-tab=github] .js-tabCounter')
  1063. end
  1064. end
  1065. end
  1066. describe 'Core Workflow' do
  1067. include_examples 'core workflow' do
  1068. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1069. let(:object_name) { 'Ticket' }
  1070. let(:before_it) do
  1071. lambda {
  1072. ensure_websocket(check_if_pinged: false) do
  1073. visit "#ticket/zoom/#{ticket.id}"
  1074. end
  1075. }
  1076. end
  1077. end
  1078. end
  1079. context 'Sidebar - Open & Closed Tickets', performs_jobs: true, searchindex: true do
  1080. let(:customer) { create(:customer, :with_org) }
  1081. let(:ticket_open) { create(:ticket, group: Group.find_by(name: 'Users'), customer: customer, title: SecureRandom.uuid) }
  1082. let(:ticket_closed) { create(:ticket, group: Group.find_by(name: 'Users'), customer: customer, state: Ticket::State.find_by(name: 'closed'), title: SecureRandom.uuid) }
  1083. before do
  1084. ticket_open
  1085. ticket_closed
  1086. perform_enqueued_jobs
  1087. searchindex_model_reload([Ticket, User, Organization])
  1088. end
  1089. it 'does show open and closed tickets in advanced search url' do
  1090. visit "#ticket/zoom/#{ticket_open.id}"
  1091. click '.tabsSidebar-tab[data-tab=customer]'
  1092. click '.user-tickets[data-type=open]'
  1093. expect(page).to have_text(ticket_open.title)
  1094. visit "#ticket/zoom/#{ticket_open.id}"
  1095. click '.user-tickets[data-type=closed]'
  1096. expect(page).to have_text(ticket_closed.title)
  1097. end
  1098. end
  1099. context 'Sidebar - Organization' do
  1100. let(:organization) { create(:organization) }
  1101. context 'members section' do
  1102. let(:customers) { create_list(:customer, 50, organization: organization) }
  1103. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users'), customer: customers.first) }
  1104. let(:members) { organization.members.reorder(id: :asc) }
  1105. before do
  1106. visit "#ticket/zoom/#{ticket.id}"
  1107. click '.tabsSidebar-tab[data-tab=organization]'
  1108. end
  1109. it 'shows first 10 members and loads more on demand' do
  1110. expect(page).to have_text(members[9].fullname)
  1111. expect(page).to have_no_text(members[10].fullname)
  1112. click '.js-showMoreMembers'
  1113. expect(page).to have_text(members[10].fullname)
  1114. end
  1115. end
  1116. end
  1117. describe 'merging happened in the background', authenticated_as: :user do
  1118. before do
  1119. merged_into_trigger && received_merge_trigger && update_trigger
  1120. visit "ticket/zoom/#{ticket.id}"
  1121. visit "ticket/zoom/#{target_ticket.id}"
  1122. ensure_websocket do
  1123. visit 'dashboard'
  1124. end
  1125. end
  1126. let(:merged_into_trigger) { create(:trigger, :conditionable, condition_ticket_action: :merged_into) }
  1127. let(:received_merge_trigger) { create(:trigger, :conditionable, condition_ticket_action: :received_merge) }
  1128. let(:update_trigger) { create(:trigger, :conditionable, condition_ticket_action: :update) }
  1129. let(:ticket) { create(:ticket) }
  1130. let(:target_ticket) { create(:ticket) }
  1131. let(:user) { create(:agent, :preferencable, notification_group_ids: [ticket, target_ticket].map(&:group_id), groups: [ticket, target_ticket].map(&:group)) }
  1132. context 'when merging ticket' do
  1133. before do
  1134. ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
  1135. end
  1136. it 'pulses source ticket' do
  1137. expect(page).to have_css("#navigation a.is-modified[data-key=\"Ticket-#{ticket.id}\"]")
  1138. end
  1139. it 'pulses target ticket' do
  1140. expect(page).to have_css("#navigation a.is-modified[data-key=\"Ticket-#{target_ticket.id}\"]")
  1141. end
  1142. end
  1143. context 'when merging and looking at online notifications', :performs_jobs do
  1144. before do
  1145. perform_enqueued_jobs do
  1146. ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
  1147. end
  1148. find('.js-toggleNotifications').click
  1149. end
  1150. it 'shows online notification for source ticket' do
  1151. expect(page).to have_text("Ticket #{ticket.title} was merged into another ticket")
  1152. end
  1153. it 'shows online notification for target ticket' do
  1154. expect(page).to have_text("Another ticket was merged into ticket #{ticket.title}")
  1155. end
  1156. end
  1157. end
  1158. describe 'Tab behaviour - Define default "stay on tab" / "close tab" behavior #257', authenticated_as: :authenticate do
  1159. def authenticate
  1160. Setting.set('ticket_secondary_action', 'closeTabOnTicketClose')
  1161. true
  1162. end
  1163. let!(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1164. before do
  1165. visit "ticket/zoom/#{ticket.id}"
  1166. end
  1167. it 'does show the default of the system' do
  1168. expect(page).to have_text('Close tab on ticket close')
  1169. end
  1170. it 'does save state for the user preferences' do
  1171. click '.js-attributeBar .dropup div'
  1172. click 'span[data-type=stayOnTab]'
  1173. refresh
  1174. expect(page).to have_text('Stay on tab')
  1175. expect(User.find_by(email: 'admin@example.com').preferences[:secondaryAction]).to eq('stayOnTab')
  1176. end
  1177. it 'does show the correct tab state after update of the ticket (#4094)' do
  1178. select 'closed', from: 'State'
  1179. click '.js-attributeBar .dropup div'
  1180. click 'span[data-type=stayOnTab]'
  1181. click '.js-submit'
  1182. expect(page.find('.js-secondaryActionButtonLabel')).to have_text('Stay on tab')
  1183. end
  1184. context 'Tab behaviour - Close tab on ticket close' do
  1185. it 'does not close the tab without any action' do
  1186. click '.js-submit'
  1187. expect(current_url).to include('ticket/zoom')
  1188. end
  1189. it 'does close the tab on ticket close' do
  1190. select 'closed', from: 'State'
  1191. click '.js-submit'
  1192. expect(current_url).not_to include('ticket/zoom')
  1193. end
  1194. end
  1195. context 'Tab behaviour - Stay on tab' do
  1196. def authenticate
  1197. Setting.set('ticket_secondary_action', 'stayOnTab')
  1198. true
  1199. end
  1200. it 'does not close the tab without any action' do
  1201. click '.js-submit'
  1202. expect(current_url).to include('ticket/zoom')
  1203. end
  1204. it 'does not close the tab on ticket close' do
  1205. select 'closed', from: 'State'
  1206. click '.js-submit'
  1207. expect(current_url).to include('ticket/zoom')
  1208. end
  1209. end
  1210. context 'Tab behaviour - Close tab' do
  1211. def authenticate
  1212. Setting.set('ticket_secondary_action', 'closeTab')
  1213. true
  1214. end
  1215. it 'does close the tab without any action' do
  1216. click '.js-submit'
  1217. expect(current_url).not_to include('ticket/zoom')
  1218. end
  1219. it 'does close the tab on ticket close' do
  1220. select 'closed', from: 'State'
  1221. click '.js-submit'
  1222. expect(current_url).not_to include('ticket/zoom')
  1223. end
  1224. end
  1225. context 'Tab behaviour - Next in overview' do
  1226. let(:ticket1) { create(:ticket, title: SecureRandom.uuid, group: Group.find_by(name: 'Users')) }
  1227. let(:ticket2) { create(:ticket, title: SecureRandom.uuid, group: Group.find_by(name: 'Users')) }
  1228. let(:ticket3) { create(:ticket, title: SecureRandom.uuid, group: Group.find_by(name: 'Users')) }
  1229. def authenticate
  1230. Setting.set('ticket_secondary_action', 'closeNextInOverview')
  1231. ticket1
  1232. ticket2
  1233. ticket3
  1234. true
  1235. end
  1236. before do
  1237. visit 'ticket/view/all_open'
  1238. end
  1239. it 'does change the tab without any action' do
  1240. click_on ticket1.title
  1241. expect(current_url).to include("ticket/zoom/#{ticket1.id}")
  1242. click '.js-submit'
  1243. expect(current_url).to include("ticket/zoom/#{ticket2.id}")
  1244. click '.js-submit'
  1245. expect(current_url).to include("ticket/zoom/#{ticket3.id}")
  1246. end
  1247. it 'does show default stay on tab if secondary action is not given' do
  1248. click_on ticket1.title
  1249. refresh
  1250. expect(page).to have_text('Stay on tab')
  1251. end
  1252. end
  1253. context 'On ticket switch' do
  1254. let(:ticket1) { create(:ticket, title: SecureRandom.uuid, group: Group.find_by(name: 'Users')) }
  1255. let(:ticket2) { create(:ticket, title: SecureRandom.uuid, group: Group.find_by(name: 'Users')) }
  1256. before do
  1257. visit "ticket/zoom/#{ticket1.id}"
  1258. visit "ticket/zoom/#{ticket2.id}"
  1259. end
  1260. it 'does setup the last behaviour' do
  1261. click '.js-attributeBar .dropup div'
  1262. click 'span[data-type=stayOnTab]'
  1263. wait.until do
  1264. User.find_by(email: 'admin@example.com').preferences['secondaryAction'] == 'stayOnTab'
  1265. end
  1266. visit "ticket/zoom/#{ticket1.id}"
  1267. expect(page).to have_text('Stay on tab')
  1268. end
  1269. end
  1270. end
  1271. describe 'Core Workflow: Show hidden attributes on group selection (ticket edit) #3739', authenticated_as: :authenticate, db_strategy: :reset do
  1272. let!(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1273. let(:field_name) { SecureRandom.uuid }
  1274. let(:field) do
  1275. create(:object_manager_attribute_text, name: field_name, display: field_name, screens: {
  1276. 'edit' => {
  1277. 'ticket.agent' => {
  1278. 'shown' => false,
  1279. 'required' => false,
  1280. }
  1281. }
  1282. })
  1283. ObjectManager::Attribute.migration_execute
  1284. end
  1285. before do
  1286. visit "#ticket/zoom/#{ticket.id}"
  1287. end
  1288. context 'when field visible' do
  1289. let(:workflow) do
  1290. create(:core_workflow,
  1291. object: 'Ticket',
  1292. perform: { "ticket.#{field_name}" => { 'operator' => 'show', 'show' => 'true' } })
  1293. end
  1294. def authenticate
  1295. field
  1296. workflow
  1297. true
  1298. end
  1299. it 'does show up the field' do
  1300. expect(page).to have_css("div[data-attribute-name='#{field_name}']")
  1301. end
  1302. end
  1303. context 'when field hidden' do
  1304. def authenticate
  1305. field
  1306. true
  1307. end
  1308. it 'does not show the field' do
  1309. expect(page).to have_css("div[data-attribute-name='#{field_name}'].is-hidden", visible: :hidden)
  1310. end
  1311. end
  1312. end
  1313. describe 'Notes on existing ticks are discarded by editing profile settings #3088' do
  1314. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1315. before do
  1316. visit "#ticket/zoom/#{ticket.id}"
  1317. end
  1318. def upload_and_set_text
  1319. page.find('.js-textarea').send_keys("Hello\nThis\nis\nimportant!\nyo\nhoho\ntest test test test")
  1320. page.find('input#fileUpload_1', visible: :all).set(Rails.root.join('test/data/mail/mail001.box'))
  1321. expect(page).to have_text('mail001.box')
  1322. wait_for_upload_present
  1323. end
  1324. def wait_for_upload_present
  1325. wait.until { Taskbar.find_by(key: "Ticket-#{ticket.id}").attributes_with_association_ids['attachments'].present? }
  1326. end
  1327. def wait_for_upload_blank
  1328. wait.until { Taskbar.find_by(key: "Ticket-#{ticket.id}").attributes_with_association_ids['attachments'].blank? }
  1329. end
  1330. def switch_language_german
  1331. visit '#profile/language'
  1332. # Suppress the modal dialog that invites to contributions for translations that are < 90% as this breaks the tests for de-de.
  1333. page.evaluate_script "App.LocalStorage.set('translation_support_no', true, App.Session.get('id'))"
  1334. page.find('.js-input').click
  1335. page.find('.js-input').set('Deutsch')
  1336. page.find('.js-input').send_keys(:enter)
  1337. click_on 'Submit'
  1338. visit "#ticket/zoom/#{ticket.id}"
  1339. expect(page).to have_text(Translation.translate('de-de', 'select attachment…'))
  1340. end
  1341. def expect_upload_and_text
  1342. expect(page.find('.article-new')).to have_text('mail001.box')
  1343. expect(page.find('.article-new')).to have_text("Hello\nThis\nis\nimportant!\nyo\nhoho\ntest test test test")
  1344. end
  1345. def expect_no_upload_and_text
  1346. expect(page.find('.article-new')).to have_no_text('mail001.box')
  1347. expect(page.find('.article-new')).to have_no_text("Hello\nThis\nis\nimportant!\nyo\nhoho\ntest test test test")
  1348. end
  1349. it 'does show up the attachments after a reload of the page' do
  1350. upload_and_set_text
  1351. expect_upload_and_text
  1352. refresh
  1353. expect_upload_and_text
  1354. end
  1355. it 'does show up the attachments after updating language (ui:rerender event)' do
  1356. upload_and_set_text
  1357. expect_upload_and_text
  1358. switch_language_german
  1359. expect_upload_and_text
  1360. end
  1361. it 'does remove attachments and text on reset' do
  1362. upload_and_set_text
  1363. expect_upload_and_text
  1364. page.find('.js-reset').click
  1365. wait_for_upload_blank
  1366. expect_no_upload_and_text
  1367. refresh
  1368. expect_no_upload_and_text
  1369. end
  1370. context 'when rerendering (#3831)' do
  1371. def rerender
  1372. page.evaluate_script("App.Event.trigger('ui:rerender')")
  1373. end
  1374. it 'does loose attachments after rerender' do
  1375. upload_and_set_text
  1376. expect_upload_and_text
  1377. rerender
  1378. expect_upload_and_text
  1379. end
  1380. it 'does not readd the attachments after reset' do
  1381. upload_and_set_text
  1382. expect_upload_and_text
  1383. page.find('.js-reset').click
  1384. wait_for_upload_blank
  1385. expect_no_upload_and_text
  1386. rerender
  1387. expect_no_upload_and_text
  1388. end
  1389. it 'does not readd the attachments after submit' do
  1390. upload_and_set_text
  1391. expect_upload_and_text
  1392. page.find('.js-submit').click
  1393. wait_for_upload_blank
  1394. expect_no_upload_and_text
  1395. rerender
  1396. expect_no_upload_and_text
  1397. end
  1398. it 'does not show the ticket as changed after the upload removal' do
  1399. page.find('input#fileUpload_1[data-initialized="true"]', visible: :all).set(Rails.root.join('test/data/mail/mail001.box'))
  1400. await_empty_ajax_queue
  1401. expect(page.find('.article-new')).to have_text('mail001.box')
  1402. wait_for_upload_present
  1403. begin
  1404. page.evaluate_script("$('div.attachment-delete.js-delete:last').trigger('click')") # not interactable
  1405. rescue # Lint/SuppressedException
  1406. # because its not interactable it also
  1407. # returns this weird exception for the jquery
  1408. # even tho it worked fine
  1409. end
  1410. expect(page).to have_no_selector('.js-reset')
  1411. end
  1412. end
  1413. end
  1414. describe 'Unable to close tickets in certran cases if core workflow is used #3710', authenticated_as: :authenticate, db_strategy: :reset do
  1415. let!(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1416. let(:field_name) { SecureRandom.uuid }
  1417. let(:field) do
  1418. create(:object_manager_attribute_text, name: field_name, display: field_name, screens: {
  1419. 'edit' => {
  1420. 'ticket.agent' => {
  1421. 'shown' => false,
  1422. 'required' => false,
  1423. }
  1424. }
  1425. })
  1426. ObjectManager::Attribute.migration_execute
  1427. end
  1428. let(:workflow) do
  1429. create(:core_workflow,
  1430. object: 'Ticket',
  1431. perform: { "ticket.#{field_name}" => { 'operator' => 'set_mandatory', 'set_mandatory' => 'true' } })
  1432. end
  1433. def authenticate
  1434. field
  1435. workflow
  1436. true
  1437. end
  1438. before do
  1439. visit "#ticket/zoom/#{ticket.id}"
  1440. end
  1441. it 'does save the ticket because the field is mandatory but hidden' do
  1442. admin = User.find_by(email: 'admin@example.com')
  1443. select admin.fullname, from: 'Owner'
  1444. find('.js-submit').click
  1445. expect(ticket.reload.owner_id).to eq(admin.id)
  1446. end
  1447. end
  1448. describe "escaped 'Set fixed' workflows don't refresh set values on active ticket sessions #3757", authenticated_as: :authenticate, db_strategy: :reset do
  1449. let(:field_name) { SecureRandom.uuid }
  1450. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users'), field_name => false) }
  1451. def authenticate
  1452. workflow
  1453. create(:object_manager_attribute_boolean, :required_screen, name: field_name, display: field_name)
  1454. ObjectManager::Attribute.migration_execute
  1455. ticket
  1456. true
  1457. end
  1458. before do
  1459. visit "#ticket/zoom/#{ticket.id}"
  1460. end
  1461. context 'when operator set_fixed_to' do
  1462. let(:workflow) do
  1463. create(:core_workflow,
  1464. object: 'Ticket',
  1465. perform: { "ticket.#{field_name}" => { 'operator' => 'set_fixed_to', 'set_fixed_to' => ['false'] } })
  1466. end
  1467. context 'when saved value is removed by set_fixed_to operator' do
  1468. it 'does show up the saved value if it would not be possible because of the restriction' do
  1469. expect(page.find("select[name='#{field_name}']").value).to eq('false')
  1470. ticket.update(field_name => true)
  1471. wait.until { page.find("select[name='#{field_name}']").value == 'true' }
  1472. expect(page.find("select[name='#{field_name}']").value).to eq('true')
  1473. end
  1474. end
  1475. end
  1476. context 'when operator remove_option' do
  1477. let(:workflow) do
  1478. create(:core_workflow,
  1479. object: 'Ticket',
  1480. perform: { "ticket.#{field_name}" => { 'operator' => 'remove_option', 'remove_option' => ['true'] } })
  1481. end
  1482. context 'when saved value is removed by set_fixed_to operator' do
  1483. it 'does show up the saved value if it would not be possible because of the restriction' do
  1484. expect(page.find("select[name='#{field_name}']").value).to eq('false')
  1485. ticket.update(field_name => true)
  1486. wait.until { page.find("select[name='#{field_name}']").value == 'true' }
  1487. expect(page.find("select[name='#{field_name}']").value).to eq('true')
  1488. end
  1489. end
  1490. end
  1491. end
  1492. context 'Basic sidebar handling because of regressions in #3757' do
  1493. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1494. before do
  1495. visit "#ticket/zoom/#{ticket.id}"
  1496. ensure_websocket
  1497. end
  1498. it 'does show up the new priority' do
  1499. high_prio = Ticket::Priority.find_by(name: '3 high')
  1500. ticket.update(priority: high_prio)
  1501. wait.until { page.find("select[name='priority_id']").value == high_prio.id.to_s }
  1502. expect(page.find("select[name='priority_id']").value).to eq(high_prio.id.to_s)
  1503. end
  1504. it 'does show up the new group (different case because it will also trigger a full rerender because of potential permission changes)' do
  1505. group = Group.find_by(name: 'some group1')
  1506. ticket.update(group: group)
  1507. wait.until { page.find("input[name='group_id']", visible: :all).value == group.id.to_s }
  1508. expect(page.find("input[name='group_id']", visible: :all).value).to eq(group.id.to_s)
  1509. end
  1510. it 'does show up the new state and pending time' do
  1511. pending_state = Ticket::State.find_by(name: 'pending reminder')
  1512. ticket.update(state: pending_state, pending_time: 1.day.from_now)
  1513. wait.until { page.find("select[name='state_id']").value == pending_state.id.to_s }
  1514. expect(page.find("select[name='state_id']").value).to eq(pending_state.id.to_s)
  1515. expect(page).to have_css("div[data-name='pending_time']")
  1516. end
  1517. it 'does merge attributes with remote priority (ajax) and local state (user)' do
  1518. select 'closed', from: 'State'
  1519. high_prio = Ticket::Priority.find_by(name: '3 high')
  1520. closed_state = Ticket::State.find_by(name: 'closed')
  1521. ticket.update(priority: high_prio)
  1522. wait.until { page.find("select[name='priority_id']").value == high_prio.id.to_s }
  1523. expect(page.find("select[name='priority_id']").value).to eq(high_prio.id.to_s)
  1524. expect(page.find("select[name='state_id']").value).to eq(closed_state.id.to_s)
  1525. end
  1526. context 'when 2 users are in 2 different tickets' do
  1527. let(:ticket2) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1528. let(:agent2) { create(:agent, password: 'test', groups: [Group.find_by(name: 'Users')]) }
  1529. before do
  1530. using_session(:second_browser) do
  1531. login(
  1532. username: agent2.login,
  1533. password: 'test',
  1534. )
  1535. visit "#ticket/zoom/#{ticket.id}"
  1536. visit "#ticket/zoom/#{ticket2.id}"
  1537. end
  1538. end
  1539. it 'does not make any changes to the second browser ticket' do
  1540. closed_state = Ticket::State.find_by(name: 'closed')
  1541. select 'closed', from: 'State'
  1542. find('.js-submit').click
  1543. using_session(:second_browser) do
  1544. sleep 3
  1545. expect(page.find("select[name='state_id']").value).not_to eq(closed_state.id.to_s)
  1546. end
  1547. end
  1548. end
  1549. end
  1550. context 'Article box opening on tickets with no changes #3789' do
  1551. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1552. before do
  1553. visit "#ticket/zoom/#{ticket.id}"
  1554. end
  1555. it 'does not expand the article box without changes' do
  1556. refresh
  1557. sleep 3
  1558. expect(page).to have_no_selector('form.article-add.is-open')
  1559. end
  1560. it 'does open and close by usage' do
  1561. find('.js-writeArea').click
  1562. find('.js-textarea').send_keys(' ')
  1563. expect(page).to have_css('form.article-add.is-open')
  1564. find('input#global-search').click
  1565. expect(page).to have_no_selector('form.article-add.is-open')
  1566. end
  1567. it 'does open automatically when body is given from sidebar' do
  1568. find('.js-textarea').send_keys('test')
  1569. wait.until { Taskbar.find_by(key: "Ticket-#{ticket.id}").state.dig('article', 'body').present? }
  1570. refresh
  1571. expect(page).to have_css('form.article-add.is-open')
  1572. end
  1573. it 'does open automatically when attachment is given from sidebar' do
  1574. page.find('input#fileUpload_1[data-initialized="true"]', visible: :all).set(Rails.root.join('test/data/mail/mail001.box'))
  1575. wait.until { Taskbar.find_by(key: "Ticket-#{ticket.id}").attributes_with_association_ids['attachments'].present? }
  1576. refresh
  1577. expect(page).to have_css('form.article-add.is-open')
  1578. end
  1579. end
  1580. context 'Owner should get cleared if not listed in changed group #3818', authenticated_as: :authenticate do
  1581. let(:group1) { create(:group) }
  1582. let(:group2) { create(:group) }
  1583. let(:agent1) { create(:agent) }
  1584. let(:agent2) { create(:agent) }
  1585. let(:ticket) { create(:ticket, group: group1, owner: agent1) }
  1586. def authenticate
  1587. agent1.group_names_access_map = {
  1588. group1.name => 'full',
  1589. group2.name => %w[read change overview]
  1590. }
  1591. agent2.group_names_access_map = {
  1592. group1.name => 'full',
  1593. group2.name => 'full',
  1594. }
  1595. agent1
  1596. end
  1597. before do
  1598. visit "#ticket/zoom/#{ticket.id}"
  1599. end
  1600. it 'does clear agent1 on select of group 2' do
  1601. set_tree_select_value('group_id', group2.name)
  1602. wait.until { page.find('select[name=owner_id]').value != agent1.id.to_s }
  1603. expect(page.find('select[name=owner_id]').value).to eq('')
  1604. expect(page.all('select[name=owner_id] option').map(&:value)).not_to include(agent1.id.to_s)
  1605. expect(page.all('select[name=owner_id] option').map(&:value)).to include(agent2.id.to_s)
  1606. end
  1607. end
  1608. describe 'Not displayed fields should not impact the edit screen #3819', authenticated_as: :authenticate, db_strategy: :reset do
  1609. let(:field_name) { SecureRandom.uuid }
  1610. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1611. def authenticate
  1612. create(:object_manager_attribute_boolean, default: nil, screens: {
  1613. edit: {
  1614. 'ticket.agent' => {
  1615. shown: false,
  1616. required: false,
  1617. }
  1618. }
  1619. })
  1620. ObjectManager::Attribute.migration_execute
  1621. ticket
  1622. true
  1623. end
  1624. before do
  1625. visit "#ticket/zoom/#{ticket.id}"
  1626. end
  1627. it 'does not show any changes for the field because it has no value and because it is not shown it should also not show the ticket as changed' do
  1628. sleep 3
  1629. expect(page).to have_no_selector('.js-reset')
  1630. end
  1631. end
  1632. describe 'Changing ticket status from "new" to any other status always results in uncommited status "closed" #3880', authenticated_as: :authenticate do
  1633. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1634. let(:workflow) do
  1635. create(:core_workflow,
  1636. object: 'Ticket',
  1637. condition_selected: {
  1638. 'ticket.priority_id': {
  1639. operator: 'is',
  1640. value: [ Ticket::Priority.find_by(name: '3 high').id.to_s ],
  1641. },
  1642. },
  1643. perform: { 'ticket.state_id' => { operator: 'remove_option', remove_option: [ Ticket::State.find_by(name: 'pending reminder').id.to_s ] } })
  1644. end
  1645. def authenticate
  1646. workflow
  1647. true
  1648. end
  1649. before do
  1650. visit "#ticket/zoom/#{ticket.id}"
  1651. end
  1652. it 'does switch back to the saved value in the ticket instead of the first value of the dropdown' do
  1653. page.select 'pending reminder', from: 'state_id'
  1654. page.select '3 high', from: 'priority_id'
  1655. expect(page).to have_select('state_id', selected: 'new')
  1656. end
  1657. end
  1658. describe 'Multiselect marked as dirty', authenticated_as: :authenticate, db_strategy: :reset do
  1659. let(:field_name) { SecureRandom.uuid }
  1660. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users'), field_name => []) }
  1661. def authenticate
  1662. create(:object_manager_attribute_multiselect, name: field_name, display: field_name, screens: {
  1663. 'edit' => {
  1664. 'ticket.agent' => {
  1665. 'shown' => true,
  1666. 'required' => false,
  1667. }
  1668. }
  1669. })
  1670. ObjectManager::Attribute.migration_execute
  1671. ticket
  1672. true
  1673. end
  1674. before do
  1675. visit "#ticket/zoom/#{ticket.id}"
  1676. end
  1677. it 'does show values properly and can save values also' do
  1678. expect(page).to have_no_css('.attributeBar-reset')
  1679. end
  1680. end
  1681. describe 'Multiselect displaying and saving', authenticated_as: :authenticate, db_strategy: :reset do
  1682. let(:field_name) { SecureRandom.uuid }
  1683. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users'), field_name => %w[value_2 value_3]) }
  1684. let(:options_hash) do
  1685. {
  1686. 'value_1' => 'value_1',
  1687. 'value_2' => 'value_2',
  1688. 'value_3' => 'value_3',
  1689. }
  1690. end
  1691. let(:data_option) { { options: options_hash, default: [] } }
  1692. def authenticate
  1693. create(:object_manager_attribute_multiselect, name: field_name, display: field_name, data_option: data_option, screens: {
  1694. 'edit' => {
  1695. 'ticket.agent' => {
  1696. 'shown' => true,
  1697. 'required' => false,
  1698. }
  1699. }
  1700. })
  1701. ObjectManager::Attribute.migration_execute
  1702. ticket
  1703. true
  1704. end
  1705. before do
  1706. visit "#ticket/zoom/#{ticket.id}"
  1707. end
  1708. def multiselect_field
  1709. page.find("select[name='#{field_name}']")
  1710. end
  1711. def multiselect_value
  1712. multiselect_field.value
  1713. end
  1714. def multiselect_set(values)
  1715. multiselect_unset_all
  1716. values = Array(values)
  1717. values.each do |value|
  1718. multiselect_field.select(value)
  1719. expect(multiselect_value).to include(value)
  1720. end
  1721. end
  1722. def multiselect_unset_all
  1723. values = multiselect_value
  1724. values.each do |value|
  1725. multiselect_field.unselect(value)
  1726. expect(multiselect_value).to not_include(value)
  1727. end
  1728. end
  1729. context 'when showing and saving values' do
  1730. it 'shows saved values properly' do
  1731. expect(multiselect_value).to eq(%w[value_2 value_3])
  1732. end
  1733. it 'saves multiple values properly' do
  1734. multiselect_set(%w[value_1 value_2])
  1735. click '.js-submit'
  1736. expect(ticket.reload[field_name]).to eq(%w[value_1 value_2])
  1737. end
  1738. it 'saves single value properly' do
  1739. multiselect_set(%w[value_1])
  1740. click '.js-submit'
  1741. expect(ticket.reload[field_name]).to eq(%w[value_1])
  1742. end
  1743. it 'removes saved values properly' do
  1744. multiselect_unset_all
  1745. click '.js-submit'
  1746. expect(ticket.reload[field_name]).to be_empty
  1747. end
  1748. end
  1749. end
  1750. describe 'Add confirmation dialog on visibility change of an article or in article creation #3924', authenticated_as: :authenticate do
  1751. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1752. let(:article) { create(:ticket_article, ticket: ticket) }
  1753. before do
  1754. visit "#ticket/zoom/#{article.ticket.id}"
  1755. end
  1756. context 'when dialog is disabled' do
  1757. def authenticate
  1758. true
  1759. end
  1760. it 'does set the article internal and external for existing articles' do
  1761. expect { page.find('.js-ArticleAction[data-type=internal]').click }.to change { article.reload.internal }.to(true)
  1762. expect { page.find('.js-ArticleAction[data-type=public]').click }.to change { article.reload.internal }.to(false)
  1763. end
  1764. it 'does set the article internal and external for new article' do
  1765. page.find('.js-writeArea').click(x: 5, y: 5)
  1766. expect(page).to have_css('.article-new .icon-internal')
  1767. expect(page).to have_no_css('.article-new .icon-public')
  1768. page.find('.article-new .icon-internal').click
  1769. expect(page).to have_no_css('.article-new .icon-internal')
  1770. expect(page).to have_css('.article-new .icon-public')
  1771. page.find('.article-new .icon-public').click
  1772. expect(page).to have_css('.article-new .icon-internal')
  1773. expect(page).to have_no_css('.article-new .icon-public')
  1774. end
  1775. end
  1776. context 'when dialog is enabled' do
  1777. def authenticate
  1778. Setting.set('ui_ticket_zoom_article_visibility_confirmation_dialog', true)
  1779. true
  1780. end
  1781. it 'does set the article internal and external for existing articles' do
  1782. expect { page.find('.js-ArticleAction[data-type=internal]').click }.to change { article.reload.internal }.to(true)
  1783. page.find('.js-ArticleAction[data-type=public]').click
  1784. in_modal do
  1785. expect { find('button[type=submit]').click }.to change { article.reload.internal }.to(false)
  1786. end
  1787. end
  1788. it 'does set the article internal and external for new article' do
  1789. page.find('.js-writeArea').click(x: 5, y: 5)
  1790. expect(page).to have_css('.article-new .icon-internal')
  1791. expect(page).to have_no_css('.article-new .icon-public')
  1792. page.find('.article-new .icon-internal').click
  1793. in_modal do
  1794. find('button[type=submit]').click
  1795. end
  1796. expect(page).to have_no_css('.article-new .icon-internal')
  1797. expect(page).to have_css('.article-new .icon-public')
  1798. page.find('.article-new .icon-public').click
  1799. expect(page).to have_css('.article-new .icon-internal')
  1800. expect(page).to have_no_css('.article-new .icon-public')
  1801. end
  1802. end
  1803. end
  1804. describe 'Show which escalation type escalated in ticket zoom #3928', authenticated_as: :authenticate do
  1805. let(:sla) { create(:sla, first_response_time: 1, update_time: 1, solution_time: 1) }
  1806. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1807. def authenticate
  1808. sla
  1809. true
  1810. end
  1811. before do
  1812. visit "#ticket/zoom/#{ticket.id}"
  1813. end
  1814. it 'does show the extended escalation information' do
  1815. page.find('.escalation-popover').hover
  1816. expect(page).to have_text('FIRST RESPONSE TIME')
  1817. expect(page).to have_text('UPDATE TIME')
  1818. expect(page).to have_text('SOLUTION TIME')
  1819. end
  1820. end
  1821. context 'Make sidebar attachments unique #3930', authenticated_as: :authenticate do
  1822. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1823. let(:article1) { create(:ticket_article, ticket: ticket) }
  1824. let(:article2) { create(:ticket_article, ticket: ticket) }
  1825. def attachment_add(article, filename)
  1826. create(:store,
  1827. object: 'Ticket::Article',
  1828. o_id: article.id,
  1829. data: "content #{filename}",
  1830. filename: filename,
  1831. preferences: {
  1832. 'Content-Type' => 'text/plain',
  1833. })
  1834. end
  1835. def authenticate
  1836. attachment_add(article1, 'some_file.txt')
  1837. attachment_add(article2, 'some_file.txt')
  1838. attachment_add(article2, 'some_file2.txt')
  1839. Setting.set('ui_ticket_zoom_sidebar_article_attachments', true)
  1840. true
  1841. end
  1842. before do
  1843. visit "#ticket/zoom/#{ticket.id}"
  1844. page.find(".tabsSidebar-tabs .tabsSidebar-tab[data-tab='attachment']").click
  1845. end
  1846. it 'does show the attachment once' do
  1847. expect(page).to have_css('.sidebar-content .attachment.attachment--preview', count: 2)
  1848. expect(page).to have_css('.sidebar-content', text: 'some_file.txt')
  1849. expect(page).to have_css('.sidebar-content', text: 'some_file2.txt')
  1850. end
  1851. it 'does show up new attachments' do
  1852. page.find('.js-textarea').send_keys('new article with attachment')
  1853. page.find('input#fileUpload_1', visible: :all).set(Rails.root.join('test/data/mail/mail001.box'))
  1854. expect(page).to have_text('mail001.box')
  1855. wait.until { Taskbar.find_by(key: "Ticket-#{ticket.id}").attributes_with_association_ids['attachments'].present? }
  1856. click '.js-submit'
  1857. expect(page).to have_css('.sidebar-content', text: 'mail001.box')
  1858. end
  1859. end
  1860. describe 'Error “customer_id required” on Macro execution #4022', authenticated_as: :authenticate do
  1861. let(:ticket) { create(:ticket, group: Group.first) }
  1862. let(:macro) { create(:macro, perform: { 'ticket.customer_id'=>{ 'pre_condition' => 'current_user.id', 'value' => nil, 'value_completion' => '' } }) }
  1863. def authenticate
  1864. ticket && macro
  1865. true
  1866. end
  1867. before do
  1868. visit "#ticket/zoom/#{ticket.id}"
  1869. end
  1870. it 'does set the agent as customer via macro' do
  1871. click '.js-openDropdownMacro'
  1872. page.find(:macro, macro.id).click
  1873. expect(ticket.reload.customer_id).to eq(User.find_by(email: 'admin@example.com').id)
  1874. end
  1875. end
  1876. context 'Assign user to multiple organizations #1573', authenticated_as: :authenticate do
  1877. let(:organizations) { create_list(:organization, 20) }
  1878. let(:customer) { create(:customer, organization: organizations[0], organizations: organizations[1..]) }
  1879. let(:ticket) { create(:ticket, group: Group.first, customer: customer) }
  1880. def authenticate
  1881. customer
  1882. true
  1883. end
  1884. before do
  1885. visit "#ticket/zoom/#{ticket.id}"
  1886. click '.tabsSidebar-tab[data-tab=customer]'
  1887. end
  1888. it 'shows only first 3 organizations and loads more on demand' do
  1889. expect(page).to have_text(organizations[1].name)
  1890. expect(page).to have_text(organizations[2].name)
  1891. expect(page).to have_no_text(organizations[10].name)
  1892. click '.js-showMoreOrganizations a'
  1893. expect(page).to have_text(organizations[10].name)
  1894. end
  1895. end
  1896. describe 'Image preview #4044' do
  1897. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1898. let(:image_as_base64) do
  1899. file = Rails.root.join('spec/fixtures/files/image/squares.png').binread
  1900. Base64.encode64(file).delete("\n")
  1901. end
  1902. let(:body) do
  1903. "<img style='width: 1004px; max-width: 100%;' src='data:image/png;base64,#{image_as_base64}'><br>"
  1904. end
  1905. let(:article) { create(:ticket_article, ticket: ticket, body: body, content_type: 'text/html') }
  1906. before do
  1907. visit "#ticket/zoom/#{article.ticket.id}"
  1908. end
  1909. it 'does open the image preview for a common image' do
  1910. within :active_ticket_article, article do
  1911. find('img').click
  1912. end
  1913. in_modal do
  1914. expect(page).to have_css('div.imagePreview img')
  1915. expect(page).to have_css('.js-cancel')
  1916. expect(page).to have_css('.js-submit')
  1917. page.find('.js-cancel').click
  1918. end
  1919. end
  1920. context 'with image and embedded link' do
  1921. let(:body) do
  1922. "<a href='https://zammad.com' title='Zammad' target='_blank'>
  1923. <img style='width: 1004px; max-width: 100%;' src='data:image/png;base64,#{image_as_base64}'>
  1924. </a><br>"
  1925. end
  1926. it 'does open the link for an image with an embedded link' do
  1927. within :active_ticket_article, article do
  1928. find('img').click
  1929. end
  1930. within_window switch_to_window_index(2) do
  1931. expect(page).to have_link(class: ['logo'])
  1932. end
  1933. close_window_index(2)
  1934. end
  1935. end
  1936. end
  1937. describe 'Copying ticket number' do
  1938. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1939. let(:ticket_number_copy_element) { 'span.ticket-number-copy svg.ticketNumberCopy-icon' }
  1940. let(:expected_clipboard_content) { (Setting.get('ticket_hook') + ticket.number).to_s }
  1941. let(:field) { find(:richtext) }
  1942. before do
  1943. visit "#ticket/zoom/#{ticket.id}"
  1944. end
  1945. it 'copies the ticket number correctly' do
  1946. find(ticket_number_copy_element).click
  1947. # simulate a paste action
  1948. within(:active_content) do
  1949. field.send_keys('')
  1950. field.click
  1951. field.send_keys([magic_key, 'v'])
  1952. end
  1953. expect(field.text).to eq(expected_clipboard_content)
  1954. end
  1955. end
  1956. describe 'Allow additional usage of Ticket Number in (Zoom) URL #849' do
  1957. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1958. it 'does find the ticket by ticket number' do
  1959. visit "#ticket/zoom/number/#{ticket.number}"
  1960. expect(current_url).to include("ticket/zoom/#{ticket.id}")
  1961. end
  1962. it 'does fail properly for ticket numbers which are not found' do
  1963. visit '#ticket/zoom/number/123456789'
  1964. expect(page).to have_text("I can't find this Ticket")
  1965. end
  1966. end
  1967. describe 'Article update causes missing icons in the UI after switching internal state #4213' do
  1968. let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
  1969. let(:article) { create(:ticket_article, ticket: ticket, body: SecureRandom.uuid) }
  1970. before do
  1971. visit "#ticket/zoom/#{article.ticket.id}"
  1972. end
  1973. it 'does find the ticket by ticket number' do
  1974. expect(page).to have_text(article.body)
  1975. article.update(body: SecureRandom.uuid)
  1976. expect(page).to have_text(article.body)
  1977. click '.js-ArticleAction[data-type=internal]'
  1978. click '.js-ArticleAction[data-type=public]'
  1979. expect(page).to have_css('.js-ArticleAction[data-type=emailReply]')
  1980. end
  1981. end
  1982. describe 'Open ticket indicator coloring setting' do
  1983. let(:elem) { find('[data-tab="customer"]') }
  1984. let(:customer) { create(:customer) }
  1985. let(:group) { Group.first }
  1986. let(:ticket) { create(:ticket, customer: customer, group: group) }
  1987. before do
  1988. Setting.set 'ui_sidebar_open_ticket_indicator_colored', state
  1989. customer.update! preferences: { tickets_open: tickets_count }
  1990. visit "ticket/zoom/#{ticket.id}"
  1991. end
  1992. context 'when enabled' do
  1993. let(:state) { true }
  1994. context 'with 1 ticket' do
  1995. let(:tickets_count) { 1 }
  1996. it 'does not highlight' do
  1997. expect(elem)
  1998. .to have_no_selector('.tabsSidebar-tab-count--danger, .tabsSidebar-tab-count--warning')
  1999. end
  2000. end
  2001. context 'with 2 tickets' do
  2002. let(:tickets_count) { 2 }
  2003. it 'highlights as warning' do
  2004. create(:ticket, customer: customer, group: group)
  2005. expect(elem)
  2006. .to have_no_selector('.tabsSidebar-tab-count--danger')
  2007. .and have_css('.tabsSidebar-tab-count--warning')
  2008. end
  2009. end
  2010. context 'with 3 tickets' do
  2011. let(:tickets_count) { 3 }
  2012. it 'highlights as danger' do
  2013. expect(elem)
  2014. .to have_css('.tabsSidebar-tab-count--danger')
  2015. .and have_no_selector('.tabsSidebar-tab-count--warning')
  2016. end
  2017. end
  2018. end
  2019. context 'when disabled' do
  2020. let(:state) { false }
  2021. context 'with 2 tickets' do
  2022. let(:tickets_count) { 2 }
  2023. it 'does not highlight' do
  2024. expect(elem)
  2025. .to have_no_selector('.tabsSidebar-tab-count--danger, .tabsSidebar-tab-count--warning')
  2026. end
  2027. end
  2028. end
  2029. end
  2030. describe 'Incorrect notification when closing a tab after setting up an object #2042', db_strategy: :reset do
  2031. let(:ticket) { create(:ticket, group: Group.first) }
  2032. before do
  2033. # Create test ticket before adding the test object attribute.
  2034. ticket
  2035. create(:object_manager_attribute_text, name: 'text_test', display: 'text_test', screens: {
  2036. 'edit' => {
  2037. 'ticket.agent' => {
  2038. 'shown' => true,
  2039. 'required' => true,
  2040. }
  2041. }
  2042. })
  2043. ObjectManager::Attribute.migration_execute
  2044. visit "#ticket/zoom/#{ticket.id}"
  2045. end
  2046. it 'does not show the discard button nor the confirmation dialog' do
  2047. within(:active_content) do
  2048. expect(page).to have_no_css('.js-reset')
  2049. end
  2050. taskbar_tab_close("Ticket-#{ticket.id}", discard_changes: false)
  2051. expect(page).to have_no_css('.modal')
  2052. end
  2053. end
  2054. end