form_helpers.rb 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875
  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: 'apps/mobile/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, with_time: false)
  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="Year"]').fill_in with: date.year
  248. element.find('[aria-label="Month"]').find('option', text: date.strftime('%B')).select_option
  249. label = date.strftime('%m/%d/%Y')
  250. label += ' 12:00 am' if with_time
  251. element.find("[aria-label=\"#{label}\"]").click
  252. yield if block_given?
  253. close_date_picker(element)
  254. wait_for_test_flag("field-date-time-#{field_id}.closed")
  255. maybe_wait_for_form_updater
  256. self # support chaining
  257. end
  258. # Selects a date and enters time in a datetime picker field.
  259. #
  260. # Usage:
  261. # find_datepicker('Date Time').select_datetime(DateTime.now)
  262. # find_datepicker('Date Time').select_datetime('2023-01-01T09:00:00.000Z')
  263. #
  264. def select_datetime(datetime)
  265. raise 'Field does not support selecting datetimes' if !type_datetime?
  266. datetime = DateTime.parse(datetime) if !datetime.is_a?(DateTime) && !datetime.is_a?(Time)
  267. select_date(datetime, with_time: true) do
  268. element.find('[aria-label="Hour"]').fill_in with: datetime.hour
  269. element.find('[aria-label="Minute"]').fill_in with: datetime.min
  270. meridian_indicator = element.find('[title="Click to toggle"]')
  271. meridian_indicator.click if meridian_indicator.text != datetime.strftime('%p')
  272. end
  273. end
  274. # Types date into a date field.
  275. #
  276. # Usage:
  277. # find_datepicker('Date Picker').type_date(Date.today)
  278. # find_datepicker('Date Picker').type_date('2023-01-01')
  279. #
  280. def type_date(date)
  281. raise 'Field does not support typing dates' if !type_date?
  282. date = Date.parse(date) if !date.is_a?(Date)
  283. # NB: For some reason, Flatpickr does not support localized date input, only ISO date works ATM.
  284. input_element.fill_in with: date.strftime('%Y-%m-%d')
  285. wait_for_test_flag("field-date-time-#{field_id}.opened")
  286. close_date_picker(element)
  287. wait_for_test_flag("field-date-time-#{field_id}.closed")
  288. self # support chaining
  289. end
  290. # Types date and time into a date field.
  291. #
  292. # Usage:
  293. # find_datepicker('Date Time').type_datetime(DateTime.now)
  294. # find_datepicker('Date Picker').type_datetime('2023-01-01T09:00:00.000Z')
  295. #
  296. def type_datetime(datetime)
  297. raise 'Field does not support typing datetimes' if !type_datetime?
  298. datetime = DateTime.parse(datetime) if !datetime.is_a?(DateTime) && !datetime.is_a?(Time)
  299. # NB: For some reason, Flatpickr does not support localized date with time input, only ISO date works ATM.
  300. input_element.fill_in with: datetime.strftime('%Y-%m-%d %H:%M')
  301. wait_for_test_flag("field-date-time-#{field_id}.opened")
  302. close_date_picker(element)
  303. wait_for_test_flag("field-date-time-#{field_id}.closed")
  304. self # support chaining
  305. end
  306. # Selects a choice in a radio form field.
  307. #
  308. # Usage:
  309. #
  310. # find_radio('articleSenderType').select_option('Outbound Call')
  311. #
  312. def select_choice(choice, **find_options)
  313. raise 'Field does not support choice selection' if !type_radio?
  314. input_element.find('label', exact_text: choice, **find_options).click
  315. maybe_wait_for_form_updater
  316. self # support chaining
  317. end
  318. def toggle
  319. raise 'Field does not support toggling' if !type_toggle? && !type_checkbox?
  320. element.find('label').click
  321. self # support chaining
  322. end
  323. def toggle_on
  324. raise 'Field does not support toggling on' if !type_toggle? && !type_checkbox?
  325. element.find('label').click if input_element['aria-checked'] == 'false' || !input_element.checked?
  326. self # support chaining
  327. end
  328. def toggle_off
  329. raise 'Field does not support toggling off' if !type_toggle? && !type_checkbox?
  330. element.find('label').click if input_element['aria-checked'] == 'true' || input_element.checked?
  331. self # support chaining
  332. end
  333. def open
  334. element.click
  335. wait_until_opened
  336. self # support chaining
  337. end
  338. def close
  339. send_keys(:escape)
  340. wait_until_closed
  341. self # support chaining
  342. end
  343. # Dialogs are teleported to the root element, so we must search them within the document body.
  344. # In order to improve the test performance, we don't do any implicit waits here.
  345. # Instead, we do explicit waits when opening/closing dialogs within the actions.
  346. def dialog_element
  347. if type_select?
  348. page.find('#common-select[role="dialog"]', wait: false)
  349. elsif type_treeselect?
  350. page.find("#dialog-field-tree-select-#{field_id}", wait: false)
  351. elsif type_tags?
  352. page.find("#dialog-field-tags-#{field_id}", wait: false)
  353. elsif autocomplete?
  354. page.find("#dialog-field-auto-complete-#{field_id}", wait: false)
  355. end
  356. end
  357. private
  358. def method_missing(method_name, *args, &)
  359. # Simulate pseudo-methods in format of `#type_[name]?` in order to determine the internal type of the field.
  360. if method_name.to_s =~ %r{^type_(.+)\?$}
  361. return element['data-type'] == $1
  362. end
  363. super(method_name, *args, &)
  364. end
  365. def respond_to_missing?(method_name, include_private = false)
  366. method_name.to_s =~ %r{^type_(.+)\?$} || super
  367. end
  368. def input?
  369. type_text? || type_color? || type_email? || type_number? || type_tel? || type_url? || type_password?
  370. end
  371. def autocomplete?
  372. type_autocomplete? || type_customer? || type_organization? || type_recipient? || type_externalDataSource?
  373. end
  374. # Input elements in supported fields define data attribute for "multiple" state.
  375. def multi_select?
  376. input_element['data-multiple'] == 'true'
  377. end
  378. def wait_until_opened
  379. return wait_for_test_flag('common-select.opened') if type_select?
  380. return wait_for_test_flag("field-tree-select-#{field_id}.opened") if type_treeselect?
  381. return wait_for_test_flag("field-date-time-#{field_id}.opened") if type_date? || !type_datetime
  382. return wait_for_test_flag("field-tags-#{field_id}.opened") if type_tags?
  383. return wait_for_test_flag("field-auto-complete-#{field_id}.opened") if autocomplete?
  384. raise 'Element cannot be opened'
  385. end
  386. def wait_until_closed
  387. return wait_for_test_flag('common-select.closed') if type_select?
  388. return wait_for_test_flag("field-tree-select-#{field_id}.closed") if type_treeselect?
  389. return wait_for_test_flag("field-date-time-#{field_id}.closed") if type_date? || !type_datetime
  390. return wait_for_test_flag("field-tags-#{field_id}.closed") if type_tags?
  391. return wait_for_test_flag("field-auto-complete-#{field_id}.closed") if autocomplete?
  392. raise 'Element cannot be closed'
  393. end
  394. def select_option_by_label(label, **find_options)
  395. within dialog_element do
  396. find('[role="option"]', text: label, **find_options).click
  397. maybe_wait_for_form_updater
  398. end
  399. end
  400. def browse_for_option(path, **find_options)
  401. components = path.split('::')
  402. # Goes back to the root page by clicking on back button multiple times.
  403. rewind = proc do
  404. depth = components.size - 1
  405. depth.times do
  406. find('[role="button"][aria-label="Back to previous page"]').click
  407. end
  408. end
  409. components.each_with_index do |option, index|
  410. # Child option is always the last item.
  411. if index == components.size - 1
  412. within dialog_element do
  413. yield option, rewind
  414. end
  415. next
  416. end
  417. # Parents come before.
  418. within dialog_element do
  419. find('[role="option"] span', text: option, **find_options).sibling('svg[role=link]').click
  420. end
  421. end
  422. end
  423. def search_for_tags_option(query, gql_filename: '', gql_number: 1)
  424. element.click
  425. wait_for_test_flag("field-tags-#{field_id}.opened")
  426. within dialog_element do
  427. find('[role="searchbox"]').fill_in with: query
  428. send_keys(:tab)
  429. wait_for_autocomplete_gql(gql_filename, gql_number)
  430. end
  431. send_keys(:escape)
  432. wait_for_test_flag("field-tags-#{field_id}.closed")
  433. maybe_wait_for_form_updater
  434. self # support chaining
  435. end
  436. def search_for_autocomplete_option(query, label: query, gql_filename: '', gql_number: 1, already_open: false, **find_options)
  437. if !already_open
  438. element.click
  439. wait_for_test_flag("field-auto-complete-#{field_id}.opened")
  440. end
  441. # calculate before closing, since we cannot access it, if dialog is closed
  442. is_multi_select = multi_select?
  443. within dialog_element do
  444. find('[role="searchbox"]').fill_in with: query
  445. wait_for_autocomplete_gql(gql_filename, gql_number)
  446. find('[role="option"]', text: label, **find_options).click
  447. maybe_wait_for_form_updater
  448. end
  449. send_keys(:escape) if is_multi_select
  450. wait_for_test_flag("field-auto-complete-#{field_id}.closed")
  451. self # support chaining
  452. end
  453. def search_for_tags_options(queries, gql_filename: '', gql_number: 1)
  454. element.click
  455. wait_for_test_flag("field-tags-#{field_id}.opened")
  456. raise 'Field does not support multiple selection' if !multi_select?
  457. within dialog_element do
  458. queries.each do |query|
  459. find('[role="searchbox"]').fill_in with: query
  460. send_keys(:tab)
  461. wait_for_autocomplete_gql(gql_filename, gql_number)
  462. end
  463. end
  464. send_keys(:escape)
  465. wait_for_test_flag("field-tags-#{field_id}.closed")
  466. maybe_wait_for_form_updater
  467. self # support chaining
  468. end
  469. def search_for_autocomplete_options(queries, labels: queries, gql_filename: '', gql_number: 1, **find_options)
  470. element.click
  471. wait_for_test_flag("field-auto-complete-#{field_id}.opened")
  472. within dialog_element do
  473. queries.each_with_index do |query, index|
  474. find('[role="searchbox"]').fill_in with: query
  475. wait_for_autocomplete_gql(gql_filename, gql_number + index)
  476. raise 'Field does not support multiple selection' if !multi_select?
  477. find('[role="option"]', text: labels[index], **find_options).click
  478. maybe_wait_for_form_updater
  479. find('[aria-label="Clear Search"]').click
  480. end
  481. end
  482. send_keys(:escape)
  483. wait_for_test_flag("field-auto-complete-#{field_id}.closed")
  484. self # support chaining
  485. end
  486. def select_treeselect_option(label, **find_options)
  487. element.click
  488. wait_for_test_flag("field-tree-select-#{field_id}.opened")
  489. # calculate before closing, since we cannot access it, if dialog is closed
  490. is_multi_select = multi_select?
  491. browse_for_option(label, **find_options) do |option|
  492. find('[role="option"]', text: option, **find_options).click
  493. maybe_wait_for_form_updater
  494. end
  495. send_keys(:escape) if is_multi_select
  496. wait_for_test_flag("field-tree-select-#{field_id}.closed")
  497. self # support chaining
  498. end
  499. def select_tags_option(label, **find_options)
  500. element.click
  501. wait_for_test_flag("field-tags-#{field_id}.opened")
  502. select_option_by_label(label, **find_options)
  503. send_keys(:escape)
  504. wait_for_test_flag("field-tags-#{field_id}.closed")
  505. self # support chaining
  506. end
  507. def select_autocomplete_option(label, **find_options)
  508. element.click
  509. wait_for_test_flag("field-auto-complete-#{field_id}.opened")
  510. # calculate before closing, since we cannot access it, if dialog is closed
  511. is_multi_select = multi_select?
  512. select_option_by_label(label, **find_options)
  513. send_keys(:escape) if is_multi_select
  514. wait_for_test_flag("field-auto-complete-#{field_id}.closed")
  515. self # support chaining
  516. end
  517. def select_treeselect_options(labels, **find_options)
  518. element.click
  519. wait_for_test_flag("field-tree-select-#{field_id}.opened")
  520. raise 'Field does not support multiple selection' if !multi_select?
  521. labels.each do |label|
  522. browse_for_option(label, **find_options) do |option, rewind|
  523. find('[role="option"]', text: option, **find_options).click
  524. maybe_wait_for_form_updater
  525. rewind.call
  526. end
  527. end
  528. send_keys(:escape)
  529. wait_for_test_flag("field-tree-select-#{field_id}.closed")
  530. self # support chaining
  531. end
  532. def select_tags_options(labels, **find_options)
  533. element.click
  534. wait_for_test_flag("field-tags-#{field_id}.opened")
  535. raise 'Field does not support multiple selection' if !multi_select?
  536. labels.each do |label|
  537. select_option_by_label(label, **find_options)
  538. end
  539. send_keys(:escape)
  540. wait_for_test_flag("field-tags-#{field_id}.closed")
  541. self # support chaining
  542. end
  543. def select_autocomplete_options(labels, **find_options)
  544. element.click
  545. wait_for_test_flag("field-auto-complete-#{field_id}.opened")
  546. raise 'Field does not support multiple selection' if !multi_select?
  547. labels.each do |label|
  548. select_option_by_label(label, **find_options)
  549. end
  550. send_keys(:escape)
  551. wait_for_test_flag("field-auto-complete-#{field_id}.closed")
  552. self # support chaining
  553. end
  554. # If a GraphQL filename is passed, we will explicitly wait for it here.
  555. # Otherwise, we will implicitly wait for a query depending on the type of the field.
  556. # If no waits are to be done, we display a friendly warning to devs, since this can lead to some instability.
  557. # In form context, expected response number will be automatically increased and tracked.
  558. def wait_for_autocomplete_gql(gql_filename, gql_number)
  559. gql_number = autocomplete_gql_number(gql_filename) || gql_number
  560. if gql_filename.present?
  561. wait_for_gql(gql_filename, number: gql_number)
  562. elsif type_customer?
  563. wait_for_gql('shared/components/Form/fields/FieldCustomer/graphql/queries/autocompleteSearch/user.graphql', number: gql_number)
  564. elsif type_organization?
  565. wait_for_gql('shared/components/Form/fields/FieldOrganization/graphql/queries/autocompleteSearch/organization.graphql', number: gql_number)
  566. elsif type_recipient?
  567. wait_for_gql('shared/components/Form/fields/FieldRecipient/graphql/queries/autocompleteSearch/recipient.graphql', number: gql_number)
  568. elsif type_externalDataSource?
  569. wait_for_gql('shared/components/Form/fields/FieldExternalDataSource/graphql/queries/autocompleteSearchObjectAttributeExternalDataSource.graphql', number: gql_number)
  570. elsif type_tags?
  571. # NB: tags autocomplete query fires only once?!
  572. wait_for_gql('shared/entities/tags/graphql/queries/autocompleteTags.graphql', number: 1, skip_clearing: true)
  573. else
  574. warn 'Warning: missing `wait_for_gql` in `search_for_autocomplete_option()`, might lead to instability'
  575. end
  576. end
  577. def autocomplete_gql_number(gql_filename)
  578. return nil if form_context.nil?
  579. return form_context.form_gql_number(:autocomplete) if gql_filename.present?
  580. return form_context.form_gql_number(:customer) if type_customer?
  581. return form_context.form_gql_number(:organization) if type_organization?
  582. return form_context.form_gql_number(:recipient) if type_recipient?
  583. return form_context.form_gql_number(:externalDataSource) if type_externalDataSource?
  584. form_context.form_gql_number(:tags) if type_tags?
  585. end
  586. def triggers_form_updater?
  587. element['data-triggers-form-updater'] == 'true'
  588. end
  589. def maybe_wait_for_form_updater
  590. return if form_context.nil? || !triggers_form_updater?
  591. gql_number = form_context.form_gql_number(:form_updater)
  592. wait_for_form_updater(gql_number)
  593. end
  594. # Click on the upper left corner of the date picker field to close it.
  595. def close_date_picker(element)
  596. element_width = element.native.size.width.to_i
  597. element_height = element.native.size.height.to_i
  598. element.click(x: -element_width / 2, y: -element_height / 2)
  599. end
  600. def clear_date
  601. input_element.send_keys(:backspace)
  602. maybe_wait_for_form_updater
  603. self # support chaining
  604. end
  605. end
  606. class ZammadFormContext
  607. attr_reader :context
  608. def initialize
  609. @context = {}
  610. end
  611. def init_form_updater(number)
  612. context[:gql_number] = {}
  613. context[:gql_number][:form_updater] = number
  614. end
  615. def form_gql_number(name)
  616. if context[:gql_number].nil?
  617. context[:gql_number] = {}
  618. end
  619. if context[:gql_number][name].nil?
  620. context[:gql_number][name] = 1
  621. else
  622. context[:gql_number][name] += 1
  623. end
  624. context[:gql_number][name]
  625. end
  626. end
  627. RSpec.configure do |config|
  628. config.include FormHelpers, type: :system, app: :mobile
  629. config.include FormHelpers, type: :system, app: :desktop_view
  630. end