common_actions.rb 13 KB

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