browser_test_helper.rb 7.5 KB

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