ticket_article_actions_spec.rb 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. require 'system/apps/mobile_old/examples/reply_article_examples'
  4. RSpec.describe 'Mobile > Ticket > Article actions', app: :mobile, authenticated_as: :agent, type: :system do
  5. let(:group) { Group.find_by(name: 'Users') }
  6. let(:agent) { create(:agent, groups: [group]) }
  7. let(:customer) { create(:customer, email: 'customer@example.com') }
  8. let(:ticket) { create(:ticket, customer: customer, group: group) }
  9. let(:to) { nil }
  10. let(:new_to) { nil }
  11. let(:result_to) { new_to || to }
  12. let(:cc) { nil }
  13. let(:article_subject) { nil }
  14. let(:before_click) { -> {} }
  15. let(:after_click) { -> {} }
  16. let(:new_subject) { nil }
  17. let(:trigger_label) { 'Reply' }
  18. let(:text_exact) { true }
  19. let(:current_text) { '' }
  20. let(:new_text) { 'This is a note' }
  21. let(:result_attachments) { [Store.last] }
  22. let(:result_text) { new_text || current_text }
  23. let(:in_reply_to) { article.message_id }
  24. let(:type_id) { article.type_id }
  25. def select_text(selector)
  26. js = %{
  27. var range = document.createRange();
  28. var selection = window.getSelection();
  29. range.selectNodeContents(document.querySelector('#{selector}'));
  30. selection.removeAllRanges();
  31. selection.addRange(range);
  32. }
  33. page.execute_script(js)
  34. end
  35. def open_article_reply_dialog()
  36. article
  37. visit "/tickets/#{ticket.id}"
  38. wait_for_gql('shared/entities/ticket/graphql/queries/ticket/articles.graphql')
  39. wait_for_form_to_settle('form-ticket-edit')
  40. before_click.call
  41. find_button('Article actions').click
  42. find_button(trigger_label).click
  43. end
  44. # we test article creation mostly on the backend because Node.js doesn't support prose-mirror
  45. context 'when article was created as email' do
  46. let(:signature) { create(:signature, active: true, body: "\#{user.firstname}<br>Signature!") }
  47. let(:group) { create(:group, signature: signature) }
  48. let(:to) { [Mail::AddressList.new(article.to).addresses.first.address] }
  49. let(:article) { create(:ticket_article, :outbound_email, ticket: ticket) }
  50. let(:current_text) { "#{agent.firstname}\nSignature!" }
  51. let(:signature_html) { "<div data-signature=\"true\" data-signature-id=\"#{signature.id}\"><p>#{agent.firstname}<br>Signature!</p></div>" }
  52. let(:result_text) { "<p>This is a note</p><p><br></p>#{signature_html}" }
  53. let(:after_click) do
  54. lambda {
  55. # wait for signature to be added
  56. wait_for_test_flag('editor.signatureAdd')
  57. }
  58. end
  59. context 'with default fields as outbound email' do
  60. include_examples 'mobile app: reply article', 'Email', attachments: true do
  61. let(:article) { create(:ticket_article, :outbound_email, ticket: ticket, from: 'from-email@example.com', to: 'to-email@example.com') }
  62. let(:to) { ['to-email@example.com'] }
  63. end
  64. end
  65. context 'with default fields as inbound email' do
  66. include_examples 'mobile app: reply article', 'Email', attachments: true do
  67. let(:article) { create(:ticket_article, :inbound_email, ticket: ticket, from: 'from-email@example.com', to: 'to-email@example.com') }
  68. let(:to) { ['from-email@example.com'] }
  69. end
  70. end
  71. context 'with default fields when article has type phone' do
  72. let(:type_id) { Ticket::Article::Type.find_by(name: 'email').id }
  73. context 'when agent sent article take article email' do
  74. include_examples 'mobile app: reply article', 'Email', attachments: true do
  75. let(:article) { create(:ticket_article, :outbound_phone, ticket: ticket, to: 'to-email@example.com') }
  76. let(:to) { ['to-email@example.com'] }
  77. end
  78. end
  79. context 'when customer sent article from phone take customer email' do
  80. include_examples 'mobile app: reply article', 'Email', attachments: true do
  81. let(:article) { create(:ticket_article, :inbound_phone, ticket: ticket, from: '+423424235533') }
  82. let(:to) { ['customer@example.com'] }
  83. end
  84. end
  85. end
  86. context 'with selected text and quote header' do
  87. before do
  88. Setting.set('ui_ticket_zoom_article_email_full_quote_header', true)
  89. end
  90. include_examples 'mobile app: reply article', 'Email', attachments: true do
  91. let(:before_click) do
  92. lambda {
  93. select_text('.Content')
  94. }
  95. end
  96. let(:current_text) { %r{On .+, #{article.created_by.fullname} wrote:\s+#{article.body}\s+#{agent.firstname}\nSignature!} }
  97. let(:result_text) do
  98. msg = '<p>This is a note<br><br></p>'
  99. msg += '<blockquote type="cite">\n'
  100. msg += "<p>On .+, #{article.created_by.fullname} wrote:</p>\n<p><br></p>\n"
  101. msg += "<p>#{article.body}</p>\n"
  102. msg += '</blockquote><p><br></p>'
  103. msg += signature_html
  104. a_string_matching(Regexp.new(msg))
  105. end
  106. end
  107. end
  108. context 'with selected text without quote header' do
  109. before do
  110. Setting.set('ui_ticket_zoom_article_email_full_quote_header', false)
  111. end
  112. include_examples 'mobile app: reply article', 'Email', attachments: true do
  113. let(:before_click) do
  114. lambda {
  115. select_text('.Content')
  116. }
  117. end
  118. let(:current_text) { "#{article.body}\n\n#{agent.firstname}\nSignature!" }
  119. let(:result_text) do
  120. "<p>This is a note<br><br></p><blockquote type=\"cite\"><p>#{article.body}</p></blockquote><p><br></p>#{signature_html}"
  121. end
  122. end
  123. end
  124. context 'with selected text when new article is already written' do
  125. before do
  126. Setting.set('ui_ticket_zoom_article_email_full_quote_header', false)
  127. end
  128. include_examples 'mobile app: reply article', 'Email', attachments: true do
  129. let(:before_click) do
  130. lambda {
  131. find_button('Add reply').click
  132. find_editor('Text').type('Text before replying')
  133. find_button('Done').click
  134. wait_for_test_flag('ticket-article-reply.closed')
  135. select_text('.Content')
  136. }
  137. end
  138. let(:current_text) { "#{article.body}\n\nText before replying\n\n#{agent.firstname}\nSignature!" }
  139. let(:result_text) do
  140. "<p>This is a note<br><br></p><blockquote type=\"cite\"><p>#{article.body}</p></blockquote><p><br></p><p>Text before replying</p><p><br></p>#{signature_html}"
  141. end
  142. end
  143. end
  144. context 'when full quote is enabled and new article is already written' do
  145. before do
  146. Setting.set('ui_ticket_zoom_article_email_full_quote_header', false)
  147. Setting.set('ui_ticket_zoom_article_email_full_quote', true)
  148. end
  149. include_examples 'mobile app: reply article', 'Email', attachments: true do
  150. let(:before_click) do
  151. lambda {
  152. find_button('Add reply').click
  153. find_editor('Text').type('Text before replying')
  154. find_button('Done').click
  155. wait_for_test_flag('ticket-article-reply.closed')
  156. }
  157. end
  158. let(:current_text) { "#{agent.firstname}\nSignature!\n#{article.body}\n\nText before replying" }
  159. let(:result_text) do
  160. "<p>This is a note</p>#{signature_html}<blockquote type=\"cite\"><p>#{article.body}</p></blockquote><p><br></p><p>Text before replying</p>"
  161. end
  162. end
  163. end
  164. context 'when article has multiple email addresses, can reply all' do
  165. include_examples 'mobile app: reply article', 'Email', attachments: true do
  166. let(:trigger_label) { 'Reply All' }
  167. let(:to) { ['e1@example.com', 'e2@example.com'] }
  168. let(:cc) { ['e3@example.com'] }
  169. let(:article) { create(:ticket_article, :outbound_email, ticket: ticket, to: to.join(', '), cc: cc.join(', ')) }
  170. end
  171. end
  172. context 'when subject is enabled' do
  173. before do
  174. Setting.set('ui_ticket_zoom_article_email_subject', true)
  175. end
  176. context 'when article has a subject use subject' do
  177. include_examples 'mobile app: reply article', 'Email', attachments: true do
  178. let(:article_subject) { 'Hello World' }
  179. let(:article) { create(:ticket_article, :outbound_email, ticket: ticket, subject: article_subject) }
  180. end
  181. end
  182. context 'when article doesn\'t have a subject use ticket title' do
  183. include_examples 'mobile app: reply article', 'Email', attachments: true do
  184. let(:article) { create(:ticket_article, :outbound_email, ticket: ticket, subject: nil) }
  185. let(:article_subject) { ticket.title }
  186. end
  187. end
  188. end
  189. context 'when adding multiple replies' do
  190. before do
  191. article
  192. end
  193. it 'keeps signature' do
  194. visit "/tickets/#{ticket.id}"
  195. wait_for_form_to_settle('form-ticket-edit')
  196. find_button('Article actions').click
  197. find_button('Reply').click
  198. wait_for_test_flag('ticket-article-reply.opened')
  199. expect(find_editor('Text')).to have_text_value("#{agent.firstname}\nSignature!")
  200. find_editor('Text').clear
  201. expect(find_editor('Text')).to have_text_value('', exact: true)
  202. find_button('Done').click
  203. wait_for_test_flag('ticket-article-reply.closed')
  204. find_button('Article actions').click
  205. find_button('Reply').click
  206. wait_for_test_flag('ticket-article-reply.opened')
  207. expect(find_editor('Text')).to have_text_value("#{agent.firstname}\nSignature!")
  208. end
  209. end
  210. context 'when forwarding email' do
  211. let(:trigger_label) { 'Forward' }
  212. let(:to) { [] }
  213. let(:new_to) { 'test@example.com' }
  214. let(:article) { create(:ticket_article, :outbound_email, ticket: ticket, subject: 'Article Subject') }
  215. let(:article_subject) { article.subject }
  216. let(:text_to) { article.to }
  217. let(:current_text) do
  218. msg = "#{agent.firstname}\nSignature!"
  219. msg += '\n\n---Begin forwarded message:---\n\n'
  220. msg += "Subject: #{article_subject}\n"
  221. msg += 'Date: \\d{2}/\\d{2}/\\d{4} \\d{1,2}:\\d{1,2} (am|pm)\n'
  222. msg += "To: #{text_to}\n\n"
  223. msg += article.body
  224. Regexp.new(msg)
  225. end
  226. let(:in_reply_to) { '' }
  227. let(:result_text) do
  228. msg = '<p>This is a note</p>' # new message
  229. msg += "<div data-signature=\"true\" data-signature-id=\"#{signature.id}\"><p>#{agent.firstname}<br>Signature!</p></div>" # signature is before forwarded message
  230. msg += '<p><br></p><p>---Begin forwarded message:---</p><p><br></p>' # new lines and "before" message
  231. # blockquote with original message and header with subject, date and "to"
  232. msg += "<blockquote type=\"cite\">\n"
  233. msg += "<p>Subject: #{article_subject}<br>"
  234. msg += 'Date: \\d{2}/\\d{2}/\\d{4} \\d{1,2}:\\d{1,2} (am|pm)<br>'
  235. msg += "To: #{escape_html_wo_single_quotes(text_to)}<br><br></p>\n"
  236. msg += "<p>#{article.body}</p>\n"
  237. msg += '</blockquote>'
  238. a_string_matching(Regexp.new(msg))
  239. end
  240. before do
  241. Setting.set('ui_ticket_zoom_article_email_subject', true)
  242. Setting.set('ui_ticket_zoom_article_email_full_quote_header', true)
  243. end
  244. context 'with attachments' do
  245. let(:result_attachments) { Store.last(2) }
  246. let(:article) do
  247. article = create(:ticket_article, :outbound_email, ticket: ticket, subject: 'Article Subject')
  248. create(
  249. :store,
  250. object: 'Ticket::Article',
  251. o_id: article.id,
  252. data: Rails.root.join('spec/fixtures/files/image/small.png').binread,
  253. filename: 'small-original.png'
  254. )
  255. article
  256. end
  257. include_examples 'mobile app: reply article', 'Email', attachments: true
  258. end
  259. context 'without attachments' do
  260. include_examples 'mobile app: reply article', 'Email', attachments: true
  261. end
  262. context 'when forwarding phone article' do
  263. let(:article) { create(:ticket_article, :outbound_phone, ticket: ticket) }
  264. let(:text_to) { "#{ticket.customer.fullname} <#{ticket.customer.email}>" }
  265. let(:type_id) { Ticket::Article::Type.find_by(name: 'email').id }
  266. include_examples 'mobile app: reply article', 'Email', attachments: true
  267. end
  268. context 'without a header' do
  269. let(:current_text) do
  270. msg = "#{agent.firstname}\nSignature!"
  271. msg += "\n\n---Begin forwarded message:---\n\n"
  272. msg += article.body
  273. Regexp.new(msg)
  274. end
  275. let(:result_text) do
  276. msg = '<p>This is a note</p>' # new message
  277. msg += "<div data-signature=\"true\" data-signature-id=\"#{signature.id}\"><p>#{agent.firstname}<br>Signature!</p></div>" # signature is before forwarded message
  278. msg += '<p><br></p><p>---Begin forwarded message:---</p><p><br></p>' # new lines and "before" message
  279. # blockquote with original message and no header
  280. msg += "<blockquote type=\"cite\"><p>#{article.body}</p></blockquote>"
  281. a_string_matching(Regexp.new(msg))
  282. end
  283. before do
  284. Setting.set('ui_ticket_zoom_article_email_full_quote_header', false)
  285. end
  286. include_examples 'mobile app: reply article', 'Email', attachments: true
  287. end
  288. end
  289. end
  290. context 'when article was created as sms' do
  291. let(:article) do
  292. create(
  293. :ticket_article,
  294. ticket: ticket,
  295. sender: Ticket::Article::Sender.lookup(name: 'Customer'),
  296. type: Ticket::Article::Type.lookup(name: 'sms'),
  297. from: '+41234567890'
  298. )
  299. end
  300. context 'with default fields' do
  301. include_examples 'mobile app: reply article', 'Sms', 'with default fields' do
  302. let(:to) { ['+41234567890'] }
  303. end
  304. end
  305. context 'with additional custom recipient' do
  306. let(:phone_number) { Faker::PhoneNumber.cell_phone_in_e164 }
  307. include_examples 'mobile app: reply article', 'Sms', 'to another recipient number' do
  308. let(:new_to) { phone_number }
  309. let(:result_to) { [phone_number, '+41234567890'] }
  310. end
  311. end
  312. it 'cannot create large article' do
  313. open_article_reply_dialog
  314. find_editor('Text').type(Faker::Lorem.characters(number: 161))
  315. click_on('Save')
  316. expect(find_editor('Text')).to have_text('This field must contain between 1 and 160 characters')
  317. end
  318. # TODO: Check how we can test sending to customer numbers.
  319. end
  320. context 'when article was created as a telegram message' do
  321. let(:article) do
  322. create(
  323. :ticket_article,
  324. ticket: ticket,
  325. sender: Ticket::Article::Sender.lookup(name: 'Customer'),
  326. type: Ticket::Article::Type.lookup(name: 'telegram personal-message'),
  327. )
  328. end
  329. include_examples 'mobile app: reply article', 'Telegram', attachments: true
  330. end
  331. context 'when article was created as a twitter status' do
  332. let(:article) do
  333. create(
  334. :twitter_article,
  335. ticket: ticket,
  336. sender: Ticket::Article::Sender.lookup(name: 'Customer'),
  337. )
  338. end
  339. include_examples 'mobile app: reply article', 'Twitter', attachments: false do
  340. let(:current_text) { article.from.to_s }
  341. let(:new_text) { '' }
  342. let(:result_text) { "#{article.from} \n/#{agent.firstname.first}#{agent.lastname.first}" }
  343. end
  344. it 'cannot create large article' do
  345. open_article_reply_dialog
  346. find_editor('Text').type(Faker::Lorem.characters(number: 281))
  347. click_on('Save')
  348. expect(find_editor('Text')).to have_text('This field must contain between 1 and 280 characters')
  349. end
  350. end
  351. context 'when article was created as a twitter dm' do
  352. include_examples 'mobile app: reply article', 'Twitter', 'DM when sender is customer', attachments: false do
  353. let(:article) do
  354. create(
  355. :twitter_dm_article,
  356. ticket: ticket,
  357. sender: Ticket::Article::Sender.lookup(name: 'Customer'),
  358. )
  359. end
  360. let(:result_text) { "#{new_text}\n/#{agent.firstname.first}#{agent.lastname.first}" }
  361. let(:to) { [article.from] }
  362. end
  363. include_examples 'mobile app: reply article', 'Twitter', 'DM when sender is agent', attachments: false do
  364. let(:article) do
  365. create(
  366. :twitter_dm_article,
  367. ticket: ticket,
  368. sender: Ticket::Article::Sender.lookup(name: 'Agent'),
  369. )
  370. end
  371. let(:result_text) { "#{new_text}\n/#{agent.firstname.first}#{agent.lastname.first}" }
  372. let(:to) { [article.to] }
  373. end
  374. it 'cannot create large article, "to" is required' do
  375. open_article_reply_dialog
  376. # unselect preselected field
  377. find_select('To').select_options([article.from])
  378. click_on('Save')
  379. expect(find_select('To')).to have_text('This field is required')
  380. end
  381. end
  382. context 'when article was created as a facebook post' do
  383. let(:article) do
  384. create(
  385. :ticket_article,
  386. ticket: ticket,
  387. sender: Ticket::Article::Sender.lookup(name: 'Customer'),
  388. type: Ticket::Article::Type.lookup(name: 'facebook feed post'),
  389. )
  390. end
  391. include_examples 'mobile app: reply article', 'Facebook', attachments: false do
  392. let(:type_id) { Ticket::Article::Type.lookup(name: 'facebook feed comment').id }
  393. let(:in_reply_to) { nil }
  394. end
  395. end
  396. end