123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- require 'rails_helper'
- RSpec.describe 'Ticket > Update > Full Quote Header', current_user_id: -> { current_user.id }, time_zone: 'Europe/London', type: :system do
- let(:group) { Group.find_by(name: 'Users') }
- let(:ticket) { create(:ticket, group: group) }
- let(:ticket_article) { create(:ticket_article, ticket: ticket, from: 'Example Name <asdf1@example.com>') }
- let(:customer) { create(:customer) }
- let(:current_user) { customer }
- let(:selection) { '' }
- prepend_before do
- Setting.set 'ui_ticket_zoom_article_email_full_quote_header', full_quote_header_setting
- end
- before do
- visit "ticket/zoom/#{ticket_article.ticket.id}"
- end
- context 'when "ui_ticket_zoom_article_email_full_quote_header" is enabled' do
- let(:full_quote_header_setting) { true }
- it 'includes sender when forwarding' do
- within(:active_content) do
- click_forward
- within(:richtext) do
- expect(page).to contain_full_quote(ticket_article).formatted_for(:forward)
- end
- end
- end
- it 'includes sender when replying' do
- within(:active_content) do
- highlight_and_click_reply
- within(:richtext) do
- expect(page).to contain_full_quote(ticket_article).formatted_for(:reply)
- end
- end
- end
- it 'includes sender when article visibility toggled' do
- within(:active_content) do
- set_internal
- highlight_and_click_reply
- within(:richtext) do
- expect(page).to contain_full_quote(ticket_article).formatted_for(:reply)
- end
- end
- end
- context 'when customer is agent' do
- let(:customer) { create(:agent) }
- it 'includes sender without email when forwarding' do
- within(:active_content) do
- click_forward
- within(:richtext) do
- expect(page).to contain_full_quote(ticket_article).formatted_for(:forward).ensuring_privacy(true)
- end
- end
- end
- end
- # https://github.com/zammad/zammad/issues/3824
- context 'when TO contains multiple senders and one of them is a known Zammad user' do
- let(:customer) { create(:customer) }
- let(:to_1) { "#{customer.fullname} <#{customer.email}>" }
- let(:to_2) { 'Example Two <two@example.org>' }
- let(:ticket_article) { create(:ticket_article, ticket: ticket, to: [to_1, to_2].join(', ')) }
- it 'includes all TO email address' do
- within(:active_content) do
- click_forward
- within(:richtext) do
- expect(page).to have_text(to_1).and(have_text(to_2))
- end
- end
- end
- end
- context 'ticket is created by agent on behalf of customer' do
- let(:agent) { create(:agent) }
- let(:current_user) { agent }
- let(:ticket) { create(:ticket, group: group, title: 'Created by agent on behalf of a customer', customer: customer) }
- let(:ticket_article) { create(:ticket_article, ticket: ticket, from: 'Created by agent on behalf of a customer', origin_by_id: customer.id) }
- it 'includes sender without email when replying' do
- within(:active_content) do
- highlight_and_click_reply
- within(:richtext) do
- expect(page).to contain_full_quote(ticket_article).formatted_for(:reply)
- end
- end
- end
- end
- # https://github.com/zammad/zammad/issues/3855
- context 'when ticket article has no recipient' do
- shared_examples 'when recipient is set to' do |recipient:, recipient_human:|
- context "when recipient is set to #{recipient_human}" do
- let(:ticket_article) { create(:ticket_article, :inbound_web, ticket: ticket, to: recipient) }
- it 'allows to forward without original recipient present' do
- within(:active_content) do
- click_forward
- within(:richtext) do
- expect(page).to contain_full_quote(ticket_article).formatted_for(:forward)
- end
- end
- end
- end
- end
- include_examples 'when recipient is set to', recipient: '', recipient_human: 'empty string'
- include_examples 'when recipient is set to', recipient: nil, recipient_human: 'nil'
- end
- end
- context 'when "ui_ticket_zoom_article_email_full_quote_header" is disabled' do
- let(:full_quote_header_setting) { false }
- it 'does not include sender when forwarding' do
- within(:active_content) do
- click_forward
- within(:richtext) do
- expect(page).not_to contain_full_quote(ticket_article).formatted_for(:forward)
- end
- end
- end
- it 'does not include sender when replying' do
- within(:active_content) do
- highlight_and_click_reply
- within(:richtext) do
- expect(page).not_to contain_full_quote(ticket_article).formatted_for(:reply)
- end
- end
- end
- end
- context 'when text is selected on page while replying' do
- let(:full_quote_header_setting) { false }
- let(:before_article_content_selector) { '.ticketZoom-header' }
- let(:after_article_content_selector) { '.ticket-article-item .humanTimeFromNow' }
- let(:article_content_selector) { '.ticket-article-item .richtext-content' }
- it 'does not quote article when bits other than the article are selected' do
- within(:active_content) do
- highlight_text(before_article_content_selector, '')
- click_reply
- within(:richtext) do
- expect(page).to have_no_text("Test Ticket\nTicket##{ticket.number} - created just now")
- end
- end
- end
- it 'quotes article when bits inside the article are selected' do
- within(:active_content) do
- highlight_text(article_content_selector, '')
- click_reply
- within(:richtext) do
- expect(page).to have_text('some message 123')
- end
- end
- end
- it 'quotes only article when bits before the article are selected as well' do
- within(:active_content) do
- highlight_text(before_article_content_selector, article_content_selector)
- click_reply
- within(:richtext) do
- expect(page).to have_no_text("Test Ticket\nTicket##{ticket.number} - created just now\nsome message 123")
- expect(page).to have_text('some message 123')
- end
- end
- end
- it 'quotes only article when bits after the article are selected as well' do
- within(:active_content) do
- highlight_text(article_content_selector, after_article_content_selector)
- click_reply
- within(:richtext) do
- expect(page).to have_no_text("some message 123\njust now")
- expect(page).to have_text('some message 123')
- end
- end
- end
- it 'quotes only article when bits both before and after the article are selected as well' do
- within(:active_content) do
- highlight_text(before_article_content_selector, after_article_content_selector)
- click_reply
- within(:richtext) do
- expect(page).to have_no_text("Test Ticket\nTicket##{ticket.number} - created just now\nsome message 123\njust now")
- expect(page).to have_text('some message 123')
- end
- end
- end
- context 'when full quote header setting is enabled' do
- let(:full_quote_header_setting) { true }
- it 'can breakout with enter from quote block' do
- within(:active_content) do
- highlight_and_click_reply
- within(:richtext) do
- wait.until do
- first('blockquote br:nth-child(2)', visible: :all)
- end
- blockquote_empty_line = first('blockquote br:nth-child(2)', visible: :all)
- page.driver.browser.action.move_to_location(blockquote_empty_line.native.location.x, blockquote_empty_line.native.location.y).click.perform
- end
- # Special handling for firefox, because the cursor is at the wrong location after the move to with click.
- if Capybara.current_driver == :zammad_firefox
- find(:richtext).send_keys(:down)
- end
- find(:richtext).send_keys(:enter)
- within(:richtext) do
- expect(page).to have_css('blockquote', count: 2)
- end
- end
- end
- end
- end
- def click_forward
- click '.js-ArticleAction[data-type=emailForward]'
- end
- def set_internal
- click '.js-ArticleAction[data-type=internal]'
- end
- def click_reply
- click '.js-ArticleAction[data-type=emailReply]'
- end
- def highlight_text(start_selector, end_selector)
- find(start_selector)
- .execute_script(<<~JAVASCRIPT, end_selector)
- let [ end_selector ] = arguments
- let end_node = $(end_selector)[0]
- if(!end_node) {
- end_node = this.nextSibling
- }
- window.getSelection().removeAllRanges()
- var range = window.document.createRange()
- range.setStart(this, 0)
- range.setEnd(end_node, end_node.childNodes.length)
- window.getSelection().addRange(range)
- JAVASCRIPT
- end
- def highlight_and_click_reply
- find('.ticket-article-item .richtext-content')
- .execute_script <<~JAVASCRIPT
- window.getSelection().removeAllRanges()
- var range = window.document.createRange()
- range.setStart(this, 0)
- range.setEnd(this.nextSibling, 0)
- window.getSelection().addRange(range)
- JAVASCRIPT
- wait.until_constant do
- find('.ticket-article-item .richtext-content').evaluate_script('window.getSelection().toString().trim()')
- end
- click_reply
- within(:richtext) do
- find('blockquote', visible: :all)
- end
- end
- define :contain_full_quote do
- match do
- confirm_content && confirm_style
- end
- match_when_negated do
- confirm_no_content
- end
- # sets expected quote format
- # @param [Symbol] :forward or :reply, defaults to :reply if not set
- chain :formatted_for do |style|
- @style = style
- end
- failure_message do
- if !confirm_style
- string = "expected\n```\n#{citation_text}\n```\nto match "
- case style
- when :reply
- string += '`On...wrote:`'
- when :forward
- string += '`Subject \n Date`'
- end
- return string
- end
- string = "expected\n```\n#{citation_text}\n```\nto "
- case style
- when :reply
- string += "have name `#{name}` and timestamp `#{timestamp_reply}`"
- string += ' but has no name' if citation_text.exclude?(name)
- string += ' but has no timestamp' if !includes_timestamp?(citation_text, timestamp_reply)
- when :forward
- string += "have name `#{name}` and timestamp `#{timestamp_forward}`"
- string += ensure_privacy? ? ' with no email' : " with email `#{email}`"
- string += ' but has no name' if citation_text.exclude?(name)
- string += ' but has no timestamp' if !includes_timestamp?(citation_text, timestamp_forward)
- string += ' but has no email' if !ensure_privacy? && citation_text.include?(email)
- end
- string
- end
- failure_message_when_negated do
- "expected\n```\n#{citation_text}\n```\nto not contain name/email/timestamp"
- end
- def style
- @style || :reply # rubocop:disable RSpec/InstanceVariable
- end
- # sets expected privacy level
- # @param [Boolean] defaults to false if not set
- chain :ensuring_privacy do |flag|
- @ensuring_privacy = flag
- end
- def ensure_privacy?
- @ensuring_privacy || false # rubocop:disable RSpec/InstanceVariable
- end
- def confirm_content
- case style
- when :reply
- confirm_content_reply
- when :forward
- confirm_content_forward
- end
- end
- def confirm_content_reply
- citation_text.include?(name) &&
- citation_text.exclude?(email) &&
- includes_timestamp?(citation_text, timestamp_reply)
- end
- def confirm_content_forward
- citation_text.include?(name) &&
- includes_timestamp?(citation_text, timestamp_forward) &&
- (ensure_privacy? ? citation_text.exclude?(email) : citation_text.include?(email))
- end
- def confirm_no_content
- citation_text.exclude?(name) &&
- citation_text.exclude?(email) &&
- citation_text.exclude?(timestamp_reply) &&
- citation_text.exclude?(timestamp_forward)
- end
- def confirm_style
- case style
- when :forward
- citation_text.match?(%r{Subject(.+)\nDate(.+)})
- when :reply
- citation_text.match?(%r{^On(.+)wrote:$})
- end
- end
- def citation
- actual.first('blockquote[type=cite]')
- end
- delegate :text, to: :citation, prefix: true
- def name
- (expected.origin_by || expected.created_by).fullname
- end
- def email
- expected.created_by.email
- end
- def timestamp_reply
- expected
- .created_at
- .in_time_zone('Europe/London')
- .strftime('%A, %B %1d, %Y at %1I:%M:%S %p')
- end
- def timestamp_forward
- expected
- .created_at
- .in_time_zone('Europe/London')
- .strftime('%m/%d/%Y %1I:%M %P')
- end
- # Chrome started to use non-breaking-space before AM/PM
- # While Firefox still has regular space
- # Using regexp with :space: to match either
- def includes_timestamp?(string, timestamp)
- regexp = Regexp.new timestamp.gsub(' ', '[[:space:]]{1}')
- string.match? regexp
- end
- end
- end
|