browser_test_helper.rb 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. module BrowserTestHelper
  2. # Sometimes tests refer to elements that get removed/re-added to the DOM when
  3. # updating the UI. This causes Selenium to throw a StaleElementReferenceError exception.
  4. # This method catches this error and retries the given amount of times re-raising
  5. # the exception if the element is still stale.
  6. # @see https://developer.mozilla.org/en-US/docs/Web/WebDriver/Errors/StaleElementReference WebDriver definition
  7. #
  8. # @example
  9. # retry_on_stale do
  10. # find('.now-here-soon-gone').click
  11. # end
  12. #
  13. # retry_on_stale(retries: 10) do
  14. # find('.now-here-soon-gone').click
  15. # end
  16. #
  17. # @raise [Selenium::WebDriver::Error::StaleElementReferenceError] If element is still stale after given number of retries
  18. def retry_on_stale(retries: 3)
  19. tries ||= 0
  20. yield
  21. rescue Selenium::WebDriver::Error::StaleElementReferenceError
  22. raise if tries == retries
  23. wait_time = tries
  24. tries += 1
  25. Rails.logger.info "Stale element found. Retry #{tries}/retries (sleeping: #{wait_time})"
  26. sleep wait_time
  27. end
  28. # Finds an element and clicks it - wrapped in one method.
  29. #
  30. # @example
  31. # click('.js-channel .btn.email')
  32. #
  33. # click(:href, '#settings/branding')
  34. #
  35. def click(*args)
  36. find(*args).click
  37. end
  38. # This is a wrapper around the Selenium::WebDriver::Wait class
  39. # with additional methods.
  40. # @see BrowserTestHelper::Waiter
  41. #
  42. # @example
  43. # wait(5).until { ... }
  44. #
  45. # @example
  46. # wait(5, interval: 0.5).until { ... }
  47. #
  48. def wait(seconds = Capybara.default_max_wait_time, **kargs)
  49. wait_args = Hash(kargs).merge(timeout: seconds)
  50. wait_handle = Selenium::WebDriver::Wait.new(wait_args)
  51. Waiter.new(wait_handle)
  52. end
  53. # This checks the number of queued AJAX requests in the frontend JS app
  54. # and assures that the number is constantly zero for 0.5 seconds.
  55. # It comes in handy when waiting for AJAX requests to be completed
  56. # before performing further actions.
  57. #
  58. # @example
  59. # await_empty_ajax_queue
  60. #
  61. def await_empty_ajax_queue
  62. wait(5, interval: 0.5).until_constant do
  63. page.evaluate_script('App.Ajax.queue().length').zero?
  64. end
  65. end
  66. # Moves the mouse from its current position by the given offset.
  67. # If the coordinates provided are outside the viewport (the mouse will end up outside the browser window)
  68. # then the viewport is scrolled to match.
  69. #
  70. # @example
  71. # move_mouse_by(x, y)
  72. # move_mouse_by(100, 200)
  73. #
  74. def move_mouse_by(x_axis, y_axis)
  75. page.driver.browser.action.move_by(x_axis, y_axis).perform
  76. end
  77. # Moves the mouse to element.
  78. #
  79. # @example
  80. # move_mouse_to(page.find('button.hover_me'))
  81. #
  82. def move_mouse_to(element)
  83. element.in_fixed_position
  84. page.driver.browser.action.move_to_location(element.native.location.x, element.native.location.y).perform
  85. end
  86. # Clicks and hold (without releasing) in the middle of the given element.
  87. #
  88. # @example
  89. # click_and_hold(ticket)
  90. # click_and_hold(tr[data-id='1'])
  91. #
  92. def click_and_hold(element)
  93. page.driver.browser.action.click_and_hold(element).perform
  94. end
  95. # Releases the depressed left mouse button at the current mouse location.
  96. #
  97. # @example
  98. # release_mouse
  99. #
  100. def release_mouse
  101. page.driver.browser.action.release.perform
  102. end
  103. class Waiter < SimpleDelegator
  104. # This method is a derivation of Selenium::WebDriver::Wait#until
  105. # which ignores Capybara::ElementNotFound exceptions raised
  106. # in the given block.
  107. #
  108. # @example
  109. # wait(5).until_exists { find('[data-title="example"]') }
  110. #
  111. def until_exists
  112. self.until do
  113. yield
  114. rescue Capybara::ElementNotFound
  115. # doesn't exist yet
  116. end
  117. rescue Selenium::WebDriver::Error::TimeOutError => e
  118. # cleanup backtrace
  119. e.set_backtrace(e.backtrace.drop(3))
  120. raise e
  121. end
  122. # This method is a derivation of Selenium::WebDriver::Wait#until
  123. # which ignores Capybara::ElementNotFound exceptions raised
  124. # in the given block.
  125. #
  126. # @example
  127. # wait(5).until_disappear { find('[data-title="example"]') }
  128. #
  129. def until_disappears
  130. self.until do
  131. yield
  132. false
  133. rescue Capybara::ElementNotFound
  134. true
  135. end
  136. rescue Selenium::WebDriver::Error::TimeOutError => e
  137. # cleanup backtrace
  138. e.set_backtrace(e.backtrace.drop(3))
  139. raise e
  140. end
  141. # This method loops a given block until the result of it is constant.
  142. #
  143. # @example
  144. # wait(5).until_constant { find('.total').text }
  145. #
  146. def until_constant
  147. previous = nil
  148. timeout = __getobj__.instance_variable_get(:@timeout)
  149. interval = __getobj__.instance_variable_get(:@interval)
  150. rounds = (timeout / interval).to_i
  151. rounds.times do
  152. sleep interval
  153. latest = yield
  154. next if latest.nil?
  155. break if latest == previous
  156. previous = latest
  157. end
  158. end
  159. end
  160. end
  161. RSpec.configure do |config|
  162. config.include BrowserTestHelper, type: :system
  163. end