123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- 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(
- # username: 'admin@example.com',
- # password: 'test',
- # )
- #
- # @example
- # login(
- # username: 'admin@example.com',
- # password: 'test',
- # remember_me: true,
- # )
- #
- # return [nil]
- def login(username:, password:, remember_me: false, app: self.class.metadata[:app], skip_waiting: false)
- ENV['FAKE_SELENIUM_LOGIN_USER_ID'] = nil
- ENV['FAKE_SELENIUM_LOGIN_PENDING'] = nil
- if !page.current_path || page.current_path.exclude?('login')
- visit '/', skip_waiting: true, app: app
- end
- case app
- when :mobile
- wait_for_test_flag('applicationLoaded.loaded', skip_clearing: true)
- within('#signin') do
- find_input('Username / Email').type(username)
- find_input('Password').type(password)
- find_toggle('Remember me').toggle_on if remember_me
- click_on('Sign in')
- end
- wait_for_test_flag('useSessionUserStore.getCurrentUser.loaded', skip_clearing: true) if !skip_waiting
- when :desktop_view
- wait_for_test_flag('applicationLoaded.loaded', skip_clearing: true)
- within('main') do
- find_input('Username / Email').type(username)
- find_input('Password').type(password)
- find_toggle('Remember me').toggle_on if remember_me
- end
- click_on('Sign in')
- wait_for_test_flag('useSessionUserStore.getCurrentUser.loaded', skip_clearing: true) if !skip_waiting
- else
- expect(page).to have_button('Sign in')
- 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_on('Sign in')
- end
- current_login
- await_empty_ajax_queue
- end
- 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
- # => 'admin@example.com'
- #
- # @return [String] the login of the currently logged in user.
- def current_login
- find('.user-menu .user a')[:title]
- end
- # Returns the User record for the currently logged in user.
- #
- # @example
- # current_user.login
- # => 'admin@example.com'
- #
- # @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
- # Logs out the currently logged in user.
- #
- # @example
- # logout
- #
- def logout(app: self.class.metadata[:app])
- ENV['FAKE_SELENIUM_LOGIN_USER_ID'] = nil
- ENV['FAKE_SELENIUM_LOGIN_PENDING'] = nil
- visit('logout', app: app)
- case app
- when :mobile
- wait_for_test_flag('logout.success', skip_clearing: true)
- when :desktop_view # rubocop:disable Lint/DuplicateBranch
- wait_for_test_flag('logout.success', skip_clearing: true)
- else
- wait.until_disappears { find('.user-menu .user a', wait: false) }
- end
- end
- # Overwrites the Capybara::Session#visit method to allow SPA navigation
- # and visiting of external URLs.
- # All routes not starting with `/` will be handled as SPA routes.
- # All routes containing `://` will be handled as an external URL.
- #
- # @see Capybara::Session#visit
- #
- # @example
- # visit('signup')
- # => visited SPA route 'localhost:32435/#signup'
- #
- # @example
- # visit('/test/ui')
- # => visited regular route 'localhost:32435/test/ui'
- #
- # @example
- # visit('https://zammad.org')
- # => visited external URL 'https://zammad.org'
- #
- def visit(route, app: self.class.metadata[:app], skip_waiting: false)
- if route.include?('://')
- return without_port do
- super(route)
- end
- elsif !route.start_with?('/')
- route = if %i[mobile desktop_view].include?(app) || route.start_with?('#')
- "/#{route}"
- else
- "/##{route}"
- end
- end
- if app == :mobile
- route = "/mobile#{route}"
- elsif app == :desktop_view
- route = "/desktop#{route}"
- end
- super(route)
- wait_for_loading_to_complete(route: route, app: app, skip_waiting: skip_waiting)
- end
- def wait_for_loading_to_complete(route: nil, app: self.class.metadata[:app], skip_waiting: false, wait_ws: false)
- case app
- when :mobile
- return if skip_waiting
- wait_for_test_flag('applicationLoaded.loaded', skip_clearing: true)
- when :desktop_view # rubocop:disable Lint/DuplicateBranch
- return if skip_waiting
- wait_for_test_flag('applicationLoaded.loaded', skip_clearing: true)
- else
- return if route && (!route.start_with?('/#') || route == '/#logout')
- wait_for_pending_login(skip_waiting)
- # 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) if !skip_waiting
- # make sure WS connection is ready to use
- ensure_websocket if wait_ws
- end
- end
- # 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
- # 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, app: self.class.metadata[:app], **options) # rubocop:disable Naming/PredicateName
- if route.is_a?(String)
- case app
- when :mobile
- if !route.start_with?('/')
- route = "/#{route}"
- end
- route = Regexp.new(Regexp.quote("/mobile#{route}"))
- when :desktop_view
- if !route.start_with?('/')
- route = "/#{route}"
- end
- route = Regexp.new(Regexp.quote("/desktop#{route}"))
- else
- route = Regexp.new(Regexp.quote("/##{route}"))
- end
- end
- options.reverse_merge!(url: true)
- have_current_path(route, **options)
- end
- # This is a convenient wrapper method around #have_current_route
- # which requires no previous `expect(page).to ` call.
- #
- # @example
- # expect_current_route('login')
- # => checks for SPA route '/#login'
- #
- def expect_current_route(route, app: self.class.metadata[:app], **)
- expect(page).to have_current_route(route, app: app, **)
- end
- # 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, :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 = create(...)
- ObjectManager::Attribute.migration_execute
- page.driver.browser.navigate.refresh
- attribute
- end
- # opens the macro list in the ticket view via click
- #
- # @example
- # open_macro_list
- #
- def open_macro_list
- click '.js-openDropdownMacro'
- end
- def open_article_meta
- retry_on_stale do
- wrapper = all('div.ticket-article-item').last
- wrapper.find('.article-content .textBubble').click
- wrapper.find('.article-content-meta .article-meta.top').in_fixed_position
- end
- end
- def use_template(template, without_taskbar: false)
- field = find('#form-template select[name="id"]')
- option = field.find(:option, template.name)
- option.select_option
- taskbar_timestamp = Taskbar.last.updated_at if !without_taskbar
- click '.sidebar-content .js-apply'
- wait.until { Taskbar.last.updated_at != taskbar_timestamp } if !without_taskbar
- end
- # Checks if modal is ready.
- # Returns modal DOM element or raises an error
- #
- # @param timeout [Integer] seconds to wait
- #
- # @return [Capybara::Element] modal DOM element
- def modal_ready(timeout: Capybara.default_max_wait_time)
- find('.modal.in.modal--ready', wait: timeout)
- rescue Capybara::ElementNotFound
- raise "Modal did not appear in #{timeout} seconds"
- end
- # Executes action inside of modal. Makes sure modal has opened and closes
- # Given block is executed within modal element
- # If RSpec's expect clause is present in the block, it does not wait for modal to close
- #
- # @param timeout [Integer] seconds to wait
- # @param disappears: [Boolean] wait for modal to close because of action taken in the block. Defaults to yes.
- # @yield [] A block to be executed scoped to the modal element
- def in_modal(timeout: Capybara.default_max_wait_time, disappears: nil, &block)
- elem = modal_ready(timeout: timeout)
- # check traces for RSpec's #expect
- trace = TracePoint.new(:call) do |tp|
- next if !(tp.method_id == :expect && tp.defined_class == RSpec::Matchers)
- # set disappers to false only if it was not set explicitly in method arguments
- disappears = false if disappears.nil?
- end
- trace.enable do
- within(elem, &block)
- end
- # return and don't wait for modal to disappear if disappears is not nil and falsey
- # if disappears is nill, default behavior is to wait
- return if !disappears.nil? && !disappears
- wait(timeout, message: "Modal did not disappear in #{timeout} seconds").until do
- elem.base.obscured?
- rescue *page.driver.invalid_element_errors
- true
- rescue Selenium::WebDriver::Error::UnknownError => e
- # Newer Chrome versions may return the following error for a missing element:
- #
- # unknown error: unhandled inspector error: {"code":-32000,"message":"No node with given id found"}
- # (Session info: chrome=113.0.5672.126)"
- #
- # This error is currently unrecognized by `Selenium::WebDriver.invalid_element_errors`,
- # so we make an explicit exception.
- e.to_s.include? '"code":-32000'
- end
- end
- # Show the popover on hover
- #
- # @example
- # popover_on_hover(page.find('button.hover_me'))
- def popover_on_hover(element)
- move_mouse_to(element)
- move_mouse_by(5, 5)
- end
- # 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
- # 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
- def refresh_with_wait
- page.refresh
- # After the refresh, we must explictly wait for the app to be completely ready.
- wait_for_loading_to_complete(wait_ws: true)
- end
- private
- def wait_for_pending_login(skip_waiting)
- return if !ENV['FAKE_SELENIUM_LOGIN_PENDING']
- # When visiting the first route after login, confirm currently logged in user.
- ENV['FAKE_SELENIUM_LOGIN_PENDING'] = nil
- current_login if !skip_waiting
- nil
- end
- end
- RSpec.configure do |config|
- config.include CommonActions, type: :system
- end
|