form_helpers.rb 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
  1. # Copyright (C) 2012-2023 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(id, **find_options)
  34. ZammadFormFieldCapybaraElementDelegator.new(find("fieldset##{id}", **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 # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
  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('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('Customer').search_for_option(customer.lastname)
  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: 'apps/mobile/entities/user/graphql/queries/user.graphql', gql_number: 4)
  98. #
  99. def search_for_option(query, gql_filename: '', gql_number: 1, **find_options)
  100. return search_for_options(query, gql_filename: gql_filename, gql_number: gql_number, **find_options) if query.is_a?(Array)
  101. return search_for_tags_option(query, gql_filename: gql_filename, gql_number: gql_number) if type_tags?
  102. return search_for_autocomplete_option(query, gql_filename: gql_filename, gql_number: gql_number, **find_options) if autocomplete?
  103. raise 'Field does not support searching for options' if !type_treeselect?
  104. element.click
  105. wait_for_test_flag("field-tree-select-#{field_id}.opened")
  106. browse_for_option(query, **find_options) do |option|
  107. find('[role="searchbox"]').fill_in with: option
  108. find('[role="option"]', text: option, **find_options).click
  109. maybe_wait_for_form_updater
  110. end
  111. send_keys(:escape) if multi_select?
  112. wait_for_test_flag("field-tree-select-#{field_id}.closed")
  113. self # support chaining
  114. end
  115. # Searches treeselect and autocomplete fields for supplied options via their labels and selects them.
  116. # NOTE: The field must support multiple selection, otherwise an error will be raised.
  117. #
  118. # Usage:
  119. #
  120. # find_treeselect('Tree Select').search_for_options(['Parent 1::Option A', 'Parent 2::Option B', 'Option C'])
  121. # find_autocomplete('Tags').search_for_options([tag_1, tag_2, tag_3])
  122. #
  123. # # To wait for a custom GraphQL response, you can provide expected `gql_filename` and/or `gql_number`.
  124. # find_autocomplete('Tags').search_for_option('foo', gql_number: 3)
  125. #
  126. def search_for_options(queries, gql_filename: '', gql_number: 1, **find_options)
  127. return search_for_tags_options(queries, gql_filename: gql_filename, gql_number: gql_number) if type_tags?
  128. return search_for_autocomplete_options(queries, gql_filename: gql_filename, gql_number: gql_number, **find_options) if autocomplete?
  129. raise 'Field does not support searching for options' if !type_treeselect?
  130. element.click
  131. wait_for_test_flag("field-tree-select-#{field_id}.opened")
  132. raise 'Field does not support multiple selection' if !multi_select?
  133. queries.each do |query|
  134. browse_for_option(query, **find_options) do |option, rewind|
  135. find('[role="searchbox"]').fill_in with: option
  136. find('[role="option"]', text: option, **find_options).click
  137. maybe_wait_for_form_updater
  138. rewind.call
  139. end
  140. end
  141. send_keys(:escape)
  142. wait_for_test_flag("field-tree-select-#{field_id}.closed")
  143. self # support chaining
  144. end
  145. # Selects an option in select, treeselect nad autocomplete fields via its label.
  146. # NOTE: The option must be part of initial options provided by the field, no autocomplete search will occur.
  147. #
  148. # Usage:
  149. #
  150. # find_select('Owner').select_option('Test Admin Agent')
  151. # find_treeselect('Tree Select').select_option('Parent 1::Option A')
  152. # find_autocomplete('Organization').select_option(secondary_organizations.last.name)
  153. #
  154. def select_option(label, **find_options)
  155. return select_options(label, **find_options) if label.is_a?(Array)
  156. return select_treeselect_option(label, **find_options) if type_treeselect?
  157. return select_tags_option(label, **find_options) if type_tags?
  158. return select_autocomplete_option(label, **find_options) if autocomplete?
  159. raise 'Element is not a field of type select' if !type_select?
  160. element.click
  161. wait_for_test_flag('common-select.opened')
  162. select_option_by_label(label, **find_options)
  163. send_keys(:escape) if multi_select?
  164. wait_for_test_flag('common-select.closed')
  165. self # support chaining
  166. end
  167. # Selects multiple options in select, treeselect and autocomplete fields via its label.
  168. # NOTE: The option must be part of initial options provided by the field, no autocomplete search will occur.
  169. # NOTE: The field must support multiple selection, otherwise an error will be raised.
  170. #
  171. # Usage:
  172. #
  173. # find_select('Multi Select').select_options(['Option 1', 'Option 2'])
  174. # find_treeselect('Multi Tree Select').select_options(['Parent 1::Option A', 'Parent 2::Option C'])
  175. # find_autocomplete('Tags').select_options(%w[foo bar])
  176. #
  177. def select_options(labels, **find_options)
  178. return select_treeselect_options(labels, **find_options) if type_treeselect?
  179. return select_tags_options(labels, **find_options) if type_tags?
  180. return select_autocomplete_options(labels, **find_options) if autocomplete?
  181. raise 'Element is not a field of type select' if !type_select?
  182. element.click
  183. wait_for_test_flag('common-select.opened')
  184. raise 'Field does not support multiple selection' if !multi_select?
  185. labels.each do |label|
  186. select_option_by_label(label, **find_options)
  187. end
  188. send_keys(:escape)
  189. wait_for_test_flag('common-select.closed')
  190. self # support chaining
  191. end
  192. # Clears selection in select, treeselect and autocomplete fields.
  193. # NOTE: The field must support selection clearing, otherwise an error will be raised.
  194. def clear_selection
  195. raise 'Field does not support clearing selection' if !type_select? && !type_treeselect? && !autocomplete?
  196. element.find('[role="button"][aria-label="Clear Selection"]').click
  197. maybe_wait_for_form_updater
  198. self # support chaining
  199. end
  200. # Types the provided text into an input or editor field.
  201. #
  202. # Usage:
  203. #
  204. # find_input('Title').type(body)
  205. # find_editor('Text').type(body)
  206. #
  207. def type(text)
  208. return type_editor(text) if type_editor?
  209. input_element.fill_in with: text
  210. maybe_wait_for_form_updater
  211. self # support chaining
  212. end
  213. def type_editor(text)
  214. raise 'Field does not support typing' if !type_editor?
  215. input_element.click.send_keys(text)
  216. maybe_wait_for_form_updater
  217. self # support chaining
  218. end
  219. # Clears the input of text, editor, date and datetime fields.
  220. def clear
  221. return clear_date if type_date? || type_datetime?
  222. raise 'Field does not support clearing' if !input? && !type_editor?
  223. input_element.click.send_keys([magic_key, 'a'], :backspace)
  224. maybe_wait_for_form_updater
  225. self # support chaining
  226. end
  227. # Selects a date in a date picker field.
  228. #
  229. # Usage:
  230. # find_datepicker('Date Picker').select_date(Date.today)
  231. # find_datepicker('Date Picker').select_date('2023-01-01')
  232. #
  233. def select_date(date, with_time: false) # rubocop:disable Metrics/CyclomaticComplexity
  234. raise 'Field does not support selecting dates' if !type_date? && !type_datetime?
  235. element.click
  236. wait_for_test_flag("field-date-time-#{field_id}.opened")
  237. date = Date.parse(date) if !date.is_a?(Date) && !date.is_a?(DateTime) && !date.is_a?(Time)
  238. element.find('[aria-label="Year"]').fill_in with: date.year
  239. element.find('[aria-label="Month"]').find('option', text: date.strftime('%B')).select_option
  240. label = date.strftime('%m/%d/%Y')
  241. label += ' 12:00 am' if with_time
  242. element.find("[aria-label=\"#{label}\"]").click
  243. yield if block_given?
  244. close_date_picker(element)
  245. wait_for_test_flag("field-date-time-#{field_id}.closed")
  246. maybe_wait_for_form_updater
  247. self # support chaining
  248. end
  249. # Selects a date and enters time in a datetime picker field.
  250. #
  251. # Usage:
  252. # find_datepicker('Date Time').select_datetime(DateTime.now)
  253. # find_datepicker('Date Time').select_datetime('2023-01-01T09:00:00.000Z')
  254. #
  255. def select_datetime(datetime)
  256. raise 'Field does not support selecting datetimes' if !type_datetime?
  257. datetime = DateTime.parse(datetime) if !datetime.is_a?(DateTime) && !datetime.is_a?(Time)
  258. select_date(datetime, with_time: true) do
  259. element.find('[aria-label="Hour"]').fill_in with: datetime.hour
  260. element.find('[aria-label="Minute"]').fill_in with: datetime.min
  261. meridian_indicator = element.find('[title="Click to toggle"]')
  262. meridian_indicator.click if meridian_indicator.text != datetime.strftime('%p')
  263. end
  264. end
  265. # Types date into a date field.
  266. #
  267. # Usage:
  268. # find_datepicker('Date Picker').type_date(Date.today)
  269. # find_datepicker('Date Picker').type_date('2023-01-01')
  270. #
  271. def type_date(date)
  272. raise 'Field does not support typing dates' if !type_date?
  273. date = Date.parse(date) if !date.is_a?(Date)
  274. # NB: For some reason, Flatpickr does not support localized date input, only ISO date works ATM.
  275. input_element.fill_in with: date.strftime('%Y-%m-%d')
  276. wait_for_test_flag("field-date-time-#{field_id}.opened")
  277. close_date_picker(element)
  278. wait_for_test_flag("field-date-time-#{field_id}.closed")
  279. self # support chaining
  280. end
  281. # Types date and time into a date field.
  282. #
  283. # Usage:
  284. # find_datepicker('Date Time').type_datetime(DateTime.now)
  285. # find_datepicker('Date Picker').type_datetime('2023-01-01T09:00:00.000Z')
  286. #
  287. def type_datetime(datetime)
  288. raise 'Field does not support typing datetimes' if !type_datetime?
  289. datetime = DateTime.parse(datetime) if !datetime.is_a?(DateTime) && !datetime.is_a?(Time)
  290. # NB: For some reason, Flatpickr does not support localized date with time input, only ISO date works ATM.
  291. input_element.fill_in with: datetime.strftime('%Y-%m-%d %H:%M')
  292. wait_for_test_flag("field-date-time-#{field_id}.opened")
  293. close_date_picker(element)
  294. wait_for_test_flag("field-date-time-#{field_id}.closed")
  295. self # support chaining
  296. end
  297. # Selects a choice in a radio form field.
  298. #
  299. # Usage:
  300. #
  301. # find_radio('articleSenderType').select_option('Outbound Call')
  302. #
  303. def select_choice(choice, **find_options)
  304. raise 'Field does not support choice selection' if !type_radio?
  305. input_element.find('label', exact_text: choice, **find_options).click
  306. maybe_wait_for_form_updater
  307. self # support chaining
  308. end
  309. def toggle
  310. raise 'Field does not support toggling' if !type_toggle? && !type_checkbox?
  311. element.find('label').click
  312. self # support chaining
  313. end
  314. def toggle_on
  315. raise 'Field does not support toggling on' if !type_toggle? && !type_checkbox?
  316. element.find('label').click if !input_element.checked?
  317. self # support chaining
  318. end
  319. def toggle_off
  320. raise 'Field does not support toggling off' if !type_toggle? && !type_checkbox?
  321. element.find('label').click if input_element.checked?
  322. self # support chaining
  323. end
  324. private
  325. def method_missing(method_name, *args, &block)
  326. # Simulate pseudo-methods in format of `#type_[name]?` in order to determine the internal type of the field.
  327. if method_name.to_s =~ %r{^type_(.+)\?$}
  328. return element['data-type'] == $1
  329. end
  330. super(method_name, *args, &block)
  331. end
  332. def respond_to_missing?(method_name, include_private = false)
  333. method_name.to_s =~ %r{^type_(.+)\?$} || super
  334. end
  335. def input?
  336. type_text? || type_color? || type_email? || type_number? || type_tel? || type_url? || type_password?
  337. end
  338. def autocomplete?
  339. type_autocomplete? || type_customer? || type_organization? || type_recipient?
  340. end
  341. # Dialogs are teleported to the root element, so we must search them within the document body.
  342. # In order to improve the test performance, we don't do any implicit waits here.
  343. # Instead, we do explicit waits when opening/closing dialogs within the actions.
  344. def dialog_element
  345. if type_select?
  346. page.find('#common-select[role="dialog"]', wait: false)
  347. elsif type_treeselect?
  348. page.find("[role=\"dialog\"] #dialog-field-tree-select-#{field_id}", wait: false)
  349. elsif type_tags?
  350. page.find("[role=\"dialog\"] #dialog-field-tags-#{field_id}", wait: false)
  351. elsif autocomplete?
  352. page.find("[role=\"dialog\"] #dialog-field-auto-complete-#{field_id}", wait: false)
  353. end
  354. end
  355. # Currently, it's possible to determine if the field supports multi selection only while the dialog is visible.
  356. # If the dialog is closed, we consider the field is single selection only.
  357. def multi_select?
  358. begin
  359. dialog_element.find('[role="listbox"]', wait: false)['aria-multiselectable'] == 'true'
  360. rescue
  361. begin
  362. find('[role="listbox"]', wait: false)['aria-multiselectable'] == 'true'
  363. rescue
  364. false
  365. end
  366. end
  367. end
  368. def select_option_by_label(label, **find_options)
  369. within dialog_element do
  370. find('[role="option"]', text: label, **find_options).click
  371. maybe_wait_for_form_updater
  372. end
  373. end
  374. def browse_for_option(path, **find_options)
  375. components = path.split('::')
  376. # Goes back to the root page by clicking on back button multiple times.
  377. rewind = proc do
  378. depth = components.size - 1
  379. depth.times do
  380. find('[role="button"][aria-label="Back to previous page"]').click
  381. end
  382. end
  383. components.each_with_index do |option, index|
  384. # Child option is always the last item.
  385. if index == components.size - 1
  386. within dialog_element do
  387. yield option, rewind
  388. end
  389. next
  390. end
  391. # Parents come before.
  392. within dialog_element do
  393. find('[role="option"] span', text: option, **find_options).sibling('svg[role=link]').click
  394. end
  395. end
  396. end
  397. def search_for_tags_option(query, gql_filename: '', gql_number: 1)
  398. element.click
  399. wait_for_test_flag("field-tags-#{field_id}.opened")
  400. within dialog_element do
  401. find('[role="searchbox"]').fill_in with: query
  402. send_keys(:tab)
  403. wait_for_autocomplete_gql(gql_filename, gql_number)
  404. end
  405. send_keys(:escape)
  406. wait_for_test_flag("field-tags-#{field_id}.closed")
  407. maybe_wait_for_form_updater
  408. self # support chaining
  409. end
  410. def search_for_autocomplete_option(query, gql_filename: '', gql_number: 1, already_open: false, **find_options)
  411. if !already_open
  412. element.click
  413. wait_for_test_flag("field-auto-complete-#{field_id}.opened")
  414. end
  415. within dialog_element do
  416. find('[role="searchbox"]').fill_in with: query
  417. wait_for_autocomplete_gql(gql_filename, gql_number)
  418. find('[role="option"]', text: query, **find_options).click
  419. maybe_wait_for_form_updater
  420. end
  421. send_keys(:escape) if multi_select?
  422. wait_for_test_flag("field-auto-complete-#{field_id}.closed")
  423. self # support chaining
  424. end
  425. def search_for_tags_options(queries, gql_filename: '', gql_number: 1)
  426. element.click
  427. wait_for_test_flag("field-tags-#{field_id}.opened")
  428. raise 'Field does not support multiple selection' if !multi_select?
  429. within dialog_element do
  430. queries.each do |query|
  431. find('[role="searchbox"]').fill_in with: query
  432. send_keys(:tab)
  433. wait_for_autocomplete_gql(gql_filename, gql_number)
  434. end
  435. end
  436. send_keys(:escape)
  437. wait_for_test_flag("field-tags-#{field_id}.closed")
  438. maybe_wait_for_form_updater
  439. self # support chaining
  440. end
  441. def search_for_autocomplete_options(queries, gql_filename: '', gql_number: 1, **find_options)
  442. element.click
  443. wait_for_test_flag("field-auto-complete-#{field_id}.opened")
  444. within dialog_element do
  445. queries.each do |query|
  446. find('[role="searchbox"]').fill_in with: query
  447. wait_for_autocomplete_gql(gql_filename, gql_number)
  448. raise 'Field does not support multiple selection' if !multi_select?
  449. find('[role="option"]', text: query, **find_options).click
  450. maybe_wait_for_form_updater
  451. find('[aria-label="Clear Search"]').click
  452. end
  453. end
  454. send_keys(:escape)
  455. wait_for_test_flag("field-auto-complete-#{field_id}.closed")
  456. self # support chaining
  457. end
  458. def select_treeselect_option(label, **find_options)
  459. element.click
  460. wait_for_test_flag("field-tree-select-#{field_id}.opened")
  461. browse_for_option(label, **find_options) do |option|
  462. find('[role="option"]', text: option, **find_options).click
  463. maybe_wait_for_form_updater
  464. end
  465. send_keys(:escape) if multi_select?
  466. wait_for_test_flag("field-tree-select-#{field_id}.closed")
  467. self # support chaining
  468. end
  469. def select_tags_option(label, **find_options)
  470. element.click
  471. wait_for_test_flag("field-tags-#{field_id}.opened")
  472. select_option_by_label(label, **find_options)
  473. send_keys(:escape)
  474. wait_for_test_flag("field-tags-#{field_id}.closed")
  475. self # support chaining
  476. end
  477. def select_autocomplete_option(label, **find_options)
  478. element.click
  479. wait_for_test_flag("field-auto-complete-#{field_id}.opened")
  480. select_option_by_label(label, **find_options)
  481. send_keys(:escape) if multi_select?
  482. wait_for_test_flag("field-auto-complete-#{field_id}.closed")
  483. self # support chaining
  484. end
  485. def select_treeselect_options(labels, **find_options)
  486. element.click
  487. wait_for_test_flag("field-tree-select-#{field_id}.opened")
  488. raise 'Field does not support multiple selection' if !multi_select?
  489. labels.each do |label|
  490. browse_for_option(label, **find_options) do |option, rewind|
  491. find('[role="option"]', text: option, **find_options).click
  492. maybe_wait_for_form_updater
  493. rewind.call
  494. end
  495. end
  496. send_keys(:escape)
  497. wait_for_test_flag("field-tree-select-#{field_id}.closed")
  498. self # support chaining
  499. end
  500. def select_tags_options(labels, **find_options)
  501. element.click
  502. wait_for_test_flag("field-tags-#{field_id}.opened")
  503. raise 'Field does not support multiple selection' if !multi_select?
  504. labels.each do |label|
  505. select_option_by_label(label, **find_options)
  506. end
  507. send_keys(:escape)
  508. wait_for_test_flag("field-tags-#{field_id}.closed")
  509. self # support chaining
  510. end
  511. def select_autocomplete_options(labels, **find_options)
  512. element.click
  513. wait_for_test_flag("field-auto-complete-#{field_id}.opened")
  514. raise 'Field does not support multiple selection' if !multi_select?
  515. labels.each do |label|
  516. select_option_by_label(label, **find_options)
  517. end
  518. send_keys(:escape)
  519. wait_for_test_flag("field-auto-complete-#{field_id}.closed")
  520. self # support chaining
  521. end
  522. # If a GraphQL filename is passed, we will explicitly wait for it here.
  523. # Otherwise, we will implicitly wait for a query depending on the type of the field.
  524. # If no waits are to be done, we display a friendly warning to devs, since this can lead to some instability.
  525. # In form context, expected response number will be automatically increased and tracked.
  526. def wait_for_autocomplete_gql(gql_filename, gql_number)
  527. gql_number = autocomplete_gql_number(gql_filename) || gql_number
  528. if gql_filename.present?
  529. wait_for_gql(gql_filename, number: gql_number)
  530. elsif type_customer?
  531. wait_for_gql('shared/components/Form/fields/FieldCustomer/graphql/queries/autocompleteSearch/user.graphql', number: gql_number)
  532. elsif type_organization?
  533. wait_for_gql('shared/components/Form/fields/FieldOrganization/graphql/queries/autocompleteSearch/organization.graphql', number: gql_number)
  534. elsif type_recipient?
  535. wait_for_gql('shared/components/Form/fields/FieldRecipient/graphql/queries/autocompleteSearch/recipient.graphql', number: gql_number)
  536. elsif type_tags?
  537. # NB: tags autocomplete query fires only once?!
  538. wait_for_gql('shared/entities/tags/graphql/queries/autocompleteTags.graphql', number: 1, skip_clearing: true)
  539. else
  540. warn 'Warning: missing `wait_for_gql` in `search_for_autocomplete_option()`, might lead to instability'
  541. end
  542. end
  543. def autocomplete_gql_number(gql_filename)
  544. return nil if form_context.nil?
  545. return form_context.form_gql_number(:autocomplete) if gql_filename.present?
  546. return form_context.form_gql_number(:customer) if type_customer?
  547. return form_context.form_gql_number(:organization) if type_organization?
  548. return form_context.form_gql_number(:recipient) if type_recipient?
  549. return form_context.form_gql_number(:tags) if type_tags?
  550. end
  551. def maybe_wait_for_form_updater
  552. return if form_context.nil?
  553. gql_number = form_context.form_gql_number(:form_updater)
  554. wait_for_form_updater(gql_number)
  555. end
  556. # Click on the upper left corner of the date picker field to close it.
  557. def close_date_picker(element)
  558. element_width = element.native.size.width.to_i
  559. element_height = element.native.size.height.to_i
  560. element.click(x: -element_width / 2, y: -element_height / 2)
  561. end
  562. def clear_date
  563. element.click
  564. wait_for_test_flag("field-date-time-#{field_id}.opened")
  565. element.find_button('Clear').click
  566. close_date_picker(element)
  567. wait_for_test_flag("field-date-time-#{field_id}.closed")
  568. maybe_wait_for_form_updater
  569. self # support chaining
  570. end
  571. end
  572. class ZammadFormContext
  573. attr_reader :context
  574. def initialize
  575. @context = {}
  576. end
  577. def init_form_updater(number)
  578. context[:gql_number] = {}
  579. context[:gql_number][:form_updater] = number
  580. end
  581. def form_gql_number(name)
  582. if context[:gql_number].nil?
  583. context[:gql_number] = {}
  584. end
  585. if context[:gql_number][name].nil?
  586. context[:gql_number][name] = 1
  587. else
  588. context[:gql_number][name] += 1
  589. end
  590. context[:gql_number][name]
  591. end
  592. end
  593. RSpec.configure do |config|
  594. config.include FormHelpers, type: :system, app: :mobile
  595. end