full_quote_header_spec.rb 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe 'Ticket > Update > Full Quote Header', current_user_id: -> { current_user.id }, time_zone: 'Europe/London', type: :system do
  4. let(:group) { Group.find_by(name: 'Users') }
  5. let(:ticket) { create(:ticket, group: group) }
  6. let(:ticket_article) { create(:ticket_article, ticket: ticket, from: 'Example Name <asdf1@example.com>') }
  7. let(:customer) { create(:customer) }
  8. let(:current_user) { customer }
  9. let(:selection) { '' }
  10. prepend_before do
  11. Setting.set 'ui_ticket_zoom_article_email_full_quote_header', full_quote_header_setting
  12. end
  13. before do
  14. visit "ticket/zoom/#{ticket_article.ticket.id}"
  15. end
  16. context 'when "ui_ticket_zoom_article_email_full_quote_header" is enabled' do
  17. let(:full_quote_header_setting) { true }
  18. it 'includes sender when forwarding' do
  19. within(:active_content) do
  20. click_forward
  21. within(:richtext) do
  22. expect(page).to contain_full_quote(ticket_article).formatted_for(:forward)
  23. end
  24. end
  25. end
  26. it 'includes sender when replying' do
  27. within(:active_content) do
  28. highlight_and_click_reply
  29. within(:richtext) do
  30. expect(page).to contain_full_quote(ticket_article).formatted_for(:reply)
  31. end
  32. end
  33. end
  34. it 'includes sender when article visibility toggled' do
  35. within(:active_content) do
  36. set_internal
  37. highlight_and_click_reply
  38. within(:richtext) do
  39. expect(page).to contain_full_quote(ticket_article).formatted_for(:reply)
  40. end
  41. end
  42. end
  43. context 'when customer is agent' do
  44. let(:customer) { create(:agent) }
  45. it 'includes sender without email when forwarding' do
  46. within(:active_content) do
  47. click_forward
  48. within(:richtext) do
  49. expect(page).to contain_full_quote(ticket_article).formatted_for(:forward).ensuring_privacy(true)
  50. end
  51. end
  52. end
  53. end
  54. # https://github.com/zammad/zammad/issues/3824
  55. context 'when TO contains multiple senders and one of them is a known Zammad user' do
  56. let(:customer) { create(:customer) }
  57. let(:to_1) { "#{customer.fullname} <#{customer.email}>" }
  58. let(:to_2) { 'Example Two <two@example.org>' }
  59. let(:ticket_article) { create(:ticket_article, ticket: ticket, to: [to_1, to_2].join(', ')) }
  60. it 'includes all TO email address' do
  61. within(:active_content) do
  62. click_forward
  63. within(:richtext) do
  64. expect(page).to have_text(to_1).and(have_text(to_2))
  65. end
  66. end
  67. end
  68. end
  69. context 'ticket is created by agent on behalf of customer' do
  70. let(:agent) { create(:agent) }
  71. let(:current_user) { agent }
  72. let(:ticket) { create(:ticket, group: group, title: 'Created by agent on behalf of a customer', customer: customer) }
  73. let(:ticket_article) { create(:ticket_article, ticket: ticket, from: 'Created by agent on behalf of a customer', origin_by_id: customer.id) }
  74. it 'includes sender without email when replying' do
  75. within(:active_content) do
  76. highlight_and_click_reply
  77. within(:richtext) do
  78. expect(page).to contain_full_quote(ticket_article).formatted_for(:reply)
  79. end
  80. end
  81. end
  82. end
  83. # https://github.com/zammad/zammad/issues/3855
  84. context 'when ticket article has no recipient' do
  85. shared_examples 'when recipient is set to' do |recipient:, recipient_human:|
  86. context "when recipient is set to #{recipient_human}" do
  87. let(:ticket_article) { create(:ticket_article, :inbound_web, ticket: ticket, to: recipient) }
  88. it 'allows to forward without original recipient present' do
  89. within(:active_content) do
  90. click_forward
  91. within(:richtext) do
  92. expect(page).to contain_full_quote(ticket_article).formatted_for(:forward)
  93. end
  94. end
  95. end
  96. end
  97. end
  98. include_examples 'when recipient is set to', recipient: '', recipient_human: 'empty string'
  99. include_examples 'when recipient is set to', recipient: nil, recipient_human: 'nil'
  100. end
  101. end
  102. context 'when "ui_ticket_zoom_article_email_full_quote_header" is disabled' do
  103. let(:full_quote_header_setting) { false }
  104. it 'does not include sender when forwarding' do
  105. within(:active_content) do
  106. click_forward
  107. within(:richtext) do
  108. expect(page).not_to contain_full_quote(ticket_article).formatted_for(:forward)
  109. end
  110. end
  111. end
  112. it 'does not include sender when replying' do
  113. within(:active_content) do
  114. highlight_and_click_reply
  115. within(:richtext) do
  116. expect(page).not_to contain_full_quote(ticket_article).formatted_for(:reply)
  117. end
  118. end
  119. end
  120. end
  121. context 'when text is selected on page while replying' do
  122. let(:full_quote_header_setting) { false }
  123. let(:before_article_content_selector) { '.ticketZoom-header' }
  124. let(:after_article_content_selector) { '.ticket-article-item .humanTimeFromNow' }
  125. let(:article_content_selector) { '.ticket-article-item .richtext-content' }
  126. it 'does not quote article when bits other than the article are selected' do
  127. within(:active_content) do
  128. highlight_text(before_article_content_selector, '')
  129. click_reply
  130. within(:richtext) do
  131. expect(page).to have_no_text("Test Ticket\nTicket##{ticket.number} - created just now")
  132. end
  133. end
  134. end
  135. it 'quotes article when bits inside the article are selected' do
  136. within(:active_content) do
  137. highlight_text(article_content_selector, '')
  138. click_reply
  139. within(:richtext) do
  140. expect(page).to have_text('some message 123')
  141. end
  142. end
  143. end
  144. it 'quotes only article when bits before the article are selected as well' do
  145. within(:active_content) do
  146. highlight_text(before_article_content_selector, article_content_selector)
  147. click_reply
  148. within(:richtext) do
  149. expect(page).to have_no_text("Test Ticket\nTicket##{ticket.number} - created just now\nsome message 123")
  150. expect(page).to have_text('some message 123')
  151. end
  152. end
  153. end
  154. it 'quotes only article when bits after the article are selected as well' do
  155. within(:active_content) do
  156. highlight_text(article_content_selector, after_article_content_selector)
  157. click_reply
  158. within(:richtext) do
  159. expect(page).to have_no_text("some message 123\njust now")
  160. expect(page).to have_text('some message 123')
  161. end
  162. end
  163. end
  164. it 'quotes only article when bits both before and after the article are selected as well' do
  165. within(:active_content) do
  166. highlight_text(before_article_content_selector, after_article_content_selector)
  167. click_reply
  168. within(:richtext) do
  169. expect(page).to have_no_text("Test Ticket\nTicket##{ticket.number} - created just now\nsome message 123\njust now")
  170. expect(page).to have_text('some message 123')
  171. end
  172. end
  173. end
  174. context 'when full quote header setting is enabled' do
  175. let(:full_quote_header_setting) { true }
  176. it 'can breakout with enter from quote block' do
  177. within(:active_content) do
  178. highlight_and_click_reply
  179. within(:richtext) do
  180. wait.until do
  181. first('blockquote br:nth-child(2)', visible: :all)
  182. end
  183. blockquote_empty_line = first('blockquote br:nth-child(2)', visible: :all)
  184. page.driver.browser.action.move_to_location(blockquote_empty_line.native.location.x, blockquote_empty_line.native.location.y).click.perform
  185. end
  186. # Special handling for firefox, because the cursor is at the wrong location after the move to with click.
  187. if Capybara.current_driver == :zammad_firefox
  188. find(:richtext).send_keys(:down)
  189. end
  190. find(:richtext).send_keys(:enter)
  191. within(:richtext) do
  192. expect(page).to have_css('blockquote', count: 2)
  193. end
  194. end
  195. end
  196. end
  197. end
  198. def click_forward
  199. click '.js-ArticleAction[data-type=emailForward]'
  200. end
  201. def set_internal
  202. click '.js-ArticleAction[data-type=internal]'
  203. end
  204. def click_reply
  205. click '.js-ArticleAction[data-type=emailReply]'
  206. end
  207. def highlight_text(start_selector, end_selector)
  208. find(start_selector)
  209. .execute_script(<<~JAVASCRIPT, end_selector)
  210. let [ end_selector ] = arguments
  211. let end_node = $(end_selector)[0]
  212. if(!end_node) {
  213. end_node = this.nextSibling
  214. }
  215. window.getSelection().removeAllRanges()
  216. var range = window.document.createRange()
  217. range.setStart(this, 0)
  218. range.setEnd(end_node, end_node.childNodes.length)
  219. window.getSelection().addRange(range)
  220. JAVASCRIPT
  221. end
  222. def highlight_and_click_reply
  223. find('.ticket-article-item .richtext-content')
  224. .execute_script <<~JAVASCRIPT
  225. window.getSelection().removeAllRanges()
  226. var range = window.document.createRange()
  227. range.setStart(this, 0)
  228. range.setEnd(this.nextSibling, 0)
  229. window.getSelection().addRange(range)
  230. JAVASCRIPT
  231. wait.until_constant do
  232. find('.ticket-article-item .richtext-content').evaluate_script('window.getSelection().toString().trim()')
  233. end
  234. click_reply
  235. within(:richtext) do
  236. find('blockquote', visible: :all)
  237. end
  238. end
  239. define :contain_full_quote do
  240. match do
  241. confirm_content && confirm_style
  242. end
  243. match_when_negated do
  244. confirm_no_content
  245. end
  246. # sets expected quote format
  247. # @param [Symbol] :forward or :reply, defaults to :reply if not set
  248. chain :formatted_for do |style|
  249. @style = style
  250. end
  251. failure_message do
  252. if !confirm_style
  253. string = "expected\n```\n#{citation_text}\n```\nto match "
  254. case style
  255. when :reply
  256. string += '`On...wrote:`'
  257. when :forward
  258. string += '`Subject \n Date`'
  259. end
  260. return string
  261. end
  262. string = "expected\n```\n#{citation_text}\n```\nto "
  263. case style
  264. when :reply
  265. string += "have name `#{name}` and timestamp `#{timestamp_reply}`"
  266. string += ' but has no name' if citation_text.exclude?(name)
  267. string += ' but has no timestamp' if !includes_timestamp?(citation_text, timestamp_reply)
  268. when :forward
  269. string += "have name `#{name}` and timestamp `#{timestamp_forward}`"
  270. string += ensure_privacy? ? ' with no email' : " with email `#{email}`"
  271. string += ' but has no name' if citation_text.exclude?(name)
  272. string += ' but has no timestamp' if !includes_timestamp?(citation_text, timestamp_forward)
  273. string += ' but has no email' if !ensure_privacy? && citation_text.include?(email)
  274. end
  275. string
  276. end
  277. failure_message_when_negated do
  278. "expected\n```\n#{citation_text}\n```\nto not contain name/email/timestamp"
  279. end
  280. def style
  281. @style || :reply # rubocop:disable RSpec/InstanceVariable
  282. end
  283. # sets expected privacy level
  284. # @param [Boolean] defaults to false if not set
  285. chain :ensuring_privacy do |flag|
  286. @ensuring_privacy = flag
  287. end
  288. def ensure_privacy?
  289. @ensuring_privacy || false # rubocop:disable RSpec/InstanceVariable
  290. end
  291. def confirm_content
  292. case style
  293. when :reply
  294. confirm_content_reply
  295. when :forward
  296. confirm_content_forward
  297. end
  298. end
  299. def confirm_content_reply
  300. citation_text.include?(name) &&
  301. citation_text.exclude?(email) &&
  302. includes_timestamp?(citation_text, timestamp_reply)
  303. end
  304. def confirm_content_forward
  305. citation_text.include?(name) &&
  306. includes_timestamp?(citation_text, timestamp_forward) &&
  307. (ensure_privacy? ? citation_text.exclude?(email) : citation_text.include?(email))
  308. end
  309. def confirm_no_content
  310. citation_text.exclude?(name) &&
  311. citation_text.exclude?(email) &&
  312. citation_text.exclude?(timestamp_reply) &&
  313. citation_text.exclude?(timestamp_forward)
  314. end
  315. def confirm_style
  316. case style
  317. when :forward
  318. citation_text.match?(%r{Subject(.+)\nDate(.+)})
  319. when :reply
  320. citation_text.match?(%r{^On(.+)wrote:$})
  321. end
  322. end
  323. def citation
  324. actual.first('blockquote[type=cite]')
  325. end
  326. delegate :text, to: :citation, prefix: true
  327. def name
  328. (expected.origin_by || expected.created_by).fullname
  329. end
  330. def email
  331. expected.created_by.email
  332. end
  333. def timestamp_reply
  334. expected
  335. .created_at
  336. .in_time_zone('Europe/London')
  337. .strftime('%A, %B %1d, %Y at %1I:%M:%S %p')
  338. end
  339. def timestamp_forward
  340. expected
  341. .created_at
  342. .in_time_zone('Europe/London')
  343. .strftime('%m/%d/%Y %1I:%M %P')
  344. end
  345. # Chrome started to use non-breaking-space before AM/PM
  346. # While Firefox still has regular space
  347. # Using regexp with :space: to match either
  348. def includes_timestamp?(string, timestamp)
  349. regexp = Regexp.new timestamp.gsub(' ', '[[:space:]]{1}')
  350. string.match? regexp
  351. end
  352. end
  353. end