browser_test_helper.rb 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. # Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
  2. module BrowserTestHelper
  3. # Sometimes tests refer to elements that get removed/re-added to the DOM when
  4. # updating the UI. This causes Selenium to throw a StaleElementReferenceError exception.
  5. # This method catches this error and retries the given amount of times re-raising
  6. # the exception if the element is still stale.
  7. # @see https://developer.mozilla.org/en-US/docs/Web/WebDriver/Errors/StaleElementReference WebDriver definition
  8. #
  9. # @example
  10. # retry_on_stale do
  11. # find('.now-here-soon-gone').click
  12. # end
  13. #
  14. # retry_on_stale(retries: 10) do
  15. # find('.now-here-soon-gone').click
  16. # end
  17. #
  18. # @raise [Selenium::WebDriver::Error::StaleElementReferenceError] If element is still stale after given number of retries
  19. def retry_on_stale(retries: 3)
  20. tries ||= 0
  21. yield
  22. rescue Selenium::WebDriver::Error::StaleElementReferenceError
  23. raise if tries == retries
  24. wait_time = tries
  25. tries += 1
  26. Rails.logger.info "Stale element found. Retry #{tries}/retries (sleeping: #{wait_time})"
  27. sleep wait_time
  28. end
  29. # Get the current cookies from the browser with the driver object.
  30. #
  31. def cookies
  32. page.driver.browser.manage.all_cookies
  33. end
  34. # Get a single cookie by the given name (regex possible)
  35. #
  36. # @example
  37. # cookie('cookie-name')
  38. #
  39. def cookie(name)
  40. cookies.find { |cookie| cookie[:name].match?(name) }
  41. end
  42. # Finds an element and clicks it - wrapped in one method.
  43. #
  44. # @example
  45. # click('.js-channel .btn.email')
  46. #
  47. # click(:href, '#settings/branding')
  48. #
  49. def click(*args)
  50. find(*args).click
  51. end
  52. # Finds svg icon in Mobile View
  53. #
  54. # @example
  55. # icon = find_icon('home')
  56. # icon.click
  57. #
  58. def find_icon(name)
  59. find("[href=\"#icon-#{name}\"]").find(:xpath, '..')
  60. end
  61. # This is a wrapper around the Selenium::WebDriver::Wait class
  62. # with additional methods.
  63. # @see BrowserTestHelper::Waiter
  64. #
  65. # @example
  66. # wait.until { ... }
  67. #
  68. # @example
  69. # wait(5, interval: 0.5).until { ... }
  70. #
  71. def wait(seconds = Capybara.default_max_wait_time, **kargs)
  72. wait_args = Hash(kargs).merge(timeout: seconds)
  73. wait_handle = Selenium::WebDriver::Wait.new(wait_args)
  74. Waiter.new(wait_handle)
  75. end
  76. # This checks the number of queued AJAX requests in the frontend JS is zero.
  77. # It comes in handy when waiting for AJAX requests to be completed
  78. # before performing further actions.
  79. #
  80. # @example
  81. # await_empty_ajax_queue
  82. #
  83. def await_empty_ajax_queue
  84. # Waiting not supported/required by mobile app.
  85. return if self.class.metadata[:app] == :mobile # self.class needed to get metadata from within an `it` block.
  86. # page.evaluate_script silently discards any present alerts, which is not desired.
  87. begin
  88. return if page.driver.browser.switch_to.alert
  89. rescue Selenium::WebDriver::Error::NoSuchAlertError # rubocop:disable Lint/SuppressedException
  90. end
  91. # skip on non app related context
  92. return if page.evaluate_script('typeof(App) !== "function" || typeof($) !== "function"')
  93. # Always wait a little bit to allow for triggering of requests.
  94. sleep 0.1
  95. wait(5).until do
  96. page.evaluate_script('App.Ajax.queue().length === 0 && $.active === 0 && Object.keys(App.FormHandlerCoreWorkflow.getRequests()).length === 0').eql? true
  97. end
  98. rescue Selenium::WebDriver::Error::TimeoutError
  99. nil # There may be cases when the default wait time is not enough.
  100. end
  101. # Moves the mouse from its current position by the given offset.
  102. # If the coordinates provided are outside the viewport (the mouse will end up outside the browser window)
  103. # then the viewport is scrolled to match.
  104. #
  105. # @example
  106. # move_mouse_by(x, y)
  107. # move_mouse_by(100, 200)
  108. #
  109. def move_mouse_by(x_axis, y_axis)
  110. page.driver.browser.action.move_by(x_axis, y_axis).perform
  111. end
  112. # Moves the mouse to element.
  113. #
  114. # @example
  115. # move_mouse_to(page.find('button.hover_me'))
  116. #
  117. def move_mouse_to(element)
  118. element.in_fixed_position
  119. page.driver.browser.action.move_to_location(element.native.location.x, element.native.location.y).perform
  120. end
  121. # Clicks and hold (without releasing) in the middle of the given element.
  122. #
  123. # @example
  124. # click_and_hold(ticket)
  125. # click_and_hold(tr[data-id='1'])
  126. #
  127. def click_and_hold(element)
  128. page.driver.browser.action.click_and_hold(element).perform
  129. end
  130. # Clicks and hold (without releasing) in the middle of the given element
  131. # and moves it to the top left of the page to show marcos batches in
  132. # overview section.
  133. #
  134. # @example
  135. # display_macro_batches(Ticket.first)
  136. #
  137. def display_macro_batches(ticket)
  138. # Get the ticket row DOM element
  139. element = page.find(:table_row, ticket.id).native
  140. # Drag the element to the top of the screen, in order to display macro batches.
  141. # First, move the mouse to the middle left part of the element to avoid popups interfering with the action.
  142. # Then, click and hold the left mouse button.
  143. # Next, move the mouse vertically, just below the top edge of the browser.
  144. # Finally, move the mouse slightly horizontally to simulate a non-linear drag.
  145. page.driver.browser.action
  146. .move_to_location(element.location.x + 50, element.location.y + 10)
  147. .click_and_hold
  148. .move_by(0, -element.location.y + 3)
  149. .move_by(3, 0)
  150. .perform
  151. end
  152. # Releases the depressed left mouse button at the current mouse location.
  153. #
  154. # @example
  155. # release_mouse
  156. #
  157. def release_mouse
  158. page.driver.browser.action.release.perform
  159. await_empty_ajax_queue
  160. end
  161. class Waiter < SimpleDelegator
  162. # This method is a derivation of Selenium::WebDriver::Wait#until
  163. # which ignores Capybara::ElementNotFound exceptions raised
  164. # in the given block.
  165. #
  166. # @example
  167. # wait.until_exists { find('[data-title="example"]') }
  168. #
  169. def until_exists
  170. self.until do
  171. yield
  172. rescue Capybara::ElementNotFound
  173. # doesn't exist yet
  174. end
  175. rescue Selenium::WebDriver::Error::TimeoutError => e
  176. # cleanup backtrace
  177. e.set_backtrace(e.backtrace.drop(3))
  178. raise e
  179. end
  180. # This method is a derivation of Selenium::WebDriver::Wait#until
  181. # which ignores Capybara::ElementNotFound exceptions raised
  182. # in the given block.
  183. #
  184. # @example
  185. # wait.until_disappear { find('[data-title="example"]') }
  186. #
  187. def until_disappears
  188. self.until do
  189. yield
  190. false
  191. rescue Capybara::ElementNotFound
  192. true
  193. end
  194. rescue Selenium::WebDriver::Error::TimeoutError => e
  195. # cleanup backtrace
  196. e.set_backtrace(e.backtrace.drop(3))
  197. raise e
  198. end
  199. # This method loops a given block until the result of it is constant.
  200. #
  201. # @example
  202. # wait.until_constant { find('.total').text }
  203. #
  204. def until_constant
  205. previous = nil
  206. timeout = __getobj__.instance_variable_get(:@timeout)
  207. interval = __getobj__.instance_variable_get(:@interval)
  208. rounds = (timeout / interval).to_i
  209. rounds.times do
  210. sleep interval
  211. latest = yield
  212. next if latest.nil?
  213. break if latest == previous
  214. previous = latest
  215. end
  216. end
  217. end
  218. end
  219. RSpec.configure do |config|
  220. config.include BrowserTestHelper, type: :system
  221. end