# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
require 'rails_helper'
require 'system/examples/core_workflow_examples'
RSpec.describe 'Ticket zoom', type: :system do
context 'when ticket has an attachment' do
let(:group) { Group.find_by(name: 'Users') }
let(:ticket) { create(:ticket, group: group) }
let(:article) { create(:ticket_article, ticket: ticket) }
let(:attachment_name) { 'some_file.txt' }
before do
create(:store,
object: 'Ticket::Article',
o_id: article.id,
data: 'some content',
filename: attachment_name,
preferences: {
'Content-Type' => 'text/plain',
})
end
context 'article was already forwarded once' do
before do
visit "#ticket/zoom/#{ticket.id}"
within(:active_content) do
find('a[data-type=emailForward]').click
click('.js-reset')
have_no_css('.js-reset')
end
end
it 'adds attachments when forwarding multiple times' do
within(:active_content) do
find('a[data-type=emailForward]').click
end
within('.js-writeArea') do
expect(page).to have_text attachment_name
end
end
end
end
context 'when using the sidebar' do
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users'), customer: create(:customer, :with_org)) }
before do
Setting.set("#{service_name}_integration", true) if defined? service_name
visit "#ticket/zoom/#{ticket.id}"
end
it 'does show the edit link for the customer' do
click '.tabsSidebar-tab[data-tab=customer]'
click '#userAction'
click_on 'Edit Customer'
modal_ready
end
it 'does show the edit link for the organization' do
click '.tabsSidebar-tab[data-tab=organization]'
click '#userAction'
click_on 'Edit Organization'
modal_ready
end
%w[idoit gitlab github].each do |service_name|
it "#{service_name} tab is hidden" do
expect(page).to have_no_css(".tabsSidebar-tab[data-tab=#{service_name}]")
end
context "when #{service_name} is enabled" do
let(:service_name) { service_name }
context 'when agent' do
it "#{service_name} tab is visible" do
expect(page).to have_css(".tabsSidebar-tab[data-tab=#{service_name}]")
end
end
context 'when customer', authenticated_as: :customer do
let(:customer) { create(:customer) }
it "#{service_name} tab is hidden" do
expect(page).to have_no_css(".tabsSidebar-tab[data-tab=#{service_name}]")
end
end
end
end
end
context 'when ticket has a calendar attachment' do
let(:group) { Group.find_by(name: 'Users') }
let(:store_file_content_name) do
Rails.root.join('spec/fixtures/files/calendar/basic.ics').read
end
let(:store_file_name) { 'basic.ics' }
let(:expected_event) do
{
'title' => 'Test Summary',
'location' => 'https://us.zoom.us/j/example?pwd=test',
'attendees' => ['M.bob@example.com', 'J.doe@example.com'],
'organizer' => 'f.sample@example.com',
'description' => 'Test description'
}
end
let(:ticket) { create(:ticket, group: group) }
let(:article) { create(:ticket_article, ticket: ticket) }
before do
create(:store,
object: 'Ticket::Article',
o_id: article.id,
data: store_file_content_name,
filename: store_file_name,
preferences: {
'Content-Type' => 'text/calendar',
})
visit "#ticket/zoom/#{ticket.id}"
end
it 'has an attached calendar file' do
within :active_ticket_article, article do
within '.attachment.file-calendar' do
expect(page).to have_text(store_file_name)
end
end
end
it 'shows a preview button for the calendar file' do
within :active_ticket_article, article do
within '.attachment.file-calendar' do
expect(page).to have_button('Preview')
end
end
end
context 'when calendar preview button is clicked' do
before do
within :active_ticket_article, article do
within '.attachment.file-calendar' do
click_on 'Preview'
end
end
end
it 'shows calender data in the model' do
in_modal do
expect(page).to have_text expected_event['title']
expect(page).to have_text expected_event['location']
expected_event['attendees'].each { |attendee| expect(page).to have_text attendee }
expect(page).to have_text expected_event['organizer']
expect(page).to have_text expected_event['description']
end
click '.js-cancel'
end
end
end
context 'replying' do
context 'Group without signature' do
let(:ticket) { create(:ticket) }
let(:current_user) { create(:agent, password: 'test', groups: [ticket.group]) }
before do
# initial article to reply to
create(:ticket_article, ticket: ticket)
end
it 'ensures that text input opens on multiple replies', authenticated_as: :current_user do
visit "ticket/zoom/#{ticket.id}"
2.times do |article_offset|
articles_existing = 1
articles_expected = articles_existing + (article_offset + 1)
all('a[data-type=emailReply]').last.click
# wait till input box expands completely
find('.attachmentPlaceholder-label').in_fixed_position
expect(page).to have_no_css('.attachmentPlaceholder-hint')
find('.articleNewEdit-body').send_keys('Some reply')
click '.js-submit'
expect(page).to have_css('.ticket-article-item', count: articles_expected)
end
end
end
context 'Group with signature', authenticated_as: :user do
let(:signature_body) { 'Sample signature here' }
let(:signature) { create(:signature, body: signature_body) }
let(:group) { create(:group, signature: signature) }
let(:ticket) { create(:ticket, group: group) }
let(:user) { create(:agent, groups: [group]) }
before do
visit "ticket/zoom/#{ticket.id}"
click '.attachmentPlaceholder'
end
it 'removes signature when switching from email reply to phone' do
click '.js-selectableTypes'
click '.js-articleTypeItem[data-value=email]'
within :richtext do
expect(page).to have_text(signature_body)
end
click '.js-selectableTypes'
click '.js-articleTypeItem[data-value=phone]'
within :richtext do
expect(page).to have_no_text(signature_body)
end
end
it 'adds signature when switching from phone to email reply' do
within :richtext do
expect(page).to have_no_text(signature_body)
end
click '.js-selectableTypes'
click '.js-articleTypeItem[data-value=email]'
within :richtext do
expect(page).to have_text(signature_body)
end
end
end
context 'to inbound phone call', authenticated_as: -> { agent }, current_user_id: -> { agent.id } do
let(:agent) { create(:agent, groups: [Group.first]) }
let(:customer) { create(:customer) }
let(:ticket) { create(:ticket, customer: customer, group: agent.groups.first) }
let!(:article) { create(:ticket_article, :inbound_phone, ticket: ticket) }
before do
create(:customer, active: false)
end
it 'goes to customer email' do
visit "ticket/zoom/#{ticket.id}"
within :active_ticket_article, article do
click '.js-ArticleAction[data-type=emailReply]'
end
within :active_content do
within '.article-new' do
expect(find('[name=to]', visible: :all).value).to eq customer.email
end
end
end
it 'check active and inactive user in TO-field' do
visit "ticket/zoom/#{ticket.id}"
within :active_ticket_article, article do
click '.js-ArticleAction[data-type=emailReply]'
end
within :active_content do
within '.article-new' do
find('[name=to] ~ .ui-autocomplete-input').fill_in with: '**'
end
end
expect(page).to have_css('ul.ui-autocomplete > li.ui-menu-item', minimum: 2)
expect(page).to have_css('ul.ui-autocomplete > li.ui-menu-item.is-inactive', count: 1)
end
end
context 'to outbound phone call', authenticated_as: -> { agent }, current_user_id: -> { agent.id } do
let(:agent) { create(:agent, groups: [Group.first]) }
let(:customer) { create(:customer) }
let(:ticket) { create(:ticket, customer: customer, group: agent.groups.first) }
let!(:article) { create(:ticket_article, :outbound_phone, ticket: ticket) }
it 'goes to customer email' do
visit "ticket/zoom/#{ticket.id}"
within :active_ticket_article, article do
click '.js-ArticleAction[data-type=emailReply]'
end
within :active_content do
within '.article-new' do
expect(find('[name=to]', visible: :all).value).to eq customer.email
end
end
end
end
context 'scrollPageHeader disappears when answering via email #3736' do
let(:ticket) do
ticket = create(:ticket, group: Group.first)
create_list(:ticket_article, 15, ticket: ticket)
ticket
end
before do
visit "ticket/zoom/#{ticket.id}"
end
it 'does reset the scrollPageHeader on rerender of the ticket' do
select User.find_by(email: 'admin@example.com').fullname, from: 'Owner'
find('.js-textarea').send_keys('test 1234')
find('.js-submit').click
expect(page).to have_css('div.scrollPageHeader .js-ticketTitleContainer')
end
end
end
describe 'delete article', authenticated_as: :authenticate do
let(:group) { Group.first }
let(:admin) { create(:admin, groups: [group]) }
let(:agent) { create(:agent, groups: [group]) }
let(:other_agent) { create(:agent, groups: [group]) }
let(:customer) { create(:customer) }
let(:article) { send(item) }
def authenticate
Setting.set('ui_ticket_zoom_article_delete_timeframe', setting_delete_timeframe) if defined?(setting_delete_timeframe)
article
user
end
def article_communication
create_ticket_article(sender_name: 'Agent', internal: false, type_name: 'email', updated_by: customer)
end
def article_note_self
create_ticket_article(sender_name: 'Agent', internal: true, type_name: 'note', updated_by: user)
end
def article_note_other
create_ticket_article(sender_name: 'Agent', internal: true, type_name: 'note', updated_by: other_agent)
end
def article_note_customer
create_ticket_article(sender_name: 'Customer', internal: false, type_name: 'note', updated_by: customer)
end
def article_note_communication_self
create(:ticket_article_type, name: 'note_communication', communication: true)
create_ticket_article(sender_name: 'Agent', internal: true, type_name: 'note_communication', updated_by: user)
end
def article_note_communication_other
create(:ticket_article_type, name: 'note_communication', communication: true)
create_ticket_article(sender_name: 'Agent', internal: true, type_name: 'note_communication', updated_by: other_agent)
end
def create_ticket_article(sender_name:, internal:, type_name:, updated_by:)
UserInfo.current_user_id = updated_by.id
ticket = create(:ticket, group: group, customer: customer)
create(:ticket_article,
sender_name: sender_name, internal: internal, type_name: type_name, ticket: ticket,
body: "to be deleted #{offset} #{item}",
created_at: offset.ago, updated_at: offset.ago)
end
context 'going through full stack' do
context 'as admin' do
let(:user) { admin }
let(:item) { 'article_note_self' }
let(:offset) { 0.minutes }
it 'succeeds' do
ensure_websocket do
visit "ticket/zoom/#{article.ticket.id}"
end
within :active_ticket_article, article do
click '.js-ArticleAction[data-type=delete]'
end
in_modal do
click '.js-submit'
end
wait.until_disappears { find :active_ticket_article, article, wait: false }
end
end
end
context 'verifying permissions matrix' do
shared_examples 'according to permission matrix' do |item:, expects_visible:, offset:, description:|
context "looking at #{description} #{item}" do
let(:item) { item }
let(:offset) { offset }
let(:matcher) { expects_visible ? :have_css : :have_no_css }
it expects_visible ? 'delete button is visible' : 'delete button is not visible' do
visit "ticket/zoom/#{article.ticket.id}"
find("#article-#{article.id}")
within :active_ticket_article, article do
expect(page).to send(matcher, '.js-ArticleAction[data-type=delete]', wait: 0)
end
end
end
end
shared_examples 'deleting ticket article' do |item:, now:, later:, much_later:|
include_examples 'according to permission matrix', item: item, expects_visible: now, offset: 0.minutes, description: 'just created'
include_examples 'according to permission matrix', item: item, expects_visible: later, offset: 6.minutes, description: 'few minutes old'
include_examples 'according to permission matrix', item: item, expects_visible: much_later, offset: 11.minutes, description: 'very old'
end
context 'as admin' do
let(:user) { admin }
include_examples 'deleting ticket article',
item: 'article_communication',
now: false, later: false, much_later: false
include_examples 'deleting ticket article',
item: 'article_note_self',
now: true, later: true, much_later: false
include_examples 'deleting ticket article',
item: 'article_note_other',
now: false, later: false, much_later: false
include_examples 'deleting ticket article',
item: 'article_note_customer',
now: false, later: false, much_later: false
include_examples 'deleting ticket article',
item: 'article_note_communication_self',
now: true, later: true, much_later: false
include_examples 'deleting ticket article',
item: 'article_note_communication_other',
now: false, later: false, much_later: false
end
context 'as agent' do
let(:user) { agent }
include_examples 'deleting ticket article',
item: 'article_communication',
now: false, later: false, much_later: false
include_examples 'deleting ticket article',
item: 'article_note_self',
now: true, later: true, much_later: false
include_examples 'deleting ticket article',
item: 'article_note_other',
now: false, later: false, much_later: false
include_examples 'deleting ticket article',
item: 'article_note_customer',
now: false, later: false, much_later: false
include_examples 'deleting ticket article',
item: 'article_note_communication_self',
now: true, later: true, much_later: false
include_examples 'deleting ticket article',
item: 'article_note_communication_other',
now: false, later: false, much_later: false
end
context 'as customer' do
let(:user) { customer }
include_examples 'deleting ticket article',
item: 'article_communication',
now: false, later: false, much_later: false
include_examples 'deleting ticket article',
item: 'article_note_customer',
now: false, later: false, much_later: false
end
context 'with custom offset' do
let(:setting_delete_timeframe) { 6_000 }
context 'as admin' do
let(:user) { admin }
include_examples 'according to permission matrix', item: 'article_note_self', expects_visible: true, offset: 5000.seconds, description: 'outside of delete timeframe'
include_examples 'according to permission matrix', item: 'article_note_self', expects_visible: false, offset: 8000.seconds, description: 'outside of delete timeframe'
end
context 'as agent' do
let(:user) { agent }
include_examples 'according to permission matrix', item: 'article_note_self', expects_visible: true, offset: 5000.seconds, description: 'outside of delete timeframe'
include_examples 'according to permission matrix', item: 'article_note_self', expects_visible: false, offset: 8000.seconds, description: 'outside of delete timeframe'
end
end
context 'with timeframe as 0' do
let(:setting_delete_timeframe) { 0 }
context 'as agent' do
let(:user) { agent }
include_examples 'according to permission matrix', item: 'article_note_self', expects_visible: true, offset: 99.days, description: 'long after'
end
end
end
context 'button is hidden on the go' do
let(:setting_delete_timeframe) { 10 }
let(:user) { agent }
let(:item) { 'article_note_self' }
let!(:article) { send(item) }
let(:offset) { 0.seconds }
it 'successfully' do
visit "ticket/zoom/#{article.ticket.id}"
within :active_ticket_article, article do
find '.js-ArticleAction[data-type=delete]' # make sure delete button did show up
expect(page).to have_no_css('.js-ArticleAction[data-type=delete]')
end
end
end
end
describe 'forwarding article with an image' do
let(:ticket_article_body) do
filename = 'squares.png'
file = Rails.root.join("spec/fixtures/files/image/#{filename}").binread
ext = File.extname(filename)[1...]
base64 = Base64.encode64(file).delete("\n")
"
"
end
def current_ticket
Ticket.find current_url.split('/').last
end
def create_ticket
visit '#ticket/create'
within :active_content do
find('[data-type=email-out]').click
find('[name=title]').fill_in with: 'Title'
find('[name=customer_id_completion]').fill_in with: 'customer@example.com'
set_tree_select_value('group_id', Group.first.name)
find(:richtext).execute_script "this.innerHTML = \"#{ticket_article_body}\""
find('.js-submit').click
end
end
def forward
within :active_content do
find('.textBubble-content .richtext-content')
click '.js-ArticleAction[data-type=emailForward]'
fill_in 'To', with: 'customer@example.com'
find('.js-submit').click
end
end
def images_identical?(image_a, image_b)
return false if image_a.height != image_b.height
return false if image_a.width != image_b.width
image_a.height.times do |y|
image_a.row(y).each_with_index do |pixel, x|
return false if pixel != image_b[x, y]
end
end
true
end
it 'keeps image intact' do
create_ticket
forward
images = current_ticket.articles.map do |article|
ChunkyPNG::Image.from_string article.attachments.first.content
end
expect(images_identical?(images.first, images.second)).to be(true)
end
end
# https://github.com/zammad/zammad/issues/3335
context 'ticket state sort order maintained when locale is de-de', authenticated_as: :user do
context 'when existing ticket is open' do
let(:user) { create(:customer, preferences: { locale: 'de-de' }) }
let(:ticket) { create(:ticket, customer: user) }
it 'shows ticket state dropdown options in sorted translated alphabetically order' do
visit "ticket/zoom/#{ticket.id}"
within :active_content, '.tabsSidebar' do
expect(all('select[name=state_id] option').map(&:text)).to eq(%w[geschlossen neu offen])
end
end
end
context 'when a new ticket is created' do
let(:user) { create(:agent, preferences: { locale: 'de-de' }, groups: [permitted_group]) }
let(:permitted_group) { create(:group) }
it 'shows ticket state dropdown options in sorted order' do
visit 'ticket/create'
expect(all('select[name=state_id] option').map(&:text)).to eq ['-', 'geschlossen', 'neu', 'offen', 'warten auf Erinnerung', 'warten auf Schließen']
end
end
end
context 'object manager attribute permission view' do
let!(:group_users) { Group.find_by(name: 'Users') }
shared_examples 'shows attributes and values for agent view and editable' do
it 'shows attributes and values for agent view and editable', authenticated_as: :current_user do
visit "ticket/zoom/#{ticket.id}"
refresh # refresh to have assets generated for ticket
expect(page).to have_select('state_id', options: ['new', 'open', 'pending reminder', 'pending close', 'closed'])
expect(page).to have_select('priority_id')
expect(page).to have_select('owner_id')
expect(page).to have_css('div.tabsSidebar-tab[data-tab=customer]')
end
end
shared_examples 'shows attributes and values for agent view but disabled' do
it 'shows attributes and values for agent view but disabled', authenticated_as: :current_user do
visit "ticket/zoom/#{ticket.id}"
refresh # refresh to have assets generated for ticket
expect(page).to have_select('state_id', disabled: true)
expect(page).to have_select('priority_id', disabled: true)
expect(page).to have_select('owner_id', disabled: true)
expect(page).to have_css('div.tabsSidebar-tab[data-tab=customer]')
end
end
shared_examples 'shows attributes and values for customer view' do
it 'shows attributes and values for customer view', authenticated_as: :current_user do
visit "ticket/zoom/#{ticket.id}"
refresh # refresh to have assets generated for ticket
expect(page).to have_select('state_id', options: %w[new open closed])
expect(page).to have_no_select('priority_id')
expect(page).to have_no_select('owner_id')
expect(page).to have_no_css('div.tabsSidebar-tab[data-tab=customer]')
end
end
context 'as customer' do
let!(:current_user) { create(:customer) }
let(:ticket) { create(:ticket, customer: current_user) }
include_examples 'shows attributes and values for customer view'
end
context 'as agent with full permissions' do
let(:current_user) { create(:agent, groups: [ group_users ]) }
let(:ticket) { create(:ticket, group: group_users) }
include_examples 'shows attributes and values for agent view and editable'
end
context 'as agent with change permissions' do
let!(:current_user) { create(:agent) }
let(:ticket) { create(:ticket, group: group_users) }
before do
current_user.group_names_access_map = {
group_users.name => %w[read change],
}
end
include_examples 'shows attributes and values for agent view and editable'
end
context 'as agent with read permissions' do
let!(:current_user) { create(:agent) }
let(:ticket) { create(:ticket, group: group_users) }
before do
current_user.group_names_access_map = {
group_users.name => 'read',
}
end
include_examples 'shows attributes and values for agent view but disabled'
end
context 'as agent+customer with full permissions' do
let!(:current_user) { create(:agent_and_customer, groups: [ group_users ]) }
context 'normal ticket' do
let(:ticket) { create(:ticket, group: group_users) }
include_examples 'shows attributes and values for agent view and editable'
end
context 'ticket where current_user is also customer' do
let(:ticket) { create(:ticket, customer: current_user, group: group_users) }
include_examples 'shows attributes and values for agent view and editable'
end
end
context 'as agent+customer with change permissions' do
let!(:current_user) { create(:agent_and_customer) }
before do
current_user.group_names_access_map = {
group_users.name => %w[read change],
}
end
context 'normal ticket' do
let(:ticket) { create(:ticket, group: group_users) }
include_examples 'shows attributes and values for agent view and editable'
end
context 'ticket where current_user is also customer' do
let(:ticket) { create(:ticket, customer: current_user, group: group_users) }
include_examples 'shows attributes and values for agent view and editable'
end
end
context 'as agent+customer with read permissions' do
let!(:current_user) { create(:agent_and_customer) }
before do
current_user.group_names_access_map = {
group_users.name => 'read',
}
end
context 'normal ticket' do
let(:ticket) { create(:ticket, group: group_users) }
include_examples 'shows attributes and values for agent view but disabled'
end
context 'ticket where current_user is also customer' do
let(:ticket) { create(:ticket, customer: current_user, group: group_users) }
include_examples 'shows attributes and values for agent view but disabled'
end
end
context 'as agent+customer but only customer for the ticket (no agent access)' do
let!(:current_user) { create(:agent_and_customer) }
let(:ticket) { create(:ticket, customer: current_user) }
include_examples 'shows attributes and values for customer view'
end
end
describe 'note visibility', authenticated_as: :customer do
context 'when logged in as a customer' do
let(:customer) { create(:customer) }
let(:ticket) { create(:ticket, customer: customer) }
let!(:ticket_article) { create(:ticket_article, ticket: ticket) }
let!(:ticket_note) { create(:ticket_article, ticket: ticket, internal: true, type_name: 'note') }
it 'previously created private note is not visible' do
visit "ticket/zoom/#{ticket_article.ticket.id}"
expect(page).to have_no_selector(:active_ticket_article, ticket_note)
end
it 'previously created private note shows up via WS push' do
visit "ticket/zoom/#{ticket_article.ticket.id}"
ensure_websocket
# make sure ticket is done loading and change will be pushed via WS
find(:active_ticket_article, ticket_article)
ticket_note.update!(internal: false)
expect(page).to have_selector(:active_ticket_article, ticket_note)
end
end
end
# https://github.com/zammad/zammad/issues/3012
describe 'article type selection' do
context 'when logged in as a customer', authenticated_as: :customer do
let(:customer) { create(:customer) }
let(:ticket) { create(:ticket, customer: customer) }
it 'hides button for single choice' do
visit "ticket/zoom/#{ticket.id}"
find('.articleNewEdit-body').send_keys('Some reply')
expect(page).to have_no_selector('.js-selectedArticleType')
end
end
context 'when logged in as an agent' do
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
it 'shows button for multiple choices' do
visit "ticket/zoom/#{ticket.id}"
find('.articleNewEdit-body').send_keys('Some reply')
expect(page).to have_css('.js-selectedArticleType')
end
end
end
# https://github.com/zammad/zammad/issues/3260
describe 'next in overview macro changes URL', authenticated_as: :authenticate do
let(:next_ticket) { create(:ticket, title: 'next Ticket', group: Group.first) }
let(:macro) { create(:macro, name: 'next macro', ux_flow_next_up: 'next_from_overview') }
def authenticate
next_ticket && macro
true
end
it 'to next Ticket ID' do
visit 'ticket/view/all_unassigned'
click_on 'Welcome to Zammad!'
click '.js-openDropdownMacro'
find(:macro, macro.id).click
wait(5, interval: 1).until_constant { current_url }
expect(current_url).to include("ticket/zoom/#{next_ticket.id}")
end
end
# https://github.com/zammad/zammad/issues/3279
describe 'previous/next clickability when at last or first ticket' do
let(:ticket_a) { create(:ticket, title: 'ticket a', group: Group.first) }
let(:ticket_b) { create(:ticket, title: 'ticket b', group: Group.first) }
before do
ticket_a && ticket_b
visit 'ticket/view/all_unassigned'
end
it 'previous is not clickable for the first item' do
open_nth_item(0)
expect(page).to have_css('.pagination .btn--split--first.is-disabled')
end
it 'next is clickable for the first item' do
open_nth_item(0)
expect { click '.pagination .btn--split--last' }.to change { page.find('.content.active')[:id] }
end
it 'previous is clickable for the middle item' do
open_nth_item(1)
expect { click '.pagination .btn--split--first' }.to change { page.find('.content.active')[:id] }
end
it 'next is clickable for the middle item' do
open_nth_item(1)
expect { click '.pagination .btn--split--last' }.to change { page.find('.content.active')[:id] }
end
it 'previous is clickable for the last item' do
open_nth_item(2)
expect { click '.pagination .btn--split--first' }.to change { page.find('.content.active')[:id] }
end
it 'next is not clickable for the last item' do
open_nth_item(2)
expect(page).to have_css('.pagination .btn--split--last.is-disabled')
end
def open_nth_item(nth)
within :active_content do
find_all('.table tr.item a[href^="#ticket/zoom"]')[nth].click
end
await_empty_ajax_queue
end
end
# https://github.com/zammad/zammad/issues/3267
describe 'previous/next buttons are added when open ticket is opened from overview' do
let(:ticket_a) { create(:ticket, title: 'ticket a', group: Group.first) }
let(:ticket_b) { create(:ticket, title: 'ticket b', group: Group.first) }
# prepare an opened ticket and go to overview
before do
ticket_a && ticket_b
visit "ticket/zoom/#{ticket_a.id}"
visit 'ticket/view/all_unassigned'
end
it 'adds previous/next buttons to existing ticket' do
within :active_content do
click_on ticket_a.title
expect(page).to have_css('.pagination-counter')
end
end
it 'keeps previous/next buttons when navigating to overview ticket from elsewhere' do
within :active_content do
click_on ticket_a.title
visit 'dashboard'
visit "ticket/zoom/#{ticket_a.id}"
expect(page).to have_css('.pagination-counter')
end
end
end
# https://github.com/zammad/zammad/issues/2942
describe 'attachments are lost in specific conditions' do
let(:ticket) { create(:ticket, group: Group.first) }
it 'attachment is retained when forwarding a fresh article' do
ensure_websocket do
visit "ticket/zoom/#{ticket.id}"
end
# add an article, forcing reset of form_id
# click in the upper most upper left corner of the article create textbox
# (that works for both Firefox and Chrome)
# to avoid clicking on attachment upload
find('.js-writeArea').click(x: 5, y: 5)
# wait for propagateOpenTextarea to be completed
find('.attachmentPlaceholder-label').in_fixed_position
expect(page).to have_no_css('.attachmentPlaceholder-hint')
# write article content
find('.articleNewEdit-body').send_keys('Some reply')
click '.js-submit'
# wait for article to be added to the page
expect(page).to have_css('.ticket-article-item', count: 1)
# create a on-the-fly article with attachment that will get pushed to open browser
article1 = create(:ticket_article, ticket: ticket)
create(:store,
object: 'Ticket::Article',
o_id: article1.id,
data: 'some content',
filename: 'some_file.txt',
preferences: {
'Content-Type' => 'text/plain',
})
# wait for article to be added to the page
expect(page).to have_css('.ticket-article-item', count: 2)
# click on forward of created article
within :active_ticket_article, article1 do
find('a[data-type=emailForward]').click
end
# wait for propagateOpenTextarea to be completed
find('.attachmentPlaceholder-label').in_fixed_position
expect(page).to have_no_css('.attachmentPlaceholder-hint')
# fill forward information and create article
fill_in 'To', with: 'forward@example.org'
find('.articleNewEdit-body').send_keys('Forwarding with the attachment')
click '.js-submit'
# wait for article to be added to the page
expect(page).to have_css('.ticket-article-item', count: 3)
# check if attachment was forwarded successfully
within :active_ticket_article, ticket.reload.articles.last do
within '.attachments--list' do
expect(page).to have_text('some_file.txt')
end
end
end
end
describe 'mentions' do
context 'when logged in as agent' do
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
let!(:other_agent) { create(:agent, groups: [Group.find_by(name: 'Users')]) }
let!(:admin) { User.find_by(email: 'admin@example.com') }
it 'can subscribe and unsubscribe' do
ensure_websocket do
visit "ticket/zoom/#{ticket.id}"
click '.js-subscriptions .js-subscribe input'
expect(page).to have_css('.js-subscriptions .js-unsubscribe input')
expect(page).to have_css('.js-subscriptions span.avatar')
click '.js-subscriptions .js-unsubscribe input'
expect(page).to have_css('.js-subscriptions .js-subscribe input')
expect(page).to have_no_selector('.js-subscriptions span.avatar')
create(:mention, mentionable: ticket, user: other_agent)
expect(page).to have_css('.js-subscriptions span.avatar')
# check history for mention entries
click 'h2.sidebar-header-headline.js-headline'
click 'li[data-type=ticket-history] a'
expect(page).to have_text("created Mention → '#{admin.firstname} #{admin.lastname}'")
expect(page).to have_text("removed Mention → '#{admin.firstname} #{admin.lastname}'")
expect(page).to have_text("created Mention → '#{other_agent.firstname} #{other_agent.lastname}'")
end
end
end
end
# https://github.com/zammad/zammad/issues/2671
describe 'Pending time field in ticket sidebar', authenticated_as: :customer do
let(:customer) { create(:customer) }
let(:ticket) { create(:ticket, customer: customer, pending_time: 1.day.from_now, state: Ticket::State.lookup(name: 'pending reminder')) }
it 'not shown to customer' do
visit "ticket/zoom/#{ticket.id}"
within :active_content do
expect(page).to have_no_css('.controls[data-name=pending_time]')
end
end
end
describe 'Pending time field in ticket sidebar as agent' do
before do
ticket.update(pending_time: 1.day.from_now, state: Ticket::State.lookup(name: 'pending reminder'))
visit "ticket/zoom/#{ticket.id}"
end
let(:ticket) { Ticket.first }
# has to run asynchronously to keep both Firefox and Safari
# https://github.com/zammad/zammad/issues/3414
# https://github.com/zammad/zammad/issues/2887
context 'when clicking timepicker component' do
it 'in the first half, hours selected' do
within :active_content do
# timepicker messes with the dom, so don't cache the element and wait a bit.
sleep 1
find('.js-timepicker').click(x: -10, y: 20)
sleep 0.5
expect(find('.js-timepicker')).to have_selection(0..2)
end
end
it 'in the second half, minutes selected' do
within :active_content do
sleep 1
find('.js-timepicker').click(x: 10, y: 20)
sleep 0.5
expect(find('.js-timepicker')).to have_selection(3..5)
end
end
end
matcher :have_selection do
match { starts_at == expected.begin && ends_at == expected.end }
def starts_at
actual.evaluate_script 'this.selectionStart'
end
def ends_at
actual.evaluate_script 'this.selectionEnd'
end
end
end
describe 'Article ID URL / link' do
let(:ticket) { create(:ticket, group: Group.first) }
let!(:article) { create(:'ticket/article', ticket: ticket) }
it 'shows Article direct link' do
ensure_websocket do
visit "ticket/zoom/#{ticket.id}"
end
url = "#{Setting.get('http_type')}://#{Setting.get('fqdn')}/#ticket/zoom/#{ticket.id}/#{article.id}"
within :active_ticket_article, article do
expect(page).to have_css(%(a[href="#{url}"]))
end
end
context 'when multiple Articles are present' do
let(:article_count) { 20 }
let(:article_top) { ticket.articles.second }
let(:article_middle) { ticket.articles[ article_count / 2 ] }
let(:article_bottom) { ticket.articles.last }
before do
article_count.times do
create(:'ticket/article', ticket: ticket, body: SecureRandom.uuid)
end
visit "ticket/zoom/#{ticket.id}"
end
def wait_for_scroll
wait(5, interval: 0.2).until_constant do
find('.ticketZoom').native.location.y
end
end
def check_shown(top: false, middle: false, bottom: false)
wait_for_scroll
expect(page).to have_css("div#article-content-#{article_top.id} .richtext-content", obscured: !top)
.and(have_css("div#article-content-#{article_middle.id} .richtext-content", obscured: !middle, wait: 0))
.and(have_css("div#article-content-#{article_bottom.id} .richtext-content", obscured: !bottom, wait: 0))
end
it 'scrolls to top article ID' do
visit "ticket/zoom/#{ticket.id}/#{article_top.id}"
check_shown(top: true)
end
it 'scrolls to middle article ID' do
visit "ticket/zoom/#{ticket.id}/#{article_middle.id}"
check_shown(middle: true)
end
it 'scrolls to bottom article ID' do
visit "ticket/zoom/#{ticket.id}/#{article_top.id}"
wait_for_scroll
visit "ticket/zoom/#{ticket.id}/#{article_bottom.id}"
check_shown(bottom: true)
end
end
context 'when long articles are present' do
it 'shows the "See more" link if you switch between the ticket and the dashboard on new articles' do
ensure_websocket do
# prerender ticket
visit "ticket/zoom/#{ticket.id}"
# ticket tab becomes background
visit 'dashboard'
end
# create a new article
article_id = create(:'ticket/article', ticket: ticket, body: "#{SecureRandom.uuid} #{"lorem ipsum\n" * 200}")
wait(30).until { has_css?('div.tasks a.is-modified') }
visit "ticket/zoom/#{ticket.id}"
within :active_content do
expect(find("div#article-content-#{article_id.id}")).to have_text('See more')
end
end
end
end
describe 'Macros', authenticated_as: :authenticate do
let(:macro_body) { 'macro body' }
let(:macro) { create(:macro, perform: { 'article.note' => { 'body' => macro_body, 'internal' => 'true', 'subject' => 'macro note' } }) }
let!(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
def authenticate
macro
true
end
it 'does html macro by default' do
visit "ticket/zoom/#{ticket.id}"
find('.js-openDropdownMacro').click
find(:macro, macro.id).click
expect(ticket.reload.articles.last.body).to eq(macro_body)
expect(ticket.reload.articles.last.content_type).to eq('text/html')
end
end
describe 'object manager attributes maxlength', authenticated_as: :authenticate, db_strategy: :reset do
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
def authenticate
ticket
create(:object_manager_attribute_text, :required_screen, name: 'maxtest', display: 'maxtest', data_option: {
'type' => 'text',
'maxlength' => 3,
'null' => true,
'translate' => false,
'default' => '',
'options' => {},
'relation' => '',
})
ObjectManager::Attribute.migration_execute
true
end
it 'checks ticket zoom' do
visit "ticket/zoom/#{ticket.id}"
within(:active_content) do
fill_in 'maxtest', with: 'hellu'
expect(page.find_field('maxtest').value).to eq('hel')
end
end
end
describe 'Update of ticket links', authenticated_as: :authenticate do
let(:ticket1) { create(:ticket, group: Group.find_by(name: 'Users')) }
let(:ticket2) { create(:ticket, group: Group.find_by(name: 'Users')) }
def authenticate
ticket1
ticket2
create(:link, from: ticket1, to: ticket2)
true
end
it 'does update the state of the ticket links' do
visit "ticket/zoom/#{ticket1.id}"
# check title changes
expect(page).to have_text(ticket2.title)
ticket2.update(title: 'super new title')
expect(page).to have_text(ticket2.reload.title)
# check state changes
expect(page).to have_css('div.links .tasks svg.open')
ticket2.update(state: Ticket::State.find_by(name: 'closed'))
expect(page).to have_css('div.links .tasks svg.closed')
end
end
describe 'GitLab Integration', :integration, authenticated_as: :authenticate, required_envs: %w[GITLAB_ENDPOINT GITLAB_APITOKEN] do
let!(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
def authenticate
Setting.set('gitlab_integration', true)
Setting.set('gitlab_config', {
api_token: ENV['GITLAB_APITOKEN'],
endpoint: ENV['GITLAB_ENDPOINT'],
})
true
end
it 'creates links and removes them' do
visit "#ticket/zoom/#{ticket.id}"
within(:active_content) do
# switch to GitLab sidebar
click('.tabsSidebar-tab[data-tab=gitlab]')
click('.sidebar-header-headline.js-headline')
# add issue
click_on 'Link issue'
fill_in 'link', with: ENV['GITLAB_ISSUE_LINK']
click_on 'Submit'
# verify issue
content = find('.sidebar-git-issue-content')
expect(content).to have_text('#1 Example issue')
expect(content).to have_text('critical')
expect(content).to have_text('special')
expect(content).to have_text('important milestone')
expect(content).to have_text('zammad-robot')
expect(ticket.reload.preferences[:gitlab][:issue_links][0]).to eq(ENV['GITLAB_ISSUE_LINK'])
# check sidebar counter increased to 1
expect(find('.tabsSidebar-tab[data-tab=gitlab] .js-tabCounter')).to have_text('1')
# delete issue
click(".sidebar-git-issue-delete span[data-issue-id='#{ENV['GITLAB_ISSUE_LINK']}']")
content = find('.sidebar[data-tab=gitlab] .sidebar-content')
expect(content).to have_text('No linked issues')
expect(ticket.reload.preferences[:gitlab][:issue_links][0]).to be_nil
# check that counter got removed
expect(page).to have_no_selector('.tabsSidebar-tab[data-tab=gitlab] .js-tabCounter')
end
end
end
describe 'GitHub Integration', :integration, authenticated_as: :authenticate, required_envs: %w[GITHUB_ENDPOINT GITHUB_APITOKEN] do
let!(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
def authenticate
Setting.set('github_integration', true)
Setting.set('github_config', {
api_token: ENV['GITHUB_APITOKEN'],
endpoint: ENV['GITHUB_ENDPOINT'],
})
true
end
it 'creates links and removes them' do
visit "#ticket/zoom/#{ticket.id}"
within(:active_content) do
# switch to GitHub sidebar
click('.tabsSidebar-tab[data-tab=github]')
click('.sidebar-header-headline.js-headline')
# add issue
click_on 'Link issue'
fill_in 'link', with: ENV['GITHUB_ISSUE_LINK']
click_on 'Submit'
# verify issue
content = find('.sidebar-git-issue-content')
expect(content).to have_text('#1575 GitHub integration')
expect(content).to have_text('enhancement')
expect(content).to have_text('integration')
expect(content).to have_text('4.0')
expect(content).to have_text('Thorsten')
expect(ticket.reload.preferences[:github][:issue_links][0]).to eq(ENV['GITHUB_ISSUE_LINK'])
# check sidebar counter increased to 1
expect(find('.tabsSidebar-tab[data-tab=github] .js-tabCounter')).to have_text('1')
# delete issue
click(".sidebar-git-issue-delete span[data-issue-id='#{ENV['GITHUB_ISSUE_LINK']}']")
content = find('.sidebar[data-tab=github] .sidebar-content')
expect(content).to have_text('No linked issues')
expect(ticket.reload.preferences[:github][:issue_links][0]).to be_nil
# check that counter got removed
expect(page).to have_no_selector('.tabsSidebar-tab[data-tab=github] .js-tabCounter')
end
end
end
describe 'Core Workflow' do
include_examples 'core workflow' do
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
let(:object_name) { 'Ticket' }
let(:before_it) do
lambda {
ensure_websocket(check_if_pinged: false) do
visit "#ticket/zoom/#{ticket.id}"
end
}
end
end
end
context 'Sidebar - Open & Closed Tickets', performs_jobs: true, searchindex: true do
let(:customer) { create(:customer, :with_org) }
let(:ticket_open) { create(:ticket, group: Group.find_by(name: 'Users'), customer: customer, title: SecureRandom.uuid) }
let(:ticket_closed) { create(:ticket, group: Group.find_by(name: 'Users'), customer: customer, state: Ticket::State.find_by(name: 'closed'), title: SecureRandom.uuid) }
before do
ticket_open
ticket_closed
perform_enqueued_jobs
searchindex_model_reload([Ticket, User, Organization])
end
it 'does show open and closed tickets in advanced search url' do
visit "#ticket/zoom/#{ticket_open.id}"
click '.tabsSidebar-tab[data-tab=customer]'
click '.user-tickets[data-type=open]'
expect(page).to have_text(ticket_open.title)
visit "#ticket/zoom/#{ticket_open.id}"
click '.user-tickets[data-type=closed]'
expect(page).to have_text(ticket_closed.title)
end
end
context 'Sidebar - Organization' do
let(:organization) { create(:organization) }
context 'members section' do
let(:customers) { create_list(:customer, 50, organization: organization) }
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users'), customer: customers.first) }
let(:members) { organization.members.reorder(id: :asc) }
before do
visit "#ticket/zoom/#{ticket.id}"
click '.tabsSidebar-tab[data-tab=organization]'
end
it 'shows first 10 members and loads more on demand' do
expect(page).to have_text(members[9].fullname)
expect(page).to have_no_text(members[10].fullname)
click '.js-showMoreMembers'
expect(page).to have_text(members[10].fullname)
end
end
end
describe 'merging happened in the background', authenticated_as: :user do
before do
merged_into_trigger && received_merge_trigger && update_trigger
visit "ticket/zoom/#{ticket.id}"
visit "ticket/zoom/#{target_ticket.id}"
ensure_websocket do
visit 'dashboard'
end
end
let(:merged_into_trigger) { create(:trigger, :conditionable, condition_ticket_action: :merged_into) }
let(:received_merge_trigger) { create(:trigger, :conditionable, condition_ticket_action: :received_merge) }
let(:update_trigger) { create(:trigger, :conditionable, condition_ticket_action: :update) }
let(:ticket) { create(:ticket) }
let(:target_ticket) { create(:ticket) }
let(:user) { create(:agent, :preferencable, notification_group_ids: [ticket, target_ticket].map(&:group_id), groups: [ticket, target_ticket].map(&:group)) }
context 'when merging ticket' do
before do
ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
end
it 'pulses source ticket' do
expect(page).to have_css("#navigation a.is-modified[data-key=\"Ticket-#{ticket.id}\"]")
end
it 'pulses target ticket' do
expect(page).to have_css("#navigation a.is-modified[data-key=\"Ticket-#{target_ticket.id}\"]")
end
end
context 'when merging and looking at online notifications', :performs_jobs do
before do
perform_enqueued_jobs do
ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
end
find('.js-toggleNotifications').click
end
it 'shows online notification for source ticket' do
expect(page).to have_text("Ticket #{ticket.title} was merged into another ticket")
end
it 'shows online notification for target ticket' do
expect(page).to have_text("Another ticket was merged into ticket #{ticket.title}")
end
end
end
describe 'Tab behaviour - Define default "stay on tab" / "close tab" behavior #257', authenticated_as: :authenticate do
def authenticate
Setting.set('ticket_secondary_action', 'closeTabOnTicketClose')
true
end
let!(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
before do
visit "ticket/zoom/#{ticket.id}"
end
it 'does show the default of the system' do
expect(page).to have_text('Close tab on ticket close')
end
it 'does save state for the user preferences' do
click '.js-attributeBar .dropup div'
click 'span[data-type=stayOnTab]'
refresh
expect(page).to have_text('Stay on tab')
expect(User.find_by(email: 'admin@example.com').preferences[:secondaryAction]).to eq('stayOnTab')
end
it 'does show the correct tab state after update of the ticket (#4094)' do
select 'closed', from: 'State'
click '.js-attributeBar .dropup div'
click 'span[data-type=stayOnTab]'
click '.js-submit'
expect(page.find('.js-secondaryActionButtonLabel')).to have_text('Stay on tab')
end
context 'Tab behaviour - Close tab on ticket close' do
it 'does not close the tab without any action' do
click '.js-submit'
expect(current_url).to include('ticket/zoom')
end
it 'does close the tab on ticket close' do
select 'closed', from: 'State'
click '.js-submit'
expect(current_url).not_to include('ticket/zoom')
end
end
context 'Tab behaviour - Stay on tab' do
def authenticate
Setting.set('ticket_secondary_action', 'stayOnTab')
true
end
it 'does not close the tab without any action' do
click '.js-submit'
expect(current_url).to include('ticket/zoom')
end
it 'does not close the tab on ticket close' do
select 'closed', from: 'State'
click '.js-submit'
expect(current_url).to include('ticket/zoom')
end
end
context 'Tab behaviour - Close tab' do
def authenticate
Setting.set('ticket_secondary_action', 'closeTab')
true
end
it 'does close the tab without any action' do
click '.js-submit'
expect(current_url).not_to include('ticket/zoom')
end
it 'does close the tab on ticket close' do
select 'closed', from: 'State'
click '.js-submit'
expect(current_url).not_to include('ticket/zoom')
end
end
context 'Tab behaviour - Next in overview' do
let(:ticket1) { create(:ticket, title: SecureRandom.uuid, group: Group.find_by(name: 'Users')) }
let(:ticket2) { create(:ticket, title: SecureRandom.uuid, group: Group.find_by(name: 'Users')) }
let(:ticket3) { create(:ticket, title: SecureRandom.uuid, group: Group.find_by(name: 'Users')) }
def authenticate
Setting.set('ticket_secondary_action', 'closeNextInOverview')
ticket1
ticket2
ticket3
true
end
before do
visit 'ticket/view/all_open'
end
it 'does change the tab without any action' do
click_on ticket1.title
expect(current_url).to include("ticket/zoom/#{ticket1.id}")
click '.js-submit'
expect(current_url).to include("ticket/zoom/#{ticket2.id}")
click '.js-submit'
expect(current_url).to include("ticket/zoom/#{ticket3.id}")
end
it 'does show default stay on tab if secondary action is not given' do
click_on ticket1.title
refresh
expect(page).to have_text('Stay on tab')
end
end
context 'On ticket switch' do
let(:ticket1) { create(:ticket, title: SecureRandom.uuid, group: Group.find_by(name: 'Users')) }
let(:ticket2) { create(:ticket, title: SecureRandom.uuid, group: Group.find_by(name: 'Users')) }
before do
visit "ticket/zoom/#{ticket1.id}"
visit "ticket/zoom/#{ticket2.id}"
end
it 'does setup the last behaviour' do
click '.js-attributeBar .dropup div'
click 'span[data-type=stayOnTab]'
wait.until do
User.find_by(email: 'admin@example.com').preferences['secondaryAction'] == 'stayOnTab'
end
visit "ticket/zoom/#{ticket1.id}"
expect(page).to have_text('Stay on tab')
end
end
end
describe 'Core Workflow: Show hidden attributes on group selection (ticket edit) #3739', authenticated_as: :authenticate, db_strategy: :reset do
let!(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
let(:field_name) { SecureRandom.uuid }
let(:field) do
create(:object_manager_attribute_text, name: field_name, display: field_name, screens: {
'edit' => {
'ticket.agent' => {
'shown' => false,
'required' => false,
}
}
})
ObjectManager::Attribute.migration_execute
end
before do
visit "#ticket/zoom/#{ticket.id}"
end
context 'when field visible' do
let(:workflow) do
create(:core_workflow,
object: 'Ticket',
perform: { "ticket.#{field_name}" => { 'operator' => 'show', 'show' => 'true' } })
end
def authenticate
field
workflow
true
end
it 'does show up the field' do
expect(page).to have_css("div[data-attribute-name='#{field_name}']")
end
end
context 'when field hidden' do
def authenticate
field
true
end
it 'does not show the field' do
expect(page).to have_css("div[data-attribute-name='#{field_name}'].is-hidden", visible: :hidden)
end
end
end
describe 'Notes on existing ticks are discarded by editing profile settings #3088' do
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
before do
visit "#ticket/zoom/#{ticket.id}"
end
def upload_and_set_text
page.find('.js-textarea').send_keys("Hello\nThis\nis\nimportant!\nyo\nhoho\ntest test test test")
page.find('input#fileUpload_1', visible: :all).set(Rails.root.join('test/data/mail/mail001.box'))
expect(page).to have_text('mail001.box')
wait_for_upload_present
end
def wait_for_upload_present
wait.until { Taskbar.find_by(key: "Ticket-#{ticket.id}").attributes_with_association_ids['attachments'].present? }
end
def wait_for_upload_blank
wait.until { Taskbar.find_by(key: "Ticket-#{ticket.id}").attributes_with_association_ids['attachments'].blank? }
end
def switch_language_german
visit '#profile/language'
# Suppress the modal dialog that invites to contributions for translations that are < 90% as this breaks the tests for de-de.
page.evaluate_script "App.LocalStorage.set('translation_support_no', true, App.Session.get('id'))"
page.find('.js-input').click
page.find('.js-input').set('Deutsch')
page.find('.js-input').send_keys(:enter)
click_on 'Submit'
visit "#ticket/zoom/#{ticket.id}"
expect(page).to have_text(Translation.translate('de-de', 'select attachment…'))
end
def expect_upload_and_text
expect(page.find('.article-new')).to have_text('mail001.box')
expect(page.find('.article-new')).to have_text("Hello\nThis\nis\nimportant!\nyo\nhoho\ntest test test test")
end
def expect_no_upload_and_text
expect(page.find('.article-new')).to have_no_text('mail001.box')
expect(page.find('.article-new')).to have_no_text("Hello\nThis\nis\nimportant!\nyo\nhoho\ntest test test test")
end
it 'does show up the attachments after a reload of the page' do
upload_and_set_text
expect_upload_and_text
refresh
expect_upload_and_text
end
it 'does show up the attachments after updating language (ui:rerender event)' do
upload_and_set_text
expect_upload_and_text
switch_language_german
expect_upload_and_text
end
it 'does remove attachments and text on reset' do
upload_and_set_text
expect_upload_and_text
page.find('.js-reset').click
wait_for_upload_blank
expect_no_upload_and_text
refresh
expect_no_upload_and_text
end
context 'when rerendering (#3831)' do
def rerender
page.evaluate_script("App.Event.trigger('ui:rerender')")
end
it 'does loose attachments after rerender' do
upload_and_set_text
expect_upload_and_text
rerender
expect_upload_and_text
end
it 'does not readd the attachments after reset' do
upload_and_set_text
expect_upload_and_text
page.find('.js-reset').click
wait_for_upload_blank
expect_no_upload_and_text
rerender
expect_no_upload_and_text
end
it 'does not readd the attachments after submit' do
upload_and_set_text
expect_upload_and_text
page.find('.js-submit').click
wait_for_upload_blank
expect_no_upload_and_text
rerender
expect_no_upload_and_text
end
it 'does not show the ticket as changed after the upload removal' do
page.find('input#fileUpload_1[data-initialized="true"]', visible: :all).set(Rails.root.join('test/data/mail/mail001.box'))
await_empty_ajax_queue
expect(page.find('.article-new')).to have_text('mail001.box')
wait_for_upload_present
begin
page.evaluate_script("$('div.attachment-delete.js-delete:last').trigger('click')") # not interactable
rescue # Lint/SuppressedException
# because its not interactable it also
# returns this weird exception for the jquery
# even tho it worked fine
end
expect(page).to have_no_selector('.js-reset')
end
end
end
describe 'Unable to close tickets in certran cases if core workflow is used #3710', authenticated_as: :authenticate, db_strategy: :reset do
let!(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
let(:field_name) { SecureRandom.uuid }
let(:field) do
create(:object_manager_attribute_text, name: field_name, display: field_name, screens: {
'edit' => {
'ticket.agent' => {
'shown' => false,
'required' => false,
}
}
})
ObjectManager::Attribute.migration_execute
end
let(:workflow) do
create(:core_workflow,
object: 'Ticket',
perform: { "ticket.#{field_name}" => { 'operator' => 'set_mandatory', 'set_mandatory' => 'true' } })
end
def authenticate
field
workflow
true
end
before do
visit "#ticket/zoom/#{ticket.id}"
end
it 'does save the ticket because the field is mandatory but hidden' do
admin = User.find_by(email: 'admin@example.com')
select admin.fullname, from: 'Owner'
find('.js-submit').click
expect(ticket.reload.owner_id).to eq(admin.id)
end
end
describe "escaped 'Set fixed' workflows don't refresh set values on active ticket sessions #3757", authenticated_as: :authenticate, db_strategy: :reset do
let(:field_name) { SecureRandom.uuid }
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users'), field_name => false) }
def authenticate
workflow
create(:object_manager_attribute_boolean, :required_screen, name: field_name, display: field_name)
ObjectManager::Attribute.migration_execute
ticket
true
end
before do
visit "#ticket/zoom/#{ticket.id}"
end
context 'when operator set_fixed_to' do
let(:workflow) do
create(:core_workflow,
object: 'Ticket',
perform: { "ticket.#{field_name}" => { 'operator' => 'set_fixed_to', 'set_fixed_to' => ['false'] } })
end
context 'when saved value is removed by set_fixed_to operator' do
it 'does show up the saved value if it would not be possible because of the restriction' do
expect(page.find("select[name='#{field_name}']").value).to eq('false')
ticket.update(field_name => true)
wait.until { page.find("select[name='#{field_name}']").value == 'true' }
expect(page.find("select[name='#{field_name}']").value).to eq('true')
end
end
end
context 'when operator remove_option' do
let(:workflow) do
create(:core_workflow,
object: 'Ticket',
perform: { "ticket.#{field_name}" => { 'operator' => 'remove_option', 'remove_option' => ['true'] } })
end
context 'when saved value is removed by set_fixed_to operator' do
it 'does show up the saved value if it would not be possible because of the restriction' do
expect(page.find("select[name='#{field_name}']").value).to eq('false')
ticket.update(field_name => true)
wait.until { page.find("select[name='#{field_name}']").value == 'true' }
expect(page.find("select[name='#{field_name}']").value).to eq('true')
end
end
end
end
context 'Basic sidebar handling because of regressions in #3757' do
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
before do
visit "#ticket/zoom/#{ticket.id}"
ensure_websocket
end
it 'does show up the new priority' do
high_prio = Ticket::Priority.find_by(name: '3 high')
ticket.update(priority: high_prio)
wait.until { page.find("select[name='priority_id']").value == high_prio.id.to_s }
expect(page.find("select[name='priority_id']").value).to eq(high_prio.id.to_s)
end
it 'does show up the new group (different case because it will also trigger a full rerender because of potential permission changes)' do
group = Group.find_by(name: 'some group1')
ticket.update(group: group)
wait.until { page.find("input[name='group_id']", visible: :all).value == group.id.to_s }
expect(page.find("input[name='group_id']", visible: :all).value).to eq(group.id.to_s)
end
it 'does show up the new state and pending time' do
pending_state = Ticket::State.find_by(name: 'pending reminder')
ticket.update(state: pending_state, pending_time: 1.day.from_now)
wait.until { page.find("select[name='state_id']").value == pending_state.id.to_s }
expect(page.find("select[name='state_id']").value).to eq(pending_state.id.to_s)
expect(page).to have_css("div[data-name='pending_time']")
end
it 'does merge attributes with remote priority (ajax) and local state (user)' do
select 'closed', from: 'State'
high_prio = Ticket::Priority.find_by(name: '3 high')
closed_state = Ticket::State.find_by(name: 'closed')
ticket.update(priority: high_prio)
wait.until { page.find("select[name='priority_id']").value == high_prio.id.to_s }
expect(page.find("select[name='priority_id']").value).to eq(high_prio.id.to_s)
expect(page.find("select[name='state_id']").value).to eq(closed_state.id.to_s)
end
context 'when 2 users are in 2 different tickets' do
let(:ticket2) { create(:ticket, group: Group.find_by(name: 'Users')) }
let(:agent2) { create(:agent, password: 'test', groups: [Group.find_by(name: 'Users')]) }
before do
using_session(:second_browser) do
login(
username: agent2.login,
password: 'test',
)
visit "#ticket/zoom/#{ticket.id}"
visit "#ticket/zoom/#{ticket2.id}"
end
end
it 'does not make any changes to the second browser ticket' do
closed_state = Ticket::State.find_by(name: 'closed')
select 'closed', from: 'State'
find('.js-submit').click
using_session(:second_browser) do
sleep 3
expect(page.find("select[name='state_id']").value).not_to eq(closed_state.id.to_s)
end
end
end
end
context 'Article box opening on tickets with no changes #3789' do
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
before do
visit "#ticket/zoom/#{ticket.id}"
end
it 'does not expand the article box without changes' do
refresh
sleep 3
expect(page).to have_no_selector('form.article-add.is-open')
end
it 'does open and close by usage' do
find('.js-writeArea').click
find('.js-textarea').send_keys(' ')
expect(page).to have_css('form.article-add.is-open')
find('input#global-search').click
expect(page).to have_no_selector('form.article-add.is-open')
end
it 'does open automatically when body is given from sidebar' do
find('.js-textarea').send_keys('test')
wait.until { Taskbar.find_by(key: "Ticket-#{ticket.id}").state.dig('article', 'body').present? }
refresh
expect(page).to have_css('form.article-add.is-open')
end
it 'does open automatically when attachment is given from sidebar' do
page.find('input#fileUpload_1[data-initialized="true"]', visible: :all).set(Rails.root.join('test/data/mail/mail001.box'))
wait.until { Taskbar.find_by(key: "Ticket-#{ticket.id}").attributes_with_association_ids['attachments'].present? }
refresh
expect(page).to have_css('form.article-add.is-open')
end
end
context 'Owner should get cleared if not listed in changed group #3818', authenticated_as: :authenticate do
let(:group1) { create(:group) }
let(:group2) { create(:group) }
let(:agent1) { create(:agent) }
let(:agent2) { create(:agent) }
let(:ticket) { create(:ticket, group: group1, owner: agent1) }
def authenticate
agent1.group_names_access_map = {
group1.name => 'full',
group2.name => %w[read change overview]
}
agent2.group_names_access_map = {
group1.name => 'full',
group2.name => 'full',
}
agent1
end
before do
visit "#ticket/zoom/#{ticket.id}"
end
it 'does clear agent1 on select of group 2' do
set_tree_select_value('group_id', group2.name)
wait.until { page.find('select[name=owner_id]').value != agent1.id.to_s }
expect(page.find('select[name=owner_id]').value).to eq('')
expect(page.all('select[name=owner_id] option').map(&:value)).not_to include(agent1.id.to_s)
expect(page.all('select[name=owner_id] option').map(&:value)).to include(agent2.id.to_s)
end
end
describe 'Not displayed fields should not impact the edit screen #3819', authenticated_as: :authenticate, db_strategy: :reset do
let(:field_name) { SecureRandom.uuid }
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
def authenticate
create(:object_manager_attribute_boolean, default: nil, screens: {
edit: {
'ticket.agent' => {
shown: false,
required: false,
}
}
})
ObjectManager::Attribute.migration_execute
ticket
true
end
before do
visit "#ticket/zoom/#{ticket.id}"
end
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
sleep 3
expect(page).to have_no_selector('.js-reset')
end
end
describe 'Changing ticket status from "new" to any other status always results in uncommited status "closed" #3880', authenticated_as: :authenticate do
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
let(:workflow) do
create(:core_workflow,
object: 'Ticket',
condition_selected: {
'ticket.priority_id': {
operator: 'is',
value: [ Ticket::Priority.find_by(name: '3 high').id.to_s ],
},
},
perform: { 'ticket.state_id' => { operator: 'remove_option', remove_option: [ Ticket::State.find_by(name: 'pending reminder').id.to_s ] } })
end
def authenticate
workflow
true
end
before do
visit "#ticket/zoom/#{ticket.id}"
end
it 'does switch back to the saved value in the ticket instead of the first value of the dropdown' do
page.select 'pending reminder', from: 'state_id'
page.select '3 high', from: 'priority_id'
expect(page).to have_select('state_id', selected: 'new')
end
end
describe 'Multiselect marked as dirty', authenticated_as: :authenticate, db_strategy: :reset do
let(:field_name) { SecureRandom.uuid }
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users'), field_name => []) }
def authenticate
create(:object_manager_attribute_multiselect, name: field_name, display: field_name, screens: {
'edit' => {
'ticket.agent' => {
'shown' => true,
'required' => false,
}
}
})
ObjectManager::Attribute.migration_execute
ticket
true
end
before do
visit "#ticket/zoom/#{ticket.id}"
end
it 'does show values properly and can save values also' do
expect(page).to have_no_css('.attributeBar-reset')
end
end
describe 'Multiselect displaying and saving', authenticated_as: :authenticate, db_strategy: :reset do
let(:field_name) { SecureRandom.uuid }
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users'), field_name => %w[value_2 value_3]) }
let(:options_hash) do
{
'value_1' => 'value_1',
'value_2' => 'value_2',
'value_3' => 'value_3',
}
end
let(:data_option) { { options: options_hash, default: [] } }
def authenticate
create(:object_manager_attribute_multiselect, name: field_name, display: field_name, data_option: data_option, screens: {
'edit' => {
'ticket.agent' => {
'shown' => true,
'required' => false,
}
}
})
ObjectManager::Attribute.migration_execute
ticket
true
end
before do
visit "#ticket/zoom/#{ticket.id}"
end
def multiselect_field
page.find("select[name='#{field_name}']")
end
def multiselect_value
multiselect_field.value
end
def multiselect_set(values)
multiselect_unset_all
values = Array(values)
values.each do |value|
multiselect_field.select(value)
expect(multiselect_value).to include(value)
end
end
def multiselect_unset_all
values = multiselect_value
values.each do |value|
multiselect_field.unselect(value)
expect(multiselect_value).to not_include(value)
end
end
context 'when showing and saving values' do
it 'shows saved values properly' do
expect(multiselect_value).to eq(%w[value_2 value_3])
end
it 'saves multiple values properly' do
multiselect_set(%w[value_1 value_2])
click '.js-submit'
expect(ticket.reload[field_name]).to eq(%w[value_1 value_2])
end
it 'saves single value properly' do
multiselect_set(%w[value_1])
click '.js-submit'
expect(ticket.reload[field_name]).to eq(%w[value_1])
end
it 'removes saved values properly' do
multiselect_unset_all
click '.js-submit'
expect(ticket.reload[field_name]).to be_empty
end
end
end
describe 'Add confirmation dialog on visibility change of an article or in article creation #3924', authenticated_as: :authenticate do
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
let(:article) { create(:ticket_article, ticket: ticket) }
before do
visit "#ticket/zoom/#{article.ticket.id}"
end
context 'when dialog is disabled' do
def authenticate
true
end
it 'does set the article internal and external for existing articles' do
expect { page.find('.js-ArticleAction[data-type=internal]').click }.to change { article.reload.internal }.to(true)
expect { page.find('.js-ArticleAction[data-type=public]').click }.to change { article.reload.internal }.to(false)
end
it 'does set the article internal and external for new article' do
page.find('.js-writeArea').click(x: 5, y: 5)
expect(page).to have_css('.article-new .icon-internal')
expect(page).to have_no_css('.article-new .icon-public')
page.find('.article-new .icon-internal').click
expect(page).to have_no_css('.article-new .icon-internal')
expect(page).to have_css('.article-new .icon-public')
page.find('.article-new .icon-public').click
expect(page).to have_css('.article-new .icon-internal')
expect(page).to have_no_css('.article-new .icon-public')
end
end
context 'when dialog is enabled' do
def authenticate
Setting.set('ui_ticket_zoom_article_visibility_confirmation_dialog', true)
true
end
it 'does set the article internal and external for existing articles' do
expect { page.find('.js-ArticleAction[data-type=internal]').click }.to change { article.reload.internal }.to(true)
page.find('.js-ArticleAction[data-type=public]').click
in_modal do
expect { find('button[type=submit]').click }.to change { article.reload.internal }.to(false)
end
end
it 'does set the article internal and external for new article' do
page.find('.js-writeArea').click(x: 5, y: 5)
expect(page).to have_css('.article-new .icon-internal')
expect(page).to have_no_css('.article-new .icon-public')
page.find('.article-new .icon-internal').click
in_modal do
find('button[type=submit]').click
end
expect(page).to have_no_css('.article-new .icon-internal')
expect(page).to have_css('.article-new .icon-public')
page.find('.article-new .icon-public').click
expect(page).to have_css('.article-new .icon-internal')
expect(page).to have_no_css('.article-new .icon-public')
end
end
end
describe 'Show which escalation type escalated in ticket zoom #3928', authenticated_as: :authenticate do
let(:sla) { create(:sla, first_response_time: 1, update_time: 1, solution_time: 1) }
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
def authenticate
sla
true
end
before do
visit "#ticket/zoom/#{ticket.id}"
end
it 'does show the extended escalation information' do
page.find('.escalation-popover').hover
expect(page).to have_text('FIRST RESPONSE TIME')
expect(page).to have_text('UPDATE TIME')
expect(page).to have_text('SOLUTION TIME')
end
end
context 'Make sidebar attachments unique #3930', authenticated_as: :authenticate do
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
let(:article1) { create(:ticket_article, ticket: ticket) }
let(:article2) { create(:ticket_article, ticket: ticket) }
def attachment_add(article, filename)
create(:store,
object: 'Ticket::Article',
o_id: article.id,
data: "content #{filename}",
filename: filename,
preferences: {
'Content-Type' => 'text/plain',
})
end
def authenticate
attachment_add(article1, 'some_file.txt')
attachment_add(article2, 'some_file.txt')
attachment_add(article2, 'some_file2.txt')
Setting.set('ui_ticket_zoom_sidebar_article_attachments', true)
true
end
before do
visit "#ticket/zoom/#{ticket.id}"
page.find(".tabsSidebar-tabs .tabsSidebar-tab[data-tab='attachment']").click
end
it 'does show the attachment once' do
expect(page).to have_css('.sidebar-content .attachment.attachment--preview', count: 2)
expect(page).to have_css('.sidebar-content', text: 'some_file.txt')
expect(page).to have_css('.sidebar-content', text: 'some_file2.txt')
end
it 'does show up new attachments' do
page.find('.js-textarea').send_keys('new article with attachment')
page.find('input#fileUpload_1', visible: :all).set(Rails.root.join('test/data/mail/mail001.box'))
expect(page).to have_text('mail001.box')
wait.until { Taskbar.find_by(key: "Ticket-#{ticket.id}").attributes_with_association_ids['attachments'].present? }
click '.js-submit'
expect(page).to have_css('.sidebar-content', text: 'mail001.box')
end
end
describe 'Error “customer_id required” on Macro execution #4022', authenticated_as: :authenticate do
let(:ticket) { create(:ticket, group: Group.first) }
let(:macro) { create(:macro, perform: { 'ticket.customer_id'=>{ 'pre_condition' => 'current_user.id', 'value' => nil, 'value_completion' => '' } }) }
def authenticate
ticket && macro
true
end
before do
visit "#ticket/zoom/#{ticket.id}"
end
it 'does set the agent as customer via macro' do
click '.js-openDropdownMacro'
page.find(:macro, macro.id).click
expect(ticket.reload.customer_id).to eq(User.find_by(email: 'admin@example.com').id)
end
end
context 'Assign user to multiple organizations #1573', authenticated_as: :authenticate do
let(:organizations) { create_list(:organization, 20) }
let(:customer) { create(:customer, organization: organizations[0], organizations: organizations[1..]) }
let(:ticket) { create(:ticket, group: Group.first, customer: customer) }
def authenticate
customer
true
end
before do
visit "#ticket/zoom/#{ticket.id}"
click '.tabsSidebar-tab[data-tab=customer]'
end
it 'shows only first 3 organizations and loads more on demand' do
expect(page).to have_text(organizations[1].name)
expect(page).to have_text(organizations[2].name)
expect(page).to have_no_text(organizations[10].name)
click '.js-showMoreOrganizations a'
expect(page).to have_text(organizations[10].name)
end
end
describe 'Image preview #4044' do
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
let(:image_as_base64) do
file = Rails.root.join('spec/fixtures/files/image/squares.png').binread
Base64.encode64(file).delete("\n")
end
let(:body) do
"
"
end
let(:article) { create(:ticket_article, ticket: ticket, body: body, content_type: 'text/html') }
before do
visit "#ticket/zoom/#{article.ticket.id}"
end
it 'does open the image preview for a common image' do
within :active_ticket_article, article do
find('img').click
end
in_modal do
expect(page).to have_css('div.imagePreview img')
expect(page).to have_css('.js-cancel')
expect(page).to have_css('.js-submit')
page.find('.js-cancel').click
end
end
context 'with image and embedded link' do
let(:body) do
"
"
end
it 'does open the link for an image with an embedded link' do
within :active_ticket_article, article do
find('img').click
end
within_window switch_to_window_index(2) do
expect(page).to have_link(class: ['logo'])
end
close_window_index(2)
end
end
end
describe 'Copying ticket number' do
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
let(:ticket_number_copy_element) { 'span.ticket-number-copy svg.ticketNumberCopy-icon' }
let(:expected_clipboard_content) { (Setting.get('ticket_hook') + ticket.number).to_s }
let(:field) { find(:richtext) }
before do
visit "#ticket/zoom/#{ticket.id}"
end
it 'copies the ticket number correctly' do
find(ticket_number_copy_element).click
# simulate a paste action
within(:active_content) do
field.send_keys('')
field.click
field.send_keys([magic_key, 'v'])
end
expect(field.text).to eq(expected_clipboard_content)
end
end
describe 'Allow additional usage of Ticket Number in (Zoom) URL #849' do
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
it 'does find the ticket by ticket number' do
visit "#ticket/zoom/number/#{ticket.number}"
expect(current_url).to include("ticket/zoom/#{ticket.id}")
end
it 'does fail properly for ticket numbers which are not found' do
visit '#ticket/zoom/number/123456789'
expect(page).to have_text("I can't find this Ticket")
end
end
describe 'Article update causes missing icons in the UI after switching internal state #4213' do
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
let(:article) { create(:ticket_article, ticket: ticket, body: SecureRandom.uuid) }
before do
visit "#ticket/zoom/#{article.ticket.id}"
end
it 'does find the ticket by ticket number' do
expect(page).to have_text(article.body)
article.update(body: SecureRandom.uuid)
expect(page).to have_text(article.body)
click '.js-ArticleAction[data-type=internal]'
click '.js-ArticleAction[data-type=public]'
expect(page).to have_css('.js-ArticleAction[data-type=emailReply]')
end
end
describe 'Open ticket indicator coloring setting' do
let(:elem) { find('[data-tab="customer"]') }
let(:customer) { create(:customer) }
let(:group) { Group.first }
let(:ticket) { create(:ticket, customer: customer, group: group) }
before do
Setting.set 'ui_sidebar_open_ticket_indicator_colored', state
customer.update! preferences: { tickets_open: tickets_count }
visit "ticket/zoom/#{ticket.id}"
end
context 'when enabled' do
let(:state) { true }
context 'with 1 ticket' do
let(:tickets_count) { 1 }
it 'does not highlight' do
expect(elem)
.to have_no_selector('.tabsSidebar-tab-count--danger, .tabsSidebar-tab-count--warning')
end
end
context 'with 2 tickets' do
let(:tickets_count) { 2 }
it 'highlights as warning' do
create(:ticket, customer: customer, group: group)
expect(elem)
.to have_no_selector('.tabsSidebar-tab-count--danger')
.and have_css('.tabsSidebar-tab-count--warning')
end
end
context 'with 3 tickets' do
let(:tickets_count) { 3 }
it 'highlights as danger' do
expect(elem)
.to have_css('.tabsSidebar-tab-count--danger')
.and have_no_selector('.tabsSidebar-tab-count--warning')
end
end
end
context 'when disabled' do
let(:state) { false }
context 'with 2 tickets' do
let(:tickets_count) { 2 }
it 'does not highlight' do
expect(elem)
.to have_no_selector('.tabsSidebar-tab-count--danger, .tabsSidebar-tab-count--warning')
end
end
end
end
describe 'Incorrect notification when closing a tab after setting up an object #2042', db_strategy: :reset do
let(:ticket) { create(:ticket, group: Group.first) }
before do
# Create test ticket before adding the test object attribute.
ticket
create(:object_manager_attribute_text, name: 'text_test', display: 'text_test', screens: {
'edit' => {
'ticket.agent' => {
'shown' => true,
'required' => true,
}
}
})
ObjectManager::Attribute.migration_execute
visit "#ticket/zoom/#{ticket.id}"
end
it 'does not show the discard button nor the confirmation dialog' do
within(:active_content) do
expect(page).to have_no_css('.js-reset')
end
taskbar_tab_close("Ticket-#{ticket.id}", discard_changes: false)
expect(page).to have_no_css('.modal')
end
end
end