2021-06-01 12:20:20 +00:00
|
|
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
|
|
|
|
2018-12-19 14:47:15 +00:00
|
|
|
module CommonActions
|
|
|
|
|
|
|
|
delegate :app_host, to: Capybara
|
|
|
|
|
|
|
|
# Performs a login with the given credentials and closes the clues (if present).
|
|
|
|
# The 'remember me' can optionally be checked.
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# login(
|
2021-08-17 12:10:02 +00:00
|
|
|
# username: 'admin@example.com',
|
2018-12-19 14:47:15 +00:00
|
|
|
# password: 'test',
|
|
|
|
# )
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# login(
|
2021-08-17 12:10:02 +00:00
|
|
|
# username: 'admin@example.com',
|
2018-12-19 14:47:15 +00:00
|
|
|
# password: 'test',
|
|
|
|
# remember_me: true,
|
|
|
|
# )
|
|
|
|
#
|
|
|
|
# return [nil]
|
|
|
|
def login(username:, password:, remember_me: false)
|
2021-11-04 13:40:58 +00:00
|
|
|
ENV['FAKE_SELENIUM_LOGIN_USER_ID'] = nil
|
|
|
|
|
2018-12-19 14:47:15 +00:00
|
|
|
visit '/'
|
|
|
|
|
|
|
|
within('#login') do
|
|
|
|
fill_in 'username', with: username
|
|
|
|
fill_in 'password', with: password
|
|
|
|
|
|
|
|
# check via label because checkbox is hidden
|
|
|
|
click('.checkbox-replacement') if remember_me
|
|
|
|
|
|
|
|
# submit
|
|
|
|
click_button
|
|
|
|
end
|
|
|
|
|
2021-12-10 12:37:47 +00:00
|
|
|
wait.until_exists do
|
2018-12-19 14:47:15 +00:00
|
|
|
current_login
|
|
|
|
end
|
2021-06-15 06:26:52 +00:00
|
|
|
|
|
|
|
await_empty_ajax_queue
|
2018-12-19 14:47:15 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Checks if the current session is logged in.
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# logged_in?
|
|
|
|
# => true
|
|
|
|
#
|
|
|
|
# @return [true, false]
|
|
|
|
def logged_in?
|
|
|
|
current_login.present?
|
|
|
|
rescue Capybara::ElementNotFound
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
|
|
|
# Returns the login of the currently logged in user.
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# current_login
|
2021-08-17 12:10:02 +00:00
|
|
|
# => 'admin@example.com'
|
2018-12-19 14:47:15 +00:00
|
|
|
#
|
|
|
|
# @return [String] the login of the currently logged in user.
|
|
|
|
def current_login
|
|
|
|
find('.user-menu .user a')[:title]
|
|
|
|
end
|
|
|
|
|
2019-07-03 16:14:28 +00:00
|
|
|
# Returns the User record for the currently logged in user.
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# current_user.login
|
2021-08-17 12:10:02 +00:00
|
|
|
# => 'admin@example.com'
|
2019-07-03 16:14:28 +00:00
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# current_user do |user|
|
|
|
|
# user.group_names_access_map = group_names_access_map
|
|
|
|
# user.save!
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# @return [User] the current user record.
|
|
|
|
def current_user
|
|
|
|
::User.find_by(login: current_login).tap do |user|
|
|
|
|
yield user if block_given?
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-12-19 14:47:15 +00:00
|
|
|
# Logs out the currently logged in user.
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# logout
|
|
|
|
#
|
|
|
|
def logout
|
2021-11-04 13:40:58 +00:00
|
|
|
ENV['FAKE_SELENIUM_LOGIN_USER_ID'] = nil
|
2018-12-19 14:47:15 +00:00
|
|
|
visit('logout')
|
2021-10-07 11:20:51 +00:00
|
|
|
|
|
|
|
wait.until_disappears { find('.user-menu .user a', wait: false) }
|
2018-12-19 14:47:15 +00:00
|
|
|
end
|
|
|
|
|
2021-02-12 13:57:45 +00:00
|
|
|
# Overwrites the Capybara::Session#visit method to allow SPA navigation
|
|
|
|
# and visiting of external URLs.
|
2018-12-19 14:47:15 +00:00
|
|
|
# All routes not starting with `/` will be handled as SPA routes.
|
2021-02-12 13:57:45 +00:00
|
|
|
# All routes containing `://` will be handled as an external URL.
|
2018-12-19 14:47:15 +00:00
|
|
|
#
|
|
|
|
# @see Capybara::Session#visit
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# visit('logout')
|
2021-02-12 13:57:45 +00:00
|
|
|
# => visited SPA route 'localhost:32435/#logout'
|
2018-12-19 14:47:15 +00:00
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# visit('/test/ui')
|
2021-02-12 13:57:45 +00:00
|
|
|
# => visited regular route 'localhost:32435/test/ui'
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# visit('https://zammad.org')
|
|
|
|
# => visited external URL 'https://zammad.org'
|
2018-12-19 14:47:15 +00:00
|
|
|
#
|
|
|
|
def visit(route)
|
2021-02-12 13:57:45 +00:00
|
|
|
if route.include?('://')
|
|
|
|
return without_port do
|
|
|
|
super(route)
|
|
|
|
end
|
|
|
|
elsif !route.start_with?('/')
|
2018-12-19 14:47:15 +00:00
|
|
|
route = "/##{route}"
|
|
|
|
end
|
|
|
|
super(route)
|
2021-06-15 06:26:52 +00:00
|
|
|
|
|
|
|
# wait for AJAX requets only on WebApp visits
|
|
|
|
return if !route.start_with?('/#')
|
|
|
|
return if route == '/#logout'
|
|
|
|
|
|
|
|
# make sure all AJAX requests are done
|
|
|
|
await_empty_ajax_queue
|
|
|
|
|
|
|
|
# make sure loading is completed (e.g. ticket zoom may take longer)
|
|
|
|
expect(page).to have_no_css('.icon-loading', wait: 30)
|
2018-12-19 14:47:15 +00:00
|
|
|
end
|
|
|
|
|
2021-02-12 13:57:45 +00:00
|
|
|
# Overwrites the global Capybara.always_include_port setting (true)
|
|
|
|
# with false. This comes in handy when visiting external pages.
|
|
|
|
#
|
|
|
|
def without_port
|
|
|
|
original = Capybara.current_session.config.always_include_port
|
|
|
|
Capybara.current_session.config.always_include_port = false
|
|
|
|
yield
|
|
|
|
ensure
|
|
|
|
Capybara.current_session.config.always_include_port = original
|
|
|
|
end
|
|
|
|
|
2018-12-19 14:47:15 +00:00
|
|
|
# This method is equivalent to Capybara::RSpecMatchers#have_current_path
|
|
|
|
# but checks the SPA route instead of the actual path.
|
|
|
|
#
|
|
|
|
# @see Capybara::RSpecMatchers#have_current_path
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# expect(page).to have_current_route('login')
|
|
|
|
# => checks for SPA route '/#login'
|
|
|
|
#
|
|
|
|
def have_current_route(route, **options)
|
|
|
|
if route.is_a?(String)
|
|
|
|
route = Regexp.new(Regexp.quote("/##{route}"))
|
|
|
|
end
|
|
|
|
|
2019-01-14 15:31:31 +00:00
|
|
|
# wait 1 sec by default because Firefox is slow
|
|
|
|
options.reverse_merge!(wait: 1, url: true)
|
2018-12-19 14:47:15 +00:00
|
|
|
|
2019-01-14 15:31:31 +00:00
|
|
|
have_current_path(route, **options)
|
2018-12-19 14:47:15 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# This is a convenient wrapper method around #have_current_route
|
|
|
|
# which requires no previous `expect(page).to ` call.
|
|
|
|
#
|
|
|
|
# @example
|
2019-01-14 15:31:31 +00:00
|
|
|
# expect_current_route('login')
|
2018-12-19 14:47:15 +00:00
|
|
|
# => checks for SPA route '/#login'
|
|
|
|
#
|
|
|
|
def expect_current_route(route, **options)
|
|
|
|
expect(page).to have_current_route(route, **options)
|
|
|
|
end
|
2019-01-04 14:26:43 +00:00
|
|
|
|
|
|
|
# Create and migrate an object manager attribute and verify that it exists. Returns the newly attribute.
|
|
|
|
#
|
|
|
|
# Create a select attribute:
|
|
|
|
# @example
|
|
|
|
# attribute = setup_attribute :object_manager_attribute_select
|
|
|
|
#
|
|
|
|
# Create a required text attribute:
|
|
|
|
# @example
|
|
|
|
# attribute = setup_attribute :object_manager_attribute_text,
|
|
|
|
# screens: attributes_for(:required_screen)
|
|
|
|
#
|
|
|
|
# Create a date attribute with custom parameters:
|
|
|
|
# @example
|
|
|
|
# attribute = setup_attribute :object_manager_attribute_date,
|
|
|
|
# data_option: {
|
|
|
|
# 'future' => true,
|
|
|
|
# 'past' => false,
|
|
|
|
# 'diff' => 24,
|
|
|
|
# 'null' => true,
|
|
|
|
# }
|
|
|
|
#
|
|
|
|
# return [attribute]
|
|
|
|
def create_attribute(attribute_name, attribute_parameters = {})
|
|
|
|
attribute = create(attribute_name, attribute_parameters)
|
|
|
|
ObjectManager::Attribute.migration_execute
|
|
|
|
page.driver.browser.navigate.refresh
|
|
|
|
attribute
|
|
|
|
end
|
2019-09-04 07:42:50 +00:00
|
|
|
|
|
|
|
# opens the macro list in the ticket view via click
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# open_macro_list
|
|
|
|
#
|
|
|
|
def open_macro_list
|
|
|
|
click '.js-openDropdownMacro'
|
|
|
|
end
|
|
|
|
|
2020-06-02 11:01:16 +00:00
|
|
|
def open_article_meta
|
2020-09-23 14:42:31 +00:00
|
|
|
retry_on_stale do
|
|
|
|
wrapper = all('div.ticket-article-item').last
|
2020-06-02 11:01:16 +00:00
|
|
|
|
2020-09-23 14:42:31 +00:00
|
|
|
wrapper.find('.article-content .textBubble').click
|
2021-12-10 12:37:47 +00:00
|
|
|
wait.until do
|
2020-09-23 14:42:31 +00:00
|
|
|
wrapper.find('.article-content-meta .article-meta.top').in_fixed_position
|
|
|
|
end
|
2020-06-02 11:01:16 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-04-30 15:38:57 +00:00
|
|
|
def use_template(template)
|
2021-12-10 12:37:47 +00:00
|
|
|
wait.until do
|
2020-04-30 15:38:57 +00:00
|
|
|
field = find('#form-template select[name="id"]')
|
|
|
|
option = field.find(:option, template.name)
|
|
|
|
option.select_option
|
|
|
|
click '.sidebar-content .js-apply'
|
|
|
|
|
|
|
|
# this is a workaround for a race condition where
|
|
|
|
# the template selection get's re-rendered after
|
|
|
|
# a selection was made. The selection is lost and
|
|
|
|
# the apply click has no effect.
|
|
|
|
template.options.any? do |attribute, value|
|
|
|
|
selector = %([name="#{attribute}"])
|
|
|
|
next if !page.has_css?(selector, wait: 0)
|
|
|
|
|
|
|
|
find(selector, wait: 0, visible: false).value == value
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-10-21 11:03:54 +00:00
|
|
|
# Checks if modal is ready
|
|
|
|
#
|
|
|
|
# @param timeout [Integer] seconds to wait
|
|
|
|
def modal_ready(timeout: 4)
|
2020-04-13 21:08:14 +00:00
|
|
|
wait(timeout).until_exists { find('.modal.in', wait: 0) }
|
2019-10-21 11:03:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Checks if modal has disappeared
|
|
|
|
#
|
|
|
|
# @param timeout [Integer] seconds to wait
|
|
|
|
def modal_disappear(timeout: 4)
|
2020-04-13 21:08:14 +00:00
|
|
|
wait(timeout).until_disappears { find('.modal', wait: 0) }
|
2019-10-21 11:03:54 +00:00
|
|
|
end
|
2020-04-13 18:24:03 +00:00
|
|
|
|
|
|
|
# Executes action inside of modal. Makes sure modal has opened and closes
|
|
|
|
#
|
|
|
|
# @param timeout [Integer] seconds to wait
|
2021-03-24 09:40:16 +00:00
|
|
|
# @param wait_for_disappear [Bool] wait for modal to close
|
|
|
|
def in_modal(timeout: 4, disappears: true, &block)
|
2020-04-13 18:24:03 +00:00
|
|
|
modal_ready(timeout: timeout)
|
|
|
|
|
2020-09-30 09:07:01 +00:00
|
|
|
within('.modal', &block)
|
2020-04-13 18:24:03 +00:00
|
|
|
|
2021-03-24 09:40:16 +00:00
|
|
|
modal_disappear(timeout: timeout) if disappears
|
2020-04-13 18:24:03 +00:00
|
|
|
end
|
2021-07-21 16:12:41 +00:00
|
|
|
|
|
|
|
# Show the popover on hover
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# popover_on_hover(page.find('button.hover_me'))
|
|
|
|
def popover_on_hover(element, wait_for_popover_killer: true)
|
|
|
|
# wait for popover killer to pass
|
|
|
|
sleep 3 if wait_for_popover_killer
|
|
|
|
|
|
|
|
move_mouse_to(element)
|
|
|
|
move_mouse_by(5, 5)
|
|
|
|
end
|
2021-09-22 20:01:39 +00:00
|
|
|
|
|
|
|
# Scroll into view with javscript.
|
|
|
|
#
|
|
|
|
# @param position [Symbol] :top or :bottom, position of the scroll into view
|
|
|
|
#
|
|
|
|
# scroll_into_view('button.js-submit)
|
|
|
|
#
|
|
|
|
def scroll_into_view(css_selector, position: :top)
|
|
|
|
page.execute_script("document.querySelector('#{css_selector}').scrollIntoView(#{position == :top})")
|
|
|
|
sleep 0.3
|
|
|
|
end
|
2021-09-27 10:49:55 +00:00
|
|
|
|
|
|
|
# Close a tab in the taskbar.
|
|
|
|
#
|
|
|
|
# @param discard_changes [Boolean] if true, discard changes
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# taskbar_tab_close('Ticket-2')
|
|
|
|
#
|
|
|
|
def taskbar_tab_close(tab_data_key, discard_changes: true)
|
|
|
|
retry_on_stale do
|
|
|
|
taskbar_entry = find(:task_with, tab_data_key)
|
|
|
|
|
|
|
|
move_mouse_to(taskbar_entry)
|
|
|
|
move_mouse_by(5, 5)
|
|
|
|
|
|
|
|
click ".tasks .task[data-key='#{tab_data_key}'] .js-close"
|
|
|
|
|
|
|
|
return if !discard_changes
|
|
|
|
|
|
|
|
in_modal do
|
|
|
|
click '.js-submit'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2018-12-19 14:47:15 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
RSpec.configure do |config|
|
|
|
|
config.include CommonActions, type: :system
|
|
|
|
end
|