# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ require 'rails_helper' require 'system/examples/core_workflow_examples' RSpec.describe 'Ticket zoom', type: :system do describe 'owner auto-assignment', authenticated_as: :authenticate do let!(:ticket) { create(:ticket, group: Group.find_by(name: 'Users'), state: Ticket::State.find_by(name: 'new')) } let!(:session_user) { User.find_by(login: 'admin@example.com') } context 'for agent disabled' do def authenticate Setting.set('ticket_auto_assignment', false) Setting.set('ticket_auto_assignment_selector', { condition: { 'ticket.state_id' => { operator: 'is', value: Ticket::State.by_category(:work_on).pluck(:id) } } }) Setting.set('ticket_auto_assignment_user_ids_ignore', []) true end it 'do not assign ticket to current session user' do visit "#ticket/zoom/#{ticket.id}" within(:active_content) do expect(page).to have_css('select[name=owner_id]') expect(page).to have_select('owner_id', selected: '-', options: ['-', 'Agent 1 Test', 'Test Admin Agent']) end end end context 'for agent enabled' do def authenticate Setting.set('ticket_auto_assignment', true) Setting.set('ticket_auto_assignment_selector', { condition: { 'ticket.state_id' => { operator: 'is', value: Ticket::State.by_category(:work_on).pluck(:id) } } }) Setting.set('ticket_auto_assignment_user_ids_ignore', setting_user_ids_ignore) if defined?(setting_user_ids_ignore) true end context 'with empty "ticket_auto_assignment_user_ids_ignore"' do it 'assigns ticket to current session user' do visit "#ticket/zoom/#{ticket.id}" within(:active_content) do expect(page).to have_css('.content.active select[name=owner_id]') expect(page).to have_select('owner_id', selected: session_user.fullname, options: ['-', 'Agent 1 Test', 'Test Admin Agent']) end end end context 'with "ticket_auto_assignment_user_ids_ignore" (as integer)' do let(:setting_user_ids_ignore) { session_user.id } it 'assigns ticket not to current session user' do visit "#ticket/zoom/#{ticket.id}" within(:active_content) do expect(page).to have_css('select[name=owner_id]') expect(page).to have_select('owner_id', selected: '-', options: ['-', 'Agent 1 Test', 'Test Admin Agent']) end end end context 'with "ticket_auto_assignment_user_ids_ignore" (as string)' do let(:setting_user_ids_ignore) { session_user.id.to_s } it 'assigns ticket not to current session user' do visit "#ticket/zoom/#{ticket.id}" within(:active_content) do expect(page).to have_css('select[name=owner_id]') expect(page).to have_select('owner_id', selected: '-', options: ['-', 'Agent 1 Test', 'Test Admin Agent']) end end end context 'with "ticket_auto_assignment_user_ids_ignore" (as [integer])' do let(:setting_user_ids_ignore) { [session_user.id] } it 'assigns ticket not to current session user' do visit "#ticket/zoom/#{ticket.id}" within(:active_content) do expect(page).to have_css('select[name=owner_id]') expect(page).to have_select('owner_id', selected: '-', options: ['-', 'Agent 1 Test', 'Test Admin Agent']) end end end context 'with "ticket_auto_assignment_user_ids_ignore" (as [string])' do let(:setting_user_ids_ignore) { [session_user.id.to_s] } it 'assigns ticket not to current session user' do visit "#ticket/zoom/#{ticket.id}" within(:active_content) do expect(page).to have_css('select[name=owner_id]') expect(page).to have_select('owner_id', selected: '-', options: ['-', 'Agent 1 Test', 'Test Admin Agent']) end end end context 'with "ticket_auto_assignment_user_ids_ignore" and other user ids' do let(:setting_user_ids_ignore) { [99_999, 999_999] } it 'assigns ticket to current session user' do visit "#ticket/zoom/#{ticket.id}" within(:active_content) do expect(page).to have_css('select[name=owner_id]') expect(page).to have_select('owner_id', selected: session_user.fullname, options: ['-', 'Agent 1 Test', 'Test Admin Agent']) end end end end end 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 Store.add( object: 'Ticket::Article', o_id: article.id, data: 'some content', filename: attachment_name, preferences: { 'Content-Type' => 'text/plain', }, created_by_id: 1, ) 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 '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 'to inbound phone call', current_user_id: -> { agent.id }, authenticated_as: -> { agent } 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', current_user_id: -> { agent.id }, authenticated_as: -> { agent } 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_selector('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}" wait.until_exists { 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 context 'S/MIME active', authenticated_as: :authenticate do let(:system_email_address) { 'smime1@example.com' } let(:email_address) { create(:email_address, email: system_email_address) } let(:group) { create(:group, email_address: email_address) } let(:agent_groups) { [group] } let(:agent) { create(:agent, groups: agent_groups) } let(:sender_email_address) { 'smime2@example.com' } let(:customer) { create(:customer, email: sender_email_address) } let!(:ticket) { create(:ticket, group: group, owner: agent, customer: customer) } def authenticate Setting.set('smime_integration', true) agent end context 'received mail' do context 'article meta information' do context 'success' do it 'shows encryption/sign information' do create(:ticket_article, preferences: { security: { type: 'S/MIME', encryption: { success: true, comment: 'COMMENT_ENCRYPT_SUCCESS', }, sign: { success: true, comment: 'COMMENT_SIGN_SUCCESS', }, } }, ticket: ticket) visit "#ticket/zoom/#{ticket.id}" expect(page).to have_css('svg.icon-lock') expect(page).to have_css('svg.icon-signed') open_article_meta expect(page).to have_css('span', text: 'Encrypted') expect(page).to have_css('span', text: 'Signed') expect(page).to have_css('span[title=COMMENT_ENCRYPT_SUCCESS]') expect(page).to have_css('span[title=COMMENT_SIGN_SUCCESS]') end end context 'error' do it 'shows create information about encryption/sign failed' do create(:ticket_article, preferences: { security: { type: 'S/MIME', encryption: { success: false, comment: 'Encryption failed because XXX', }, sign: { success: false, comment: 'Sign failed because XXX', }, } }, ticket: ticket) visit "#ticket/zoom/#{ticket.id}" expect(page).to have_css('svg.icon-not-signed') open_article_meta expect(page).to have_css('div.alert.alert--warning', text: 'Encryption failed because XXX') expect(page).to have_css('div.alert.alert--warning', text: 'Sign failed because XXX') end end end context 'certificate not present at time of arrival' do let(:mail) do smime1 = create(:smime_certificate, :with_private, fixture: system_email_address) smime2 = create(:smime_certificate, :with_private, fixture: sender_email_address) mail = Channel::EmailBuild.build( from: sender_email_address, to: system_email_address, body: 'somebody with some text', content_type: 'text/plain', security: { type: 'S/MIME', sign: { success: true, }, encryption: { success: true, }, }, ) smime1.destroy smime2.destroy mail end it 'does retry successfully' do parsed_mail = Channel::EmailParser.new.parse(mail.to_s) ticket, article, _user, _mail = Channel::EmailParser.new.process({ group_id: group.id }, parsed_mail['raw']) expect(Ticket::Article.find(article.id).body).to eq('no visible content') create(:smime_certificate, fixture: sender_email_address) create(:smime_certificate, :with_private, fixture: system_email_address) visit "#ticket/zoom/#{ticket.id}" expect(page).to have_no_css('.article-content', text: 'somebody with some text') click '.js-securityRetryProcess' expect(page).to have_css('.article-content', text: 'somebody with some text') end it 'does fail on retry (S/MIME function buttons no longer working in tickets #3957)' do parsed_mail = Channel::EmailParser.new.parse(mail.to_s) ticket, article, _user, _mail = Channel::EmailParser.new.process({ group_id: group.id }, parsed_mail['raw']) expect(Ticket::Article.find(article.id).body).to eq('no visible content') visit "#ticket/zoom/#{ticket.id}" expect(page).to have_no_css('.article-content', text: 'somebody with some text') click '.js-securityRetryProcess' expect(page).to have_css('#notify', text: 'Decryption failed! Private key for decryption could not be found.') end end end context 'replying', authenticated_as: :setup_and_authenticate do def setup_and_authenticate create(:ticket_article, ticket: ticket, from: customer.email) create(:smime_certificate, :with_private, fixture: system_email_address) create(:smime_certificate, fixture: sender_email_address) authenticate end it 'plain' do visit "#ticket/zoom/#{ticket.id}" all('a[data-type=emailReply]').last.click find('.articleNewEdit-body').send_keys('Test') expect(page).to have_css('.js-securityEncrypt.btn--active') expect(page).to have_css('.js-securitySign.btn--active') click '.js-securityEncrypt' click '.js-securitySign' click '.js-submit' expect(page).to have_css('.ticket-article-item', count: 2) expect(Ticket::Article.last.preferences['security']['encryption']['success']).to be_nil expect(Ticket::Article.last.preferences['security']['sign']['success']).to be_nil end it 'signed' do visit "#ticket/zoom/#{ticket.id}" all('a[data-type=emailReply]').last.click find('.articleNewEdit-body').send_keys('Test') expect(page).to have_css('.js-securityEncrypt.btn--active') expect(page).to have_css('.js-securitySign.btn--active') click '.js-securityEncrypt' click '.js-submit' expect(page).to have_css('.ticket-article-item', count: 2) expect(Ticket::Article.last.preferences['security']['encryption']['success']).to be_nil expect(Ticket::Article.last.preferences['security']['sign']['success']).to be true end it 'encrypted' do visit "#ticket/zoom/#{ticket.id}" all('a[data-type=emailReply]').last.click find('.articleNewEdit-body').send_keys('Test') expect(page).to have_css('.js-securityEncrypt.btn--active') expect(page).to have_css('.js-securitySign.btn--active') click '.js-securitySign' click '.js-submit' expect(page).to have_css('.ticket-article-item', count: 2) expect(Ticket::Article.last.preferences['security']['encryption']['success']).to be true expect(Ticket::Article.last.preferences['security']['sign']['success']).to be_nil end it 'signed and encrypted' do visit "#ticket/zoom/#{ticket.id}" all('a[data-type=emailReply]').last.click find('.articleNewEdit-body').send_keys('Test') expect(page).to have_css('.js-securityEncrypt.btn--active') expect(page).to have_css('.js-securitySign.btn--active') click '.js-submit' expect(page).to have_css('.ticket-article-item', count: 2) expect(Ticket::Article.last.preferences['security']['encryption']['success']).to be true expect(Ticket::Article.last.preferences['security']['sign']['success']).to be true end end context 'Group default behavior' do let(:smime_config) { {} } def authenticate Setting.set('smime_integration', true) Setting.set('smime_config', smime_config) create(:ticket_article, ticket: ticket, from: customer.email) create(:smime_certificate, :with_private, fixture: system_email_address) create(:smime_certificate, fixture: sender_email_address) agent end shared_examples 'security defaults example' do |sign:, encrypt:| it "security defaults sign: #{sign}, encrypt: #{encrypt}" do within(:active_content) do encrypt_button = find('.js-securityEncrypt') sign_button = find('.js-securitySign') active_button_class = '.btn--active' expect(encrypt_button.matches_css?(active_button_class)).to be(encrypt) expect(sign_button.matches_css?(active_button_class)).to be(sign) end end end shared_examples 'security defaults' do |sign:, encrypt:| before do visit "#ticket/zoom/#{ticket.id}" within(:active_content) do all('a[data-type=emailReply]').last.click find('.articleNewEdit-body').send_keys('Test') end end include_examples 'security defaults example', sign: sign, encrypt: encrypt end shared_examples 'security defaults group change' do |sign:, encrypt:| before do visit "#ticket/zoom/#{ticket.id}" within(:active_content) do all('a[data-type=emailReply]').last.click find('.articleNewEdit-body').send_keys('Test') select new_group.name, from: 'group_id' end end include_examples 'security defaults example', sign: sign, encrypt: encrypt end context 'not configured' do it_behaves_like 'security defaults', sign: true, encrypt: true end context 'configuration present' do let(:smime_config) do { 'group_id' => group_defaults } end let(:group_defaults) do { 'default_encryption' => { group.id.to_s => default_encryption, }, 'default_sign' => { group.id.to_s => default_sign, } } end let(:default_sign) { true } let(:default_encryption) { true } shared_examples 'sign and encrypt variations' do |check_examples_name| it_behaves_like check_examples_name, sign: true, encrypt: true context 'no value' do let(:group_defaults) { {} } it_behaves_like check_examples_name, sign: true, encrypt: true end context 'signing disabled' do let(:default_sign) { false } it_behaves_like check_examples_name, sign: false, encrypt: true end context 'encryption disabled' do let(:default_encryption) { false } it_behaves_like check_examples_name, sign: true, encrypt: false end end context 'same Group' do it_behaves_like 'sign and encrypt variations', 'security defaults' end context 'Group change' do let(:new_group) { create(:group, email_address: email_address) } let(:agent_groups) { [group, new_group] } let(:group_defaults) do { 'default_encryption' => { new_group.id.to_s => default_encryption, }, 'default_sign' => { new_group.id.to_s => default_sign, } } end it_behaves_like 'sign and encrypt variations', 'security defaults group change' end end end end describe 'linking Knowledge Base answer' do include_context 'basic Knowledge Base' let(:ticket) { create :ticket, group: Group.find_by(name: 'Users') } let(:answer) { published_answer } let(:translation) { answer.translations.first } shared_examples 'verify linking' do it 'allows to look up an answer' do visit "#ticket/zoom/#{ticket.id}" within :active_content do within '.link_kb_answers' do find('.js-add').click find('.js-input').send_keys translation.title find(%(li[data-value="#{translation.id}"])).click expect(find('.link_kb_answers ol')).to have_text translation.title end end end end context 'with ES', searchindex: true, authenticated_as: :authenticate do def authenticate configure_elasticsearch(required: true, rebuild: true) do answer end true end include_examples 'verify linking' end context 'without ES', authenticated_as: :authenticate do def authenticate answer true end include_examples 'verify linking' end end describe 'forwarding article with an image' do let(:ticket_article_body) do filename = 'squares.png' file = File.binread(Rails.root.join("spec/fixtures/image/#{filename}")) 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' find('[name=group_id]').select 'Users' find(:richtext).execute_script "this.innerHTML = \"#{ticket_article_body}\"" find('.js-submit').click end end def forward within :active_content do wait.until_exists { 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_css('select[name=state_id][disabled]') expect(page).to have_css('select[name=priority_id][disabled]') expect(page).to have_css('select[name=owner_id][disabled]') 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}" # 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_selector('.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 { click '.pagination .previous' }.not_to change { page.find('.content.active')[:id] } end it 'next is clickable for the first item' do open_nth_item(0) expect { click '.pagination .next' }.to change { page.find('.content.active')[:id] } end it 'previous is clickable for the middle item' do open_nth_item(1) expect { click '.pagination .previous' }.to change { page.find('.content.active')[:id] } end it 'next is clickable for the middle item' do open_nth_item(1) expect { click '.pagination .next' }.to change { page.find('.content.active')[:id] } end it 'previous is clickable for the last item' do open_nth_item(2) expect { click '.pagination .previous' }.to change { page.find('.content.active')[:id] } end it 'next is not clickable for the last item' do open_nth_item(2) expect { click '.pagination .next' }.not_to change { page.find('.content.active')[:id] } 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) Store.add( object: 'Ticket::Article', o_id: article1.id, data: 'some content', filename: 'some_file.txt', preferences: { 'Content-Type' => 'text/plain', }, created_by_id: 1, ) # 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')]) } it 'can subscribe and unsubscribe' do ensure_websocket do visit "ticket/zoom/#{ticket.id}" click '.js-subscriptions .js-subscribe input' expect(page).to have_selector('.js-subscriptions .js-unsubscribe input') expect(page).to have_selector('.js-subscriptions span.avatar') click '.js-subscriptions .js-unsubscribe input' expect(page).to have_selector('.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_selector('.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') expect(page).to have_text('removed Mention') 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}" sleep 3 # wait for popover killer to pass end let(:ticket) { Ticket.first } let(:elem) { find('.js-timepicker') } # 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 elem.click({ x: 10, y: 20 }) expect(elem).to have_selection(0..2) end end it 'in the second half, minutes selected' do within :active_content do elem.click({ x: 35, y: 20 }) expect(elem).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 'will properly show 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, name: 'maxtest', display: 'maxtest', screens: attributes_for(:required_screen), 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 '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('feature backlog') 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', searchindex: true, authenticated_as: :authenticate 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) } def authenticate ticket_open ticket_closed configure_elasticsearch(required: true, rebuild: true) Scheduler.worker(true) true 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.order(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', 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 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', visible: :all).set(Rails.root.join('test/data/mail/mail001.box')) 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, name: field_name, display: field_name, screens: attributes_for(:required_screen) 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}" 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("select[name='group_id']").value == group.id.to_s } expect(page.find("select[name='group_id']").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_selector("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-textarea').send_keys(' ') expect(page).to have_selector('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_selector('form.article-add.is-open') end it 'does open automatically when attachment is given from sidebar' do page.find('input#fileUpload_1', 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_selector('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 select group2.name, from: 'Group' 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 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[key_2 key_3]) } 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 def multiselect_value page.find("select[name='#{field_name}']").value end def multiselect_set(values) multiselect_unset_all values = Array(values) values.each do |value| page.find("select[name='#{field_name}']").select(value) end end def multiselect_unset_all values = page.all("select[name='#{field_name}'] option").map(&:text) values.each do |value| page.find("select[name='#{field_name}']").unselect(value) end end it 'does show values properly and can save values also' do # check ticket state rendering wait.until { multiselect_value == %w[key_2 key_3] } expect(multiselect_value).to eq(%w[key_2 key_3]) # save 2 values multiselect_set(%w[value_1 value_2]) click '.js-submit' wait.until { ticket.reload[field_name] == %w[key_1 key_2] } # save 1 value multiselect_set(['value_1']) click '.js-submit' wait.until { ticket.reload[field_name] == ['key_1'] } # unset all values multiselect_unset_all click '.js-submit' wait.until { ticket.reload[field_name].nil? } 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 expect(page).to have_css('.modal-dialog') expect { find('.modal-dialog button[type=submit]').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_css('.modal-dialog') find('.modal-dialog button[type=submit]').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 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 sleep 4 # wait for popup killer 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) Store.add( object: 'Ticket::Article', o_id: article.id, data: "content #{filename}", filename: filename, preferences: { 'Content-Type' => 'text/plain', }, created_by_id: 1, ) 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_selector('.sidebar-content .attachment.attachment--preview', count: 2) expect(page).to have_selector('.sidebar-content', text: 'some_file.txt') expect(page).to have_selector('.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_selector('.sidebar-content', text: 'mail001.box') end end end