form_helpers.rb 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require_relative 'test_flags'
  3. # Form helpers below are loaded for the new stack app only and provide functions for returning the form field elements.
  4. module FormHelpers
  5. @form_context = nil
  6. # Returns the outer container element of the form field via its label.
  7. # The returned object is always an instance of `Capybara::Node::Element``, with some added sugar on top.
  8. def find_outer(label, **find_options)
  9. ZammadFormFieldCapybaraElementDelegator.new(find('.formkit-outer') { |element| element.has_css?('label', text: label, **find_options) }, @form_context)
  10. end
  11. # Usage:
  12. #
  13. # find_input('Title')
  14. # find_select('Owner')
  15. # find_treeselect('Category')
  16. # find_autocomplete('Customer')
  17. # find_editor('Text')
  18. # find_datepicker('Pending till')
  19. # find_toggle('Remember me')
  20. #
  21. # # In case of ambiguous labels, make sure to pass `exact_text` option
  22. # find_datepicker(nil, exact_text: 'Date')
  23. #
  24. alias find_input find_outer
  25. alias find_select find_outer
  26. alias find_treeselect find_outer
  27. alias find_autocomplete find_outer
  28. alias find_editor find_outer
  29. alias find_datepicker find_outer
  30. alias find_toggle find_outer
  31. # Returns the outer container element of the form field radio via its ID.
  32. # The returned object is always an instance of `Capybara::Node::Element``, with some added sugar on top.
  33. def find_radio(name, **find_options)
  34. ZammadFormFieldCapybaraElementDelegator.new(first("[name^=\"#{name}\"]", **find_options).ancestor('.formkit-outer'), @form_context)
  35. end
  36. # Provides a form context for stabilizing multiple field interactions.
  37. # This is implemented by tracking of the expected form updater and other GraphQL responses.
  38. # To define custom starting form updater response number, use the `form_updater_gql_number` argument (default: nil).
  39. #
  40. # Usage:
  41. #
  42. # within_form(form_updater_gql_number: 2) do
  43. # find_autocomplete('CC').search_for_options([email_address_1, email_address_2])
  44. # find_autocomplete('Tags').search_for_options([tag_1, tag_2, tag_3]).select_options(%w[foo bar])
  45. # find_editor('Text').type(body)
  46. # end
  47. #
  48. def within_form(form_updater_gql_number: nil)
  49. setup_form_context(form_updater_gql_number)
  50. yield
  51. demolish_form_context
  52. end
  53. private
  54. def setup_form_context(form_updater_gql_number)
  55. @form_context = ZammadFormContext.new
  56. return if form_updater_gql_number.blank?
  57. @form_context.init_form_updater(form_updater_gql_number)
  58. end
  59. def demolish_form_context
  60. @form_context = nil
  61. end
  62. end
  63. # Extension below allows for execution of custom actions on the returned form field elements.
  64. # This class delegates any missing methods upstream to `Capybara::Node::Element` class.
  65. class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
  66. attr_reader :element, :form_context
  67. include Capybara::DSL
  68. include BrowserTestHelper
  69. include TestFlags
  70. def initialize(element, form_context)
  71. @element = element
  72. @form_context = form_context
  73. super(element)
  74. end
  75. # Returns identifier of the form field.
  76. def field_id
  77. return element.find('.formkit-input', visible: :all)['id'] if input? || type_date? || type_datetime?
  78. return element.find('textarea')['id'] if type_textarea?
  79. return element.find('.formkit-fieldset')['id'] if type_radio?
  80. return element.find('[role="textbox"]')['id'] if type_editor?
  81. return element.find('[role="switch"], input[type="checkbox"]', visible: :all)['id'] if type_toggle? || type_checkbox?
  82. element.find('output', visible: :all)['id']
  83. end
  84. # Returns (hidden) input element used by several form field implementations to track the current value.
  85. # NOTE: A returned element might not be a regular INPUT field due to custom implementation.
  86. def input_element
  87. element.find("##{field_id}", visible: :all)
  88. end
  89. # Searches treeselect and autocomplete fields for supplied option via its label and selects it.
  90. #
  91. # Usage:
  92. #
  93. # find_treeselect('Tree Select').search_for_option('Parent 1::Option A')
  94. # find_autocomplete('Tags').search_for_option(tag_1)
  95. #
  96. # # To wait for a custom GraphQL response, you can provide expected `gql_filename` and/or `gql_number`.
  97. # find_autocomplete('Custom').search_for_option('foo', gql_filename: 'shared/entities/user/graphql/queries/user.graphql', gql_number: 4)
  98. #
  99. # # To select an autocomplete option with a different text than the query, provide an optional `label` parameter.
  100. # find autocomplete('Customer').search_for_option(customer.email, label: customer.fullname)
  101. #
  102. def search_for_option(query, label: query, gql_filename: '', gql_number: 1, **find_options)
  103. return search_for_options(query, gql_filename: gql_filename, gql_number: gql_number, **find_options) if query.is_a?(Array)
  104. return search_for_tags_option(query, gql_filename: gql_filename, gql_number: gql_number) if type_tags?
  105. return search_for_autocomplete_option(query, label: label, gql_filename: gql_filename, gql_number: gql_number, **find_options) if autocomplete?
  106. raise 'Field does not support searching for options' if !type_treeselect?
  107. element.click
  108. wait_for_test_flag("field-tree-select-#{field_id}.opened")
  109. # calculate before closing, since we cannot access it, if dialog is closed
  110. is_multi_select = multi_select?
  111. browse_for_option(query, **find_options) do |option|
  112. find('[role="searchbox"]').fill_in with: option
  113. find('[role="option"]', text: option, **find_options).click
  114. maybe_wait_for_form_updater
  115. end
  116. send_keys(:escape) if is_multi_select
  117. wait_for_test_flag("field-tree-select-#{field_id}.closed")
  118. self # support chaining
  119. end
  120. # Searches treeselect and autocomplete fields for supplied options via their labels and selects them.
  121. # NOTE: The field must support multiple selection, otherwise an error will be raised.
  122. #
  123. # Usage:
  124. #
  125. # find_treeselect('Tree Select').search_for_options(['Parent 1::Option A', 'Parent 2::Option B', 'Option C'])
  126. # find_autocomplete('Tags').search_for_options([tag_1, tag_2, tag_3])
  127. #
  128. # # To wait for a custom GraphQL response, you can provide expected `gql_filename` and/or `gql_number`.
  129. # find_autocomplete('Tags').search_for_option('foo', gql_number: 3)
  130. #
  131. def search_for_options(queries, labels: queries, gql_filename: '', gql_number: 1, **find_options)
  132. return search_for_tags_options(queries, gql_filename: gql_filename, gql_number: gql_number) if type_tags?
  133. return search_for_autocomplete_options(queries, labels: labels, gql_filename: gql_filename, gql_number: gql_number, **find_options) if autocomplete?
  134. raise 'Field does not support searching for options' if !type_treeselect?
  135. element.click
  136. wait_for_test_flag("field-tree-select-#{field_id}.opened")
  137. raise 'Field does not support multiple selection' if !multi_select?
  138. queries.each do |query|
  139. browse_for_option(query, **find_options) do |option, rewind|
  140. find('[role="searchbox"]').fill_in with: option
  141. find('[role="option"]', text: option, **find_options).click
  142. maybe_wait_for_form_updater
  143. rewind.call
  144. end
  145. end
  146. send_keys(:escape)
  147. wait_for_test_flag("field-tree-select-#{field_id}.closed")
  148. self # support chaining
  149. end
  150. # Selects an option in select, treeselect nad autocomplete fields via its label.
  151. # NOTE: The option must be part of initial options provided by the field, no autocomplete search will occur.
  152. #
  153. # Usage:
  154. #
  155. # find_select('Owner').select_option('Test Admin Agent')
  156. # find_treeselect('Tree Select').select_option('Parent 1::Option A')
  157. # find_autocomplete('Organization').select_option(secondary_organizations.last.name)
  158. #
  159. def select_option(label, **find_options)
  160. return select_options(label, **find_options) if label.is_a?(Array)
  161. return select_treeselect_option(label, **find_options) if type_treeselect?
  162. return select_tags_option(label, **find_options) if type_tags?
  163. return select_autocomplete_option(label, **find_options) if autocomplete?
  164. raise 'Element is not a field of type select' if !type_select?
  165. element.click
  166. wait_for_test_flag('common-select.opened')
  167. # calculate before closing, since we cannot access it, if dialog is closed
  168. is_multi_select = multi_select?
  169. select_option_by_label(label, **find_options)
  170. send_keys(:escape) if is_multi_select
  171. wait_for_test_flag('common-select.closed')
  172. self # support chaining
  173. end
  174. # Selects multiple options in select, treeselect and autocomplete fields via its label.
  175. # NOTE: The option must be part of initial options provided by the field, no autocomplete search will occur.
  176. # NOTE: The field must support multiple selection, otherwise an error will be raised.
  177. #
  178. # Usage:
  179. #
  180. # find_select('Multi Select').select_options(['Option 1', 'Option 2'])
  181. # find_treeselect('Multi Tree Select').select_options(['Parent 1::Option A', 'Parent 2::Option C'])
  182. # find_autocomplete('Tags').select_options(%w[foo bar])
  183. #
  184. def select_options(labels, **find_options)
  185. return select_treeselect_options(labels, **find_options) if type_treeselect?
  186. return select_tags_options(labels, **find_options) if type_tags?
  187. return select_autocomplete_options(labels, **find_options) if autocomplete?
  188. raise 'Element is not a field of type select' if !type_select?
  189. element.click
  190. wait_for_test_flag('common-select.opened')
  191. raise 'Field does not support multiple selection' if !multi_select?
  192. labels.each do |label|
  193. select_option_by_label(label, **find_options)
  194. end
  195. send_keys(:escape)
  196. wait_for_test_flag('common-select.closed')
  197. self # support chaining
  198. end
  199. # Clears selection in select, treeselect and autocomplete fields.
  200. # NOTE: The field must support selection clearing, otherwise an error will be raised.
  201. def clear_selection
  202. raise 'Field does not support clearing selection' if !type_select? && !type_treeselect? && !autocomplete?
  203. element.find('[role="button"][aria-label="Clear Selection"]').click
  204. maybe_wait_for_form_updater
  205. self # support chaining
  206. end
  207. # Types the provided text into an input or editor field.
  208. #
  209. # Usage:
  210. #
  211. # find_input('Title').type(body)
  212. # find_editor('Text').type(body)
  213. #
  214. def type(text, **type_options)
  215. return type_editor(text, **type_options) if type_editor?
  216. input_element.fill_in with: text
  217. maybe_wait_for_form_updater
  218. self # support chaining
  219. end
  220. def type_editor(text, click: true)
  221. raise 'Field does not support typing' if !type_editor?
  222. cursor_home_shortcut = mac_platform? ? %i[command up] : %i[control home]
  223. input_element.click.send_keys(cursor_home_shortcut) if click
  224. input_element.send_keys(text)
  225. maybe_wait_for_form_updater
  226. self # support chaining
  227. end
  228. # Clears the input of text, editor, date and datetime fields.
  229. def clear
  230. return clear_date if type_date? || type_datetime?
  231. raise 'Field does not support clearing' if !input? && !type_editor?
  232. input_element.click.send_keys([magic_key, 'a'], :backspace)
  233. maybe_wait_for_form_updater
  234. self # support chaining
  235. end
  236. # Selects a date in a date picker field.
  237. #
  238. # Usage:
  239. # find_datepicker('Date Picker').select_date(Date.today)
  240. # find_datepicker('Date Picker').select_date('2023-01-01')
  241. #
  242. def select_date(date)
  243. raise 'Field does not support selecting dates' if !type_date? && !type_datetime?
  244. element.click
  245. wait_for_test_flag("field-date-time-#{field_id}.opened")
  246. date = Date.parse(date) if !date.is_a?(Date) && !date.is_a?(DateTime) && !date.is_a?(Time)
  247. element.find('[aria-label*="Open the years overlay"]').click
  248. element.find('.dp__overlay_col', text: date.year).click
  249. element.find('[aria-label*="Open the months overlay"]').click
  250. element.find('.dp__overlay_col', text: date.strftime('%b')).click
  251. id = date.strftime('%Y-%m-%d')
  252. element.find_by_id(id).click # rubocop:disable Rails/DynamicFindBy
  253. yield if block_given?
  254. # close_date_picker(element)
  255. # wait_for_test_flag("field-date-time-#{field_id}.closed")
  256. maybe_wait_for_form_updater
  257. self # support chaining
  258. end
  259. # Selects a date and enters time in a datetime picker field.
  260. #
  261. # Usage:
  262. # find_datepicker('Date Time').select_datetime(DateTime.now)
  263. # find_datepicker('Date Time').select_datetime('2023-01-01T09:00:00.000Z')
  264. #
  265. def select_datetime(datetime)
  266. raise 'Field does not support selecting datetimes' if !type_datetime?
  267. datetime = DateTime.parse(datetime) if !datetime.is_a?(DateTime) && !datetime.is_a?(Time)
  268. select_date(datetime) do
  269. element.find('[aria-label="Open the time picker"]').click
  270. element.find('[aria-label*="Open the hours overlay"]').click
  271. element.find('.dp__overlay_col', text: format('%02d', datetime.hour)).click
  272. element.find('[aria-label*="Open the minutes overlay"]').click
  273. element.find('.dp__overlay_col', text: format('%02d', datetime.min)).click
  274. meridian_indicator = element.find('[aria-label="Toggle AM/PM mode"]')
  275. meridian_indicator.click if meridian_indicator.text != datetime.strftime('%p')
  276. end
  277. end
  278. # Types date into a date field.
  279. #
  280. # Usage:
  281. # find_datepicker('Date Picker').type_date(Date.today)
  282. # find_datepicker('Date Picker').type_date('2023-01-01')
  283. #
  284. def type_date(date)
  285. raise 'Field does not support typing dates' if !type_date?
  286. date = Date.parse(date) if !date.is_a?(Date)
  287. # TODO: Support locales other than `en`, depending on the language of the current user.
  288. input_element.fill_in with: date.strftime('%m/%d/%Y')
  289. input_element.send_keys :return
  290. # wait_for_test_flag("field-date-time-#{field_id}.opened")
  291. # close_date_picker(element)
  292. # wait_for_test_flag("field-date-time-#{field_id}.closed")
  293. maybe_wait_for_form_updater
  294. self # support chaining
  295. end
  296. # Types date and time into a date field.
  297. #
  298. # Usage:
  299. # find_datepicker('Date Time').type_datetime(DateTime.now)
  300. # find_datepicker('Date Picker').type_datetime('2023-01-01T09:00:00.000Z')
  301. #
  302. def type_datetime(datetime)
  303. raise 'Field does not support typing datetimes' if !type_datetime?
  304. datetime = DateTime.parse(datetime) if !datetime.is_a?(DateTime) && !datetime.is_a?(Time)
  305. # TODO: Support locales other than `en`, depending on the language of the current user.
  306. input_element.fill_in with: datetime.strftime('%m/%d/%Y %-l:%M %P')
  307. input_element.send_keys :return
  308. # wait_for_test_flag("field-date-time-#{field_id}.opened")
  309. # close_date_picker(element)
  310. # wait_for_test_flag("field-date-time-#{field_id}.closed")
  311. maybe_wait_for_form_updater
  312. self # support chaining
  313. end
  314. # Selects a choice in a radio form field.
  315. #
  316. # Usage:
  317. #
  318. # find_radio('articleSenderType').select_option('Outbound Call')
  319. #
  320. def select_choice(choice, **find_options)
  321. raise 'Field does not support choice selection' if !type_radio?
  322. input_element.find('label', exact_text: choice, **find_options).click
  323. maybe_wait_for_form_updater
  324. self # support chaining
  325. end
  326. def toggle
  327. raise 'Field does not support toggling' if !type_toggle? && !type_checkbox?
  328. element.find('label').click
  329. self # support chaining
  330. end
  331. def toggle_on
  332. raise 'Field does not support toggling on' if !type_toggle? && !type_checkbox?
  333. element.find('label').click if input_element['aria-checked'] == 'false' || !input_element.checked?
  334. self # support chaining
  335. end
  336. def toggle_off
  337. raise 'Field does not support toggling off' if !type_toggle? && !type_checkbox?
  338. element.find('label').click if input_element['aria-checked'] == 'true' || input_element.checked?
  339. self # support chaining
  340. end
  341. def open
  342. element.click
  343. wait_until_opened
  344. self # support chaining
  345. end
  346. def close
  347. send_keys(:escape)
  348. wait_until_closed
  349. self # support chaining
  350. end
  351. # Dialogs are teleported to the root element, so we must search them within the document body.
  352. # In order to improve the test performance, we don't do any implicit waits here.
  353. # Instead, we do explicit waits when opening/closing dialogs within the actions.
  354. def dialog_element
  355. if type_select?
  356. page.find('#common-select[role="dialog"]', wait: false)
  357. elsif type_treeselect?
  358. page.find("#dialog-field-tree-select-#{field_id}", wait: false)
  359. elsif type_tags?
  360. page.find("#dialog-field-tags-#{field_id}", wait: false)
  361. elsif autocomplete?
  362. page.find("#dialog-field-auto-complete-#{field_id}", wait: false)
  363. end
  364. end
  365. private
  366. def method_missing(method_name, *, &)
  367. # Simulate pseudo-methods in format of `#type_[name]?` in order to determine the internal type of the field.
  368. if method_name.to_s =~ %r{^type_(.+)\?$}
  369. return element['data-type'] == $1
  370. end
  371. super
  372. end
  373. def respond_to_missing?(method_name, include_private = false)
  374. method_name.to_s =~ %r{^type_(.+)\?$} || super
  375. end
  376. def input?
  377. type_text? || type_color? || type_email? || type_number? || type_tel? || type_url? || type_password?
  378. end
  379. def autocomplete?
  380. type_autocomplete? || type_customer? || type_organization? || type_recipient? || type_externalDataSource?
  381. end
  382. # Input elements in supported fields define data attribute for "multiple" state.
  383. def multi_select?
  384. input_element['data-multiple'] == 'true'
  385. end
  386. def wait_until_opened
  387. return wait_for_test_flag('common-select.opened') if type_select?
  388. return wait_for_test_flag("field-tree-select-#{field_id}.opened") if type_treeselect?
  389. return wait_for_test_flag("field-date-time-#{field_id}.opened") if type_date? || !type_datetime
  390. return wait_for_test_flag("field-tags-#{field_id}.opened") if type_tags?
  391. return wait_for_test_flag("field-auto-complete-#{field_id}.opened") if autocomplete?
  392. raise 'Element cannot be opened'
  393. end
  394. def wait_until_closed
  395. return wait_for_test_flag('common-select.closed') if type_select?
  396. return wait_for_test_flag("field-tree-select-#{field_id}.closed") if type_treeselect?
  397. return wait_for_test_flag("field-date-time-#{field_id}.closed") if type_date? || !type_datetime
  398. return wait_for_test_flag("field-tags-#{field_id}.closed") if type_tags?
  399. return wait_for_test_flag("field-auto-complete-#{field_id}.closed") if autocomplete?
  400. raise 'Element cannot be closed'
  401. end
  402. def select_option_by_label(label, **find_options)
  403. within dialog_element do
  404. find('[role="option"]', text: label, **find_options).click
  405. maybe_wait_for_form_updater
  406. end
  407. end
  408. def browse_for_option(path, **find_options)
  409. components = path.split('::')
  410. # Goes back to the root page by clicking on back button multiple times.
  411. rewind = proc do
  412. depth = components.size - 1
  413. depth.times do
  414. find('[role="button"][aria-label="Back to previous page"]').click
  415. end
  416. end
  417. components.each_with_index do |option, index|
  418. # Child option is always the last item.
  419. if index == components.size - 1
  420. within dialog_element do
  421. yield option, rewind
  422. end
  423. next
  424. end
  425. # Parents come before.
  426. within dialog_element do
  427. find('[role="option"] span', text: option, **find_options).sibling('svg[role=link]').click
  428. end
  429. end
  430. end
  431. def search_for_tags_option(query, gql_filename: '', gql_number: 1)
  432. element.click
  433. wait_for_test_flag("field-tags-#{field_id}.opened")
  434. within dialog_element do
  435. find('[role="searchbox"]').fill_in with: query
  436. send_keys(:tab)
  437. wait_for_autocomplete_gql(gql_filename, gql_number)
  438. end
  439. send_keys(:escape)
  440. wait_for_test_flag("field-tags-#{field_id}.closed")
  441. maybe_wait_for_form_updater
  442. self # support chaining
  443. end
  444. def search_for_autocomplete_option(query, label: query, gql_filename: '', gql_number: 1, already_open: false, **find_options)
  445. if !already_open
  446. element.click
  447. wait_for_test_flag("field-auto-complete-#{field_id}.opened")
  448. end
  449. # calculate before closing, since we cannot access it, if dialog is closed
  450. is_multi_select = multi_select?
  451. within dialog_element do
  452. find('[role="searchbox"]').fill_in with: query
  453. wait_for_autocomplete_gql(gql_filename, gql_number)
  454. find('[role="option"]', text: label, **find_options).click
  455. maybe_wait_for_form_updater
  456. end
  457. send_keys(:escape) if is_multi_select
  458. wait_for_test_flag("field-auto-complete-#{field_id}.closed")
  459. self # support chaining
  460. end
  461. def search_for_tags_options(queries, gql_filename: '', gql_number: 1)
  462. element.click
  463. wait_for_test_flag("field-tags-#{field_id}.opened")
  464. raise 'Field does not support multiple selection' if !multi_select?
  465. within dialog_element do
  466. queries.each do |query|
  467. find('[role="searchbox"]').fill_in with: query
  468. send_keys(:tab)
  469. wait_for_autocomplete_gql(gql_filename, gql_number)
  470. end
  471. end
  472. send_keys(:escape)
  473. wait_for_test_flag("field-tags-#{field_id}.closed")
  474. maybe_wait_for_form_updater
  475. self # support chaining
  476. end
  477. def search_for_autocomplete_options(queries, labels: queries, gql_filename: '', gql_number: 1, **find_options)
  478. element.click
  479. wait_for_test_flag("field-auto-complete-#{field_id}.opened")
  480. within dialog_element do
  481. queries.each_with_index do |query, index|
  482. find('[role="searchbox"]').fill_in with: query
  483. wait_for_autocomplete_gql(gql_filename, gql_number + index)
  484. raise 'Field does not support multiple selection' if !multi_select?
  485. find('[role="option"]', text: labels[index], **find_options).click
  486. maybe_wait_for_form_updater
  487. find('[aria-label="Clear Search"]').click
  488. end
  489. end
  490. send_keys(:escape)
  491. wait_for_test_flag("field-auto-complete-#{field_id}.closed")
  492. self # support chaining
  493. end
  494. def select_treeselect_option(label, **find_options)
  495. element.click
  496. wait_for_test_flag("field-tree-select-#{field_id}.opened")
  497. # calculate before closing, since we cannot access it, if dialog is closed
  498. is_multi_select = multi_select?
  499. browse_for_option(label, **find_options) do |option|
  500. find('[role="option"]', text: option, **find_options).click
  501. maybe_wait_for_form_updater
  502. end
  503. send_keys(:escape) if is_multi_select
  504. wait_for_test_flag("field-tree-select-#{field_id}.closed")
  505. self # support chaining
  506. end
  507. def select_tags_option(label, **find_options)
  508. element.click
  509. wait_for_test_flag("field-tags-#{field_id}.opened")
  510. select_option_by_label(label, **find_options)
  511. send_keys(:escape)
  512. wait_for_test_flag("field-tags-#{field_id}.closed")
  513. self # support chaining
  514. end
  515. def select_autocomplete_option(label, **find_options)
  516. element.click
  517. wait_for_test_flag("field-auto-complete-#{field_id}.opened")
  518. # calculate before closing, since we cannot access it, if dialog is closed
  519. is_multi_select = multi_select?
  520. select_option_by_label(label, **find_options)
  521. send_keys(:escape) if is_multi_select
  522. wait_for_test_flag("field-auto-complete-#{field_id}.closed")
  523. self # support chaining
  524. end
  525. def select_treeselect_options(labels, **find_options)
  526. element.click
  527. wait_for_test_flag("field-tree-select-#{field_id}.opened")
  528. raise 'Field does not support multiple selection' if !multi_select?
  529. labels.each do |label|
  530. browse_for_option(label, **find_options) do |option, rewind|
  531. find('[role="option"]', text: option, **find_options).click
  532. maybe_wait_for_form_updater
  533. rewind.call
  534. end
  535. end
  536. send_keys(:escape)
  537. wait_for_test_flag("field-tree-select-#{field_id}.closed")
  538. self # support chaining
  539. end
  540. def select_tags_options(labels, **find_options)
  541. element.click
  542. wait_for_test_flag("field-tags-#{field_id}.opened")
  543. raise 'Field does not support multiple selection' if !multi_select?
  544. labels.each do |label|
  545. select_option_by_label(label, **find_options)
  546. end
  547. send_keys(:escape)
  548. wait_for_test_flag("field-tags-#{field_id}.closed")
  549. self # support chaining
  550. end
  551. def select_autocomplete_options(labels, **find_options)
  552. element.click
  553. wait_for_test_flag("field-auto-complete-#{field_id}.opened")
  554. raise 'Field does not support multiple selection' if !multi_select?
  555. labels.each do |label|
  556. select_option_by_label(label, **find_options)
  557. end
  558. send_keys(:escape)
  559. wait_for_test_flag("field-auto-complete-#{field_id}.closed")
  560. self # support chaining
  561. end
  562. # If a GraphQL filename is passed, we will explicitly wait for it here.
  563. # Otherwise, we will implicitly wait for a query depending on the type of the field.
  564. # If no waits are to be done, we display a friendly warning to devs, since this can lead to some instability.
  565. # In form context, expected response number will be automatically increased and tracked.
  566. def wait_for_autocomplete_gql(gql_filename, gql_number)
  567. gql_number = autocomplete_gql_number(gql_filename) || gql_number
  568. if gql_filename.present?
  569. wait_for_gql(gql_filename, number: gql_number)
  570. elsif type_customer?
  571. wait_for_gql('shared/components/Form/fields/FieldCustomer/graphql/queries/autocompleteSearch/user.graphql', number: gql_number)
  572. elsif type_organization?
  573. wait_for_gql('shared/components/Form/fields/FieldOrganization/graphql/queries/autocompleteSearch/organization.graphql', number: gql_number)
  574. elsif type_recipient?
  575. wait_for_gql('shared/components/Form/fields/FieldRecipient/graphql/queries/autocompleteSearch/recipient.graphql', number: gql_number)
  576. elsif type_externalDataSource?
  577. wait_for_gql('shared/components/Form/fields/FieldExternalDataSource/graphql/queries/autocompleteSearchObjectAttributeExternalDataSource.graphql', number: gql_number)
  578. elsif type_tags?
  579. # NB: tags autocomplete query fires only once?!
  580. wait_for_gql('shared/entities/tags/graphql/queries/autocompleteTags.graphql', number: 1, skip_clearing: true)
  581. else
  582. warn 'Warning: missing `wait_for_gql` in `search_for_autocomplete_option()`, might lead to instability'
  583. end
  584. end
  585. def autocomplete_gql_number(gql_filename)
  586. return nil if form_context.nil?
  587. return form_context.form_gql_number(:autocomplete) if gql_filename.present?
  588. return form_context.form_gql_number(:customer) if type_customer?
  589. return form_context.form_gql_number(:organization) if type_organization?
  590. return form_context.form_gql_number(:recipient) if type_recipient?
  591. return form_context.form_gql_number(:externalDataSource) if type_externalDataSource?
  592. form_context.form_gql_number(:tags) if type_tags?
  593. end
  594. def triggers_form_updater?
  595. element['data-triggers-form-updater'] == 'true'
  596. end
  597. def maybe_wait_for_form_updater
  598. return if form_context.nil? || !triggers_form_updater?
  599. gql_number = form_context.form_gql_number(:form_updater)
  600. wait_for_form_updater(gql_number)
  601. end
  602. # Click on the upper left corner of the date picker field to close it.
  603. def close_date_picker(element)
  604. element_width = element.native.size.width.to_i
  605. element_height = element.native.size.height.to_i
  606. element.click(x: -element_width / 2, y: -element_height / 2)
  607. end
  608. def clear_date
  609. element.find('[role="button"][aria-label="Clear Selection"]').click
  610. maybe_wait_for_form_updater
  611. self # support chaining
  612. end
  613. end
  614. class ZammadFormContext
  615. attr_reader :context
  616. def initialize
  617. @context = {}
  618. end
  619. def init_form_updater(number)
  620. context[:gql_number] = {}
  621. context[:gql_number][:form_updater] = number
  622. end
  623. def form_gql_number(name)
  624. if context[:gql_number].nil?
  625. context[:gql_number] = {}
  626. end
  627. if context[:gql_number][name].nil?
  628. context[:gql_number][name] = 1
  629. else
  630. context[:gql_number][name] += 1
  631. end
  632. context[:gql_number][name]
  633. end
  634. end
  635. RSpec.configure do |config|
  636. config.include FormHelpers, type: :system, app: :mobile
  637. config.include FormHelpers, type: :system, app: :desktop_view
  638. end