common_actions.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. # Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
  2. module CommonActions
  3. delegate :app_host, to: Capybara
  4. # Performs a login with the given credentials and closes the clues (if present).
  5. # The 'remember me' can optionally be checked.
  6. #
  7. # @example
  8. # login(
  9. # username: 'admin@example.com',
  10. # password: 'test',
  11. # )
  12. #
  13. # @example
  14. # login(
  15. # username: 'admin@example.com',
  16. # password: 'test',
  17. # remember_me: true,
  18. # )
  19. #
  20. # return [nil]
  21. def login(username:, password:, remember_me: false, app: self.class.metadata[:app])
  22. ENV['FAKE_SELENIUM_LOGIN_USER_ID'] = nil
  23. if !page.current_path || page.current_path.exclude?('login')
  24. visit '/', skip_waiting: true
  25. end
  26. case app
  27. when :mobile
  28. wait_for_test_flag('applicationLoaded.loaded', skip_clearing: true)
  29. within('#login') do
  30. fill_in 'login', with: username
  31. fill_in 'password', with: password
  32. check 'rememberMe' if remember_me
  33. click_button
  34. end
  35. wait_for_test_flag('useSessionUserStore.getCurrentUser.loaded', skip_clearing: true)
  36. else
  37. within('#login') do
  38. fill_in 'username', with: username
  39. fill_in 'password', with: password
  40. # check via label because checkbox is hidden
  41. click('.checkbox-replacement') if remember_me
  42. # submit
  43. click_button
  44. end
  45. wait.until_exists do
  46. current_login
  47. end
  48. await_empty_ajax_queue
  49. end
  50. end
  51. # Checks if the current session is logged in.
  52. #
  53. # @example
  54. # logged_in?
  55. # => true
  56. #
  57. # @return [true, false]
  58. def logged_in?
  59. current_login.present?
  60. rescue Capybara::ElementNotFound
  61. false
  62. end
  63. # Returns the login of the currently logged in user.
  64. #
  65. # @example
  66. # current_login
  67. # => 'admin@example.com'
  68. #
  69. # @return [String] the login of the currently logged in user.
  70. def current_login
  71. find('.user-menu .user a')[:title]
  72. end
  73. # Returns the User record for the currently logged in user.
  74. #
  75. # @example
  76. # current_user.login
  77. # => 'admin@example.com'
  78. #
  79. # @example
  80. # current_user do |user|
  81. # user.group_names_access_map = group_names_access_map
  82. # user.save!
  83. # end
  84. #
  85. # @return [User] the current user record.
  86. def current_user
  87. ::User.find_by(login: current_login).tap do |user|
  88. yield user if block_given?
  89. end
  90. end
  91. # Logs out the currently logged in user.
  92. #
  93. # @example
  94. # logout
  95. #
  96. def logout(app: self.class.metadata[:app])
  97. ENV['FAKE_SELENIUM_LOGIN_USER_ID'] = nil
  98. visit('logout')
  99. case app
  100. when :mobile
  101. wait_for_test_flag('logout.success', skip_clearing: true)
  102. else
  103. wait.until_disappears { find('.user-menu .user a', wait: false) }
  104. end
  105. end
  106. # Overwrites the Capybara::Session#visit method to allow SPA navigation
  107. # and visiting of external URLs.
  108. # All routes not starting with `/` will be handled as SPA routes.
  109. # All routes containing `://` will be handled as an external URL.
  110. #
  111. # @see Capybara::Session#visit
  112. #
  113. # @example
  114. # visit('logout')
  115. # => visited SPA route 'localhost:32435/#logout'
  116. #
  117. # @example
  118. # visit('/test/ui')
  119. # => visited regular route 'localhost:32435/test/ui'
  120. #
  121. # @example
  122. # visit('https://zammad.org')
  123. # => visited external URL 'https://zammad.org'
  124. #
  125. def visit(route, app: self.class.metadata[:app], skip_waiting: false)
  126. if route.include?('://')
  127. return without_port do
  128. super(route)
  129. end
  130. elsif !route.start_with?('/')
  131. route = case app
  132. when :mobile
  133. "/#{route}"
  134. else
  135. "/##{route}"
  136. end
  137. end
  138. if app == :mobile
  139. route = "/mobile#{route}"
  140. end
  141. super(route)
  142. wait_for_loading_to_complete(route: route, app: app, skip_waiting: skip_waiting)
  143. end
  144. def wait_for_loading_to_complete(route:, app: self.class.metadata[:app], skip_waiting: false)
  145. case app
  146. when :mobile
  147. return if skip_waiting
  148. wait_for_test_flag('applicationLoaded.loaded', skip_clearing: true)
  149. else
  150. return if route && (!route.start_with?('/#') || route == '/#logout')
  151. # make sure all AJAX requests are done
  152. await_empty_ajax_queue
  153. # make sure loading is completed (e.g. ticket zoom may take longer)
  154. expect(page).to have_no_css('.icon-loading', wait: 30) if !skip_waiting
  155. end
  156. end
  157. # Overwrites the global Capybara.always_include_port setting (true)
  158. # with false. This comes in handy when visiting external pages.
  159. #
  160. def without_port
  161. original = Capybara.current_session.config.always_include_port
  162. Capybara.current_session.config.always_include_port = false
  163. yield
  164. ensure
  165. Capybara.current_session.config.always_include_port = original
  166. end
  167. # This method is equivalent to Capybara::RSpecMatchers#have_current_path
  168. # but checks the SPA route instead of the actual path.
  169. #
  170. # @see Capybara::RSpecMatchers#have_current_path
  171. #
  172. # @example
  173. # expect(page).to have_current_route('login')
  174. # => checks for SPA route '/#login'
  175. #
  176. def have_current_route(route, app: self.class.metadata[:app], **options)
  177. if route.is_a?(String)
  178. case app
  179. when :mobile
  180. if !route.start_with?('/')
  181. route = "/#{route}"
  182. end
  183. route = Regexp.new(Regexp.quote("/mobile#{route}"))
  184. else
  185. route = Regexp.new(Regexp.quote("/##{route}"))
  186. end
  187. end
  188. options.reverse_merge!(url: true)
  189. have_current_path(route, **options)
  190. end
  191. # This is a convenient wrapper method around #have_current_route
  192. # which requires no previous `expect(page).to ` call.
  193. #
  194. # @example
  195. # expect_current_route('login')
  196. # => checks for SPA route '/#login'
  197. #
  198. def expect_current_route(route, app: self.class.metadata[:app], **options)
  199. expect(page).to have_current_route(route, app: app, **options)
  200. end
  201. # Create and migrate an object manager attribute and verify that it exists. Returns the newly attribute.
  202. #
  203. # Create a select attribute:
  204. # @example
  205. # attribute = setup_attribute :object_manager_attribute_select
  206. #
  207. # Create a required text attribute:
  208. # @example
  209. # attribute = setup_attribute :object_manager_attribute_text,
  210. # screens: attributes_for(:required_screen)
  211. #
  212. # Create a date attribute with custom parameters:
  213. # @example
  214. # attribute = setup_attribute :object_manager_attribute_date,
  215. # data_option: {
  216. # 'future' => true,
  217. # 'past' => false,
  218. # 'diff' => 24,
  219. # 'null' => true,
  220. # }
  221. #
  222. # return [attribute]
  223. def create_attribute(attribute_name, attribute_parameters = {})
  224. attribute = create(attribute_name, attribute_parameters)
  225. ObjectManager::Attribute.migration_execute
  226. page.driver.browser.navigate.refresh
  227. attribute
  228. end
  229. # opens the macro list in the ticket view via click
  230. #
  231. # @example
  232. # open_macro_list
  233. #
  234. def open_macro_list
  235. click '.js-openDropdownMacro'
  236. end
  237. def open_article_meta
  238. retry_on_stale do
  239. wrapper = all('div.ticket-article-item').last
  240. wrapper.find('.article-content .textBubble').click
  241. wait.until do
  242. wrapper.find('.article-content-meta .article-meta.top').in_fixed_position
  243. end
  244. end
  245. end
  246. def use_template(template)
  247. wait.until do
  248. field = find('#form-template select[name="id"]')
  249. option = field.find(:option, template.name)
  250. option.select_option
  251. click '.sidebar-content .js-apply'
  252. # this is a workaround for a race condition where
  253. # the template selection get's re-rendered after
  254. # a selection was made. The selection is lost and
  255. # the apply click has no effect.
  256. template.options.any? do |attribute, value|
  257. selector = %([name="#{attribute}"])
  258. next if !page.has_css?(selector, wait: 0)
  259. find(selector, wait: 0, visible: false).value == value
  260. end
  261. end
  262. end
  263. # Checks if modal is ready.
  264. # Returns modal DOM element or raises an error
  265. #
  266. # @param timeout [Integer] seconds to wait
  267. #
  268. # @return [Capybara::Element] modal DOM element
  269. def modal_ready(timeout: Capybara.default_max_wait_time)
  270. find('.modal.in.modal--ready', wait: timeout)
  271. rescue Capybara::ElementNotFound
  272. raise "Modal did not appear in #{timeout} seconds"
  273. end
  274. # Executes action inside of modal. Makes sure modal has opened and closes
  275. # Given block is executed within modal element
  276. # If RSpec's expect clause is present in the block, it does not wait for modal to close
  277. #
  278. # @param timeout [Integer] seconds to wait
  279. # @param disappears: [Boolean] wait for modal to close because of action taken in the block. Defaults to yes.
  280. # @yield [] A block to be executed scoped to the modal element
  281. def in_modal(timeout: Capybara.default_max_wait_time, disappears: nil, &block)
  282. elem = modal_ready(timeout: timeout)
  283. # check traces for RSpec's #expect
  284. trace = TracePoint.new(:call) do |tp|
  285. next if !(tp.method_id == :expect && tp.defined_class == RSpec::Matchers)
  286. # set disappers to false only if it was not set explicitly in method arguments
  287. disappears = false if disappears.nil?
  288. end
  289. trace.enable do
  290. within(elem, &block)
  291. end
  292. # return and don't wait for modal to disappear if disappears is not nil and falsey
  293. # if disappears is nill, default behavior is to wait
  294. return if !disappears.nil? && !disappears
  295. wait(timeout, message: "Modal did not disappear in #{timeout} seconds").until do
  296. elem.base.obscured?
  297. rescue *page.driver.invalid_element_errors
  298. true
  299. end
  300. end
  301. # Show the popover on hover
  302. #
  303. # @example
  304. # popover_on_hover(page.find('button.hover_me'))
  305. def popover_on_hover(element)
  306. move_mouse_to(element)
  307. move_mouse_by(5, 5)
  308. end
  309. # Scroll into view with javscript.
  310. #
  311. # @param position [Symbol] :top or :bottom, position of the scroll into view
  312. #
  313. # scroll_into_view('button.js-submit)
  314. #
  315. def scroll_into_view(css_selector, position: :top)
  316. page.execute_script("document.querySelector('#{css_selector}').scrollIntoView(#{position == :top})")
  317. sleep 0.3
  318. end
  319. # Close a tab in the taskbar.
  320. #
  321. # @param discard_changes [Boolean] if true, discard changes
  322. #
  323. # @example
  324. # taskbar_tab_close('Ticket-2')
  325. #
  326. def taskbar_tab_close(tab_data_key, discard_changes: true)
  327. retry_on_stale do
  328. taskbar_entry = find(:task_with, tab_data_key)
  329. move_mouse_to(taskbar_entry)
  330. move_mouse_by(5, 5)
  331. click ".tasks .task[data-key='#{tab_data_key}'] .js-close"
  332. return if !discard_changes
  333. in_modal do
  334. click '.js-submit'
  335. end
  336. end
  337. end
  338. end
  339. RSpec.configure do |config|
  340. config.include CommonActions, type: :system
  341. end