Browse Source

Maintenance: Add create/edit tests and helpers for ticket in new desktop view.

Martin Gruner 3 months ago
parent
commit
b54155478d

+ 5 - 0
.dev/rubocop/default.yml

@@ -418,11 +418,16 @@ Capybara/ClickLinkOrButtonStyle:
   EnforcedStyle: link_or_button
 
 RSpec/ExampleLength:
+  inherit_mode:
+    merge:
+      - Exclude
   CountAsOne:
     - 'array'
     - 'hash'
     - 'heredoc'
   Max: 25
+  Exclude:
+    - "spec/system/**/*.rb"
 
 FactoryBot/ExcessiveCreateList:
   Enabled: false

+ 0 - 5
.dev/rubocop/todo.rspec.yml

@@ -204,11 +204,6 @@ RSpec/ExampleLength:
     - 'spec/requests/ticket_spec.rb'
     - 'spec/requests/user/organization_spec.rb'
     - 'spec/requests/user_spec.rb'
-    - 'spec/system/chat_spec.rb'
-    - 'spec/system/setup/system_spec.rb'
-    - 'spec/system/system/object_manager_spec.rb'
-    - 'spec/system/ticket/update/simultaneously_with_two_user_spec.rb'
-    - 'spec/system/ticket/zoom_spec.rb'
 
 RSpec/ExpectInHook:
   Exclude:

+ 16 - 1
app/frontend/apps/desktop/components/Form/fields/FieldDate/FieldDateTimeInput.vue

@@ -3,13 +3,14 @@
 <script setup lang="ts">
 import VueDatePicker, { type DatePickerInstance } from '@vuepic/vue-datepicker'
 import { storeToRefs } from 'pinia'
-import { computed, ref, toRef } from 'vue'
+import { computed, nextTick, ref, toRef } from 'vue'
 
 import useValue from '#shared/components/Form/composables/useValue.ts'
 import type { DateTimeContext } from '#shared/components/Form/fields/FieldDate/types.ts'
 import { useDateTime } from '#shared/components/Form/fields/FieldDate/useDateTime.ts'
 import { EnumTextDirection } from '#shared/graphql/types.ts'
 import { i18n } from '#shared/i18n.ts'
+import testFlags from '#shared/utils/testFlags.ts'
 
 import { useThemeStore } from '#desktop/stores/theme.ts'
 import '@vuepic/vue-datepicker/dist/main.css'
@@ -61,6 +62,18 @@ const inputIcon = computed(() => {
 const picker = ref<DatePickerInstance>()
 
 const { isDarkMode } = storeToRefs(useThemeStore())
+
+const open = () => {
+  nextTick(() => {
+    testFlags.set('field-date-time.opened')
+  })
+}
+
+const closed = () => {
+  nextTick(() => {
+    testFlags.set('field-date-time.closed')
+  })
+}
 </script>
 
 <template>
@@ -95,6 +108,8 @@ const { isDarkMode } = storeToRefs(useThemeStore())
       :text-input="{ openMenu: 'toggle' }"
       auto-apply
       offset="12"
+      @open="open"
+      @closed="closed"
       @blur="context.handlers.blur"
     >
       <template

+ 4 - 4
i18n/zammad.pot

@@ -2829,7 +2829,7 @@ msgstr ""
 
 #: app/assets/javascripts/app/views/generic/searchable_select.jst.eco:56
 #: app/frontend/apps/desktop/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInput.vue:679
-#: app/frontend/apps/desktop/components/Form/fields/FieldDate/FieldDateTimeInput.vue:138
+#: app/frontend/apps/desktop/components/Form/fields/FieldDate/FieldDateTimeInput.vue:153
 #: app/frontend/apps/desktop/components/Form/fields/FieldSelect/FieldSelectInput.vue:333
 #: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInput.vue:382
 #: app/frontend/apps/mobile/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInput.vue:155
@@ -16244,7 +16244,7 @@ msgstr ""
 msgid "To work on Tickets."
 msgstr ""
 
-#: app/frontend/apps/desktop/components/Form/fields/FieldDate/FieldDateTimeInput.vue:49
+#: app/frontend/apps/desktop/components/Form/fields/FieldDate/FieldDateTimeInput.vue:50
 #: app/frontend/apps/mobile/components/Form/fields/FieldDate/FieldDateTimeInput.vue:109
 #: public/assets/chat/chat-no-jquery.coffee:215
 #: public/assets/chat/chat.coffee:213
@@ -18208,7 +18208,7 @@ msgstr ""
 msgid "at least one letter is required"
 msgstr ""
 
-#: app/assets/javascripts/app/lib/app_post/utils.coffee:1054
+#: app/assets/javascripts/app/lib/app_post/utils.coffee:1058
 #: app/frontend/shared/composables/form/useCheckBodyAttachmentReference.ts:9
 msgid "attachment,attached,enclosed,enclosure"
 msgstr ""
@@ -18739,7 +18739,7 @@ msgid "in process"
 msgstr ""
 
 #: app/assets/javascripts/app/controllers/_ui_element/active.coffee:5
-#: app/assets/javascripts/app/lib/app_post/utils.coffee:1429
+#: app/assets/javascripts/app/lib/app_post/utils.coffee:1433
 #: app/assets/javascripts/app/models/webhook.coffee:34
 #: app/assets/javascripts/app/views/generic/object_search/item_object.jst.eco:16
 #: app/assets/javascripts/app/views/generic/object_search/item_organization.jst.eco:4

+ 164 - 53
spec/support/capybara/form_helpers.rb

@@ -118,22 +118,31 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
   #   # To select an autocomplete option with a different text than the query, provide an optional `label` parameter.
   #   find autocomplete('Customer').search_for_option(customer.email, label: customer.fullname)
   #
-  def search_for_option(query, label: query, gql_filename: '', gql_number: 1, **find_options)
+  def search_for_option(query, label: query, gql_filename: '', gql_number: 1, use_action: false, **find_options)
     return search_for_options(query, gql_filename: gql_filename, gql_number: gql_number, **find_options) if query.is_a?(Array)
     return search_for_tags_option(query, gql_filename: gql_filename, gql_number: gql_number) if type_tags?
-    return search_for_autocomplete_option(query, label: label, gql_filename: gql_filename, gql_number: gql_number, **find_options) if autocomplete?
+    return search_for_autocomplete_option(query, label: label, gql_filename: gql_filename, gql_number: gql_number, use_action: use_action, **find_options) if autocomplete?
 
     raise 'Field does not support searching for options' if !type_treeselect?
 
     element.click
 
-    wait_for_test_flag("field-tree-select-#{field_id}.opened")
+    wait_until_opened
 
     # calculate before closing, since we cannot access it, if dialog is closed
     is_multi_select = multi_select?
 
     browse_for_option(query, **find_options) do |option|
-      find('[role="searchbox"]').fill_in with: option
+
+      # Filter input in desktop view is part of the element, not the dropdown/dialog.
+      searchbox = if desktop_view?
+                    element.find('[role="searchbox"]')
+                  else
+                    find('[role="searchbox"]')
+                  end
+
+      searchbox.fill_in with: option
+
       find('[role="option"]', text: option, **find_options).click
 
       maybe_wait_for_form_updater
@@ -141,7 +150,7 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
 
     send_keys(:escape) if is_multi_select
 
-    wait_for_test_flag("field-tree-select-#{field_id}.closed")
+    wait_until_closed
 
     self # support chaining
   end
@@ -157,9 +166,9 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
   #   # To wait for a custom GraphQL response, you can provide expected `gql_filename` and/or `gql_number`.
   #   find_autocomplete('Tags').search_for_option('foo', gql_number: 3)
   #
-  def search_for_options(queries, labels: queries, gql_filename: '', gql_number: 1, **find_options)
+  def search_for_options(queries, labels: queries, gql_filename: '', gql_number: 1, use_action: false, **find_options)
     return search_for_tags_options(queries, gql_filename: gql_filename, gql_number: gql_number) if type_tags?
-    return search_for_autocomplete_options(queries, labels: labels, gql_filename: gql_filename, gql_number: gql_number, **find_options) if autocomplete?
+    return search_for_autocomplete_options(queries, labels: labels, gql_filename: gql_filename, gql_number: gql_number, use_action: use_action, **find_options) if autocomplete?
 
     raise 'Field does not support searching for options' if !type_treeselect?
 
@@ -171,7 +180,15 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
 
     queries.each do |query|
       browse_for_option(query, **find_options) do |option, rewind|
-        find('[role="searchbox"]').fill_in with: option
+        # Filter input in desktop view is part of the element, not the dropdown/dialog.
+        searchbox = if desktop_view?
+                      element.find('[role="searchbox"]')
+                    else
+                      find('[role="searchbox"]')
+                    end
+
+        searchbox.fill_in with: option
+
         find('[role="option"]', text: option, **find_options).click
 
         maybe_wait_for_form_updater
@@ -187,7 +204,7 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
     self # support chaining
   end
 
-  # Selects an option in select, treeselect nad autocomplete fields via its label.
+  # Selects an option in select, treeselect and autocomplete fields via its label.
   #   NOTE: The option must be part of initial options provided by the field, no autocomplete search will occur.
   #
   # Usage:
@@ -319,7 +336,7 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
 
     element.click
 
-    wait_for_test_flag("field-date-time-#{field_id}.opened")
+    wait_until_opened
 
     date = Date.parse(date) if !date.is_a?(Date) && !date.is_a?(DateTime) && !date.is_a?(Time)
 
@@ -331,11 +348,16 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
     id = date.strftime('%Y-%m-%d')
     element.find_by_id(id).click # rubocop:disable Rails/DynamicFindBy
 
+    if desktop_view?
+      element.click
+      wait_until_opened
+    end
+
     yield if block_given?
 
     # close_date_picker(element)
 
-    # wait_for_test_flag("field-date-time-#{field_id}.closed")
+    # wait_until_closed
 
     maybe_wait_for_form_updater
 
@@ -473,6 +495,23 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
     self # support chaining
   end
 
+  def menu_element
+    return dropdown_element if desktop_view?
+
+    dialog_element
+  end
+
+  # Dropdowns are teleported to the root element, so we must search them within the document body.
+  #   In order to improve the test performance, we don't do any implicit waits here.
+  #   Instead, we do explicit waits when opening/closing dropdowns within the actions.
+  def dropdown_element
+    if type_select? || type_tags? || autocomplete?
+      page.find('#common-select > [role="menu"]', wait: false)
+    elsif type_treeselect?
+      page.find('#field-tree-select-input-dropdown > [role="menu"]', wait: false)
+    end
+  end
+
   # Dialogs are teleported to the root element, so we must search them within the document body.
   #   In order to improve the test performance, we don't do any implicit waits here.
   #   Instead, we do explicit waits when opening/closing dialogs within the actions.
@@ -518,27 +557,45 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
   end
 
   def wait_until_opened
+    return wait_until_opened_desktop_view if desktop_view?
     return wait_for_test_flag('common-select.opened') if type_select?
     return wait_for_test_flag("field-tree-select-#{field_id}.opened") if type_treeselect?
-    return wait_for_test_flag("field-date-time-#{field_id}.opened") if type_date? || !type_datetime
+    return wait_for_test_flag("field-date-time-#{field_id}.opened") if type_date? || type_datetime?
     return wait_for_test_flag("field-tags-#{field_id}.opened") if type_tags?
     return wait_for_test_flag("field-auto-complete-#{field_id}.opened") if autocomplete?
 
-    raise 'Element cannot be opened'
+    raise "Couldn't detect if element was opened"
+  end
+
+  def wait_until_opened_desktop_view
+    return wait_for_test_flag('common-select.opened') if type_select? || type_tags? || autocomplete?
+    return wait_for_test_flag('field-tree-select-input-dropdown.opened') if type_treeselect?
+    return wait_for_test_flag('field-date-time.opened') if type_date? || type_datetime?
+
+    raise "Couldn't detect if element was opened"
   end
 
   def wait_until_closed
+    return wait_until_closed_desktop_view if desktop_view?
     return wait_for_test_flag('common-select.closed') if type_select?
     return wait_for_test_flag("field-tree-select-#{field_id}.closed") if type_treeselect?
-    return wait_for_test_flag("field-date-time-#{field_id}.closed") if type_date? || !type_datetime
+    return wait_for_test_flag("field-date-time-#{field_id}.closed") if type_date? || type_datetime?
     return wait_for_test_flag("field-tags-#{field_id}.closed") if type_tags?
     return wait_for_test_flag("field-auto-complete-#{field_id}.closed") if autocomplete?
 
-    raise 'Element cannot be closed'
+    raise "Couldn't detect if element was closed"
+  end
+
+  def wait_until_closed_desktop_view
+    return wait_for_test_flag('common-select.closed') if type_select? || type_tags? || autocomplete?
+    return wait_for_test_flag('field-tree-select-input-dropdown.closed') if type_treeselect?
+    return wait_for_test_flag('field-date-time.closed') if type_date? || type_datetime?
+
+    raise "Couldn't detect if element was closed"
   end
 
   def select_option_by_label(label, **find_options)
-    within dialog_element do
+    within menu_element do
       find('[role="option"]', text: label, **find_options).click
 
       maybe_wait_for_form_updater
@@ -556,11 +613,17 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
       end
     end
 
+    child_menu_button = if desktop_view?
+                          'div[role="button"]'
+                        else
+                          'svg[role=link]'
+                        end
+
     components.each_with_index do |option, index|
 
       # Child option is always the last item.
       if index == components.size - 1
-        within dialog_element do
+        within menu_element do
           yield option, rewind
         end
 
@@ -568,8 +631,8 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
       end
 
       # Parents come before.
-      within dialog_element do
-        find('[role="option"] span', text: option, **find_options).sibling('svg[role=link]').click
+      within menu_element do
+        find('[role="option"] span', text: option, **find_options).sibling(child_menu_button).click
       end
     end
   end
@@ -577,10 +640,17 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
   def search_for_tags_option(query, gql_filename: '', gql_number: 1)
     element.click
 
-    wait_for_test_flag("field-tags-#{field_id}.opened")
+    wait_until_opened
+
+    within menu_element do
+      # Filter input in desktop view is part of the element, not the dropdown/dialog.
+      searchbox = if desktop_view?
+                    element.find('[role="searchbox"]')
+                  else
+                    find('[role="searchbox"]')
+                  end
 
-    within dialog_element do
-      find('[role="searchbox"]').fill_in with: query
+      searchbox.fill_in with: query
 
       send_keys(:tab)
 
@@ -589,36 +659,47 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
 
     send_keys(:escape)
 
-    wait_for_test_flag("field-tags-#{field_id}.closed")
+    wait_until_closed
 
     maybe_wait_for_form_updater
 
     self # support chaining
   end
 
-  def search_for_autocomplete_option(query, label: query, gql_filename: '', gql_number: 1, already_open: false, **find_options)
+  def search_for_autocomplete_option(query, label: query, gql_filename: '', gql_number: 1, already_open: false, use_action: false, **find_options)
     if !already_open
       element.click
 
-      wait_for_test_flag("field-auto-complete-#{field_id}.opened")
+      wait_until_opened
     end
 
     # calculate before closing, since we cannot access it, if dialog is closed
     is_multi_select = multi_select?
 
-    within dialog_element do
-      find('[role="searchbox"]').fill_in with: query
+    within menu_element do
+      # Filter input in desktop view is part of the element, not the dropdown/dialog.
+      searchbox = if desktop_view?
+                    element.find('[role="searchbox"]')
+                  else
+                    find('[role="searchbox"]')
+                  end
+
+      searchbox.fill_in with: query
 
       wait_for_autocomplete_gql(gql_filename, gql_number)
 
-      find('[role="option"]', text: label, **find_options).click
+      if use_action
+        find('[role="button"]', **find_options).click
+      else
+        find('[role="option"]', text: label, **find_options).click
+      end
 
       maybe_wait_for_form_updater
     end
 
     send_keys(:escape) if is_multi_select
 
-    wait_for_test_flag("field-auto-complete-#{field_id}.closed")
+    wait_until_closed
 
     self # support chaining
   end
@@ -626,13 +707,20 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
   def search_for_tags_options(queries, gql_filename: '', gql_number: 1)
     element.click
 
-    wait_for_test_flag("field-tags-#{field_id}.opened")
+    wait_until_opened
 
     raise 'Field does not support multiple selection' if !multi_select?
 
-    within dialog_element do
+    within menu_element do
       queries.each do |query|
-        find('[role="searchbox"]').fill_in with: query
+        # Filter input in desktop view is part of the element, not the dropdown/dialog.
+        searchbox = if desktop_view?
+                      element.find('[role="searchbox"]')
+                    else
+                      find('[role="searchbox"]')
+                    end
+
+        searchbox.fill_in with: query
 
         send_keys(:tab)
 
@@ -642,37 +730,51 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
 
     send_keys(:escape)
 
-    wait_for_test_flag("field-tags-#{field_id}.closed")
+    wait_until_closed
 
     maybe_wait_for_form_updater
 
     self # support chaining
   end
 
-  def search_for_autocomplete_options(queries, labels: queries, gql_filename: '', gql_number: 1, **find_options)
+  def search_for_autocomplete_options(queries, labels: queries, gql_filename: '', gql_number: 1, use_action: false, **find_options)
     element.click
 
-    wait_for_test_flag("field-auto-complete-#{field_id}.opened")
+    wait_until_opened
 
-    within dialog_element do
+    within menu_element do
       queries.each_with_index do |query, index|
-        find('[role="searchbox"]').fill_in with: query
+
+        # Filter input in desktop view is part of the element, not the dropdown/dialog.
+        searchbox = if desktop_view?
+                      element.find('[role="searchbox"]')
+                    else
+                      find('[role="searchbox"]')
+                    end
+
+        searchbox.fill_in with: query
 
         wait_for_autocomplete_gql(gql_filename, gql_number + index)
 
         raise 'Field does not support multiple selection' if !multi_select?
 
-        find('[role="option"]', text: labels[index], **find_options).click
+        if use_action
+          find('[role="button"]', text: 'add new email address', **find_options).click
+        else
+          find('[role="option"]', text: labels[index], **find_options).click
+        end
 
         maybe_wait_for_form_updater
 
-        find('[aria-label="Clear Search"]').click
+        if !use_action
+          find('[aria-label="Clear Search"]').click
+        end
       end
     end
 
     send_keys(:escape)
 
-    wait_for_test_flag("field-auto-complete-#{field_id}.closed")
+    wait_until_closed
 
     self # support chaining
   end
@@ -680,7 +782,7 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
   def select_treeselect_option(label, **find_options)
     element.click
 
-    wait_for_test_flag("field-tree-select-#{field_id}.opened")
+    wait_until_opened
 
     # calculate before closing, since we cannot access it, if dialog is closed
     is_multi_select = multi_select?
@@ -693,7 +795,7 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
 
     send_keys(:escape) if is_multi_select
 
-    wait_for_test_flag("field-tree-select-#{field_id}.closed")
+    wait_until_closed
 
     self # support chaining
   end
@@ -701,13 +803,13 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
   def select_tags_option(label, **find_options)
     element.click
 
-    wait_for_test_flag("field-tags-#{field_id}.opened")
+    wait_until_opened
 
     select_option_by_label(label, **find_options)
 
     send_keys(:escape)
 
-    wait_for_test_flag("field-tags-#{field_id}.closed")
+    wait_until_closed
 
     self # support chaining
   end
@@ -715,7 +817,7 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
   def select_autocomplete_option(label, **find_options)
     element.click
 
-    wait_for_test_flag("field-auto-complete-#{field_id}.opened")
+    wait_until_opened
 
     # calculate before closing, since we cannot access it, if dialog is closed
     is_multi_select = multi_select?
@@ -724,7 +826,7 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
 
     send_keys(:escape) if is_multi_select
 
-    wait_for_test_flag("field-auto-complete-#{field_id}.closed")
+    wait_until_closed
 
     self # support chaining
   end
@@ -732,7 +834,7 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
   def select_treeselect_options(labels, **find_options)
     element.click
 
-    wait_for_test_flag("field-tree-select-#{field_id}.opened")
+    wait_until_opened
 
     raise 'Field does not support multiple selection' if !multi_select?
 
@@ -748,7 +850,7 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
 
     send_keys(:escape)
 
-    wait_for_test_flag("field-tree-select-#{field_id}.closed")
+    wait_until_closed
 
     self # support chaining
   end
@@ -756,7 +858,7 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
   def select_tags_options(labels, **find_options)
     element.click
 
-    wait_for_test_flag("field-tags-#{field_id}.opened")
+    wait_until_opened
 
     raise 'Field does not support multiple selection' if !multi_select?
 
@@ -766,7 +868,7 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
 
     send_keys(:escape)
 
-    wait_for_test_flag("field-tags-#{field_id}.closed")
+    wait_until_closed
 
     self # support chaining
   end
@@ -774,7 +876,7 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
   def select_autocomplete_options(labels, **find_options)
     element.click
 
-    wait_for_test_flag("field-auto-complete-#{field_id}.opened")
+    wait_until_opened
 
     raise 'Field does not support multiple selection' if !multi_select?
 
@@ -784,7 +886,7 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
 
     send_keys(:escape)
 
-    wait_for_test_flag("field-auto-complete-#{field_id}.closed")
+    wait_until_closed
 
     self # support chaining
   end
@@ -799,7 +901,12 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
     if gql_filename.present?
       wait_for_gql(gql_filename, number: gql_number)
     elsif type_customer?
-      wait_for_gql('shared/components/Form/fields/FieldCustomer/graphql/queries/autocompleteSearch/user.graphql', number: gql_number)
+      query_name = if desktop_view?
+                     'shared/components/Form/fields/FieldCustomer/graphql/queries/autocompleteSearch/generic.graphql'
+                   else
+                     'shared/components/Form/fields/FieldCustomer/graphql/queries/autocompleteSearch/user.graphql'
+                   end
+      wait_for_gql(query_name, number: gql_number)
     elsif type_organization?
       wait_for_gql('shared/components/Form/fields/FieldOrganization/graphql/queries/autocompleteSearch/organization.graphql', number: gql_number)
     elsif type_recipient?
@@ -852,6 +959,10 @@ class ZammadFormFieldCapybaraElementDelegator < SimpleDelegator
 
     self # support chaining
   end
+
+  def desktop_view?
+    RSpec.current_example.metadata[:app] == :desktop_view
+  end
 end
 
 class ZammadFormContext

+ 297 - 0
spec/system/apps/desktop/form_helpers_spec.rb

@@ -0,0 +1,297 @@
+# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+require 'rails_helper'
+
+RSpec.describe 'Form helpers', app: :desktop_view, authenticated_as: :agent, db_strategy: :reset, type: :system do
+  let(:group)       { Group.find_by(name: 'Users') }
+  let(:agent)       { create(:agent, groups: [group]) }
+  let(:object_name) { 'Ticket' }
+  let(:screens) do
+    {
+      create_middle: {
+        '-all-' => {
+          shown:    true,
+          required: false,
+        },
+      },
+    }
+  end
+
+  before do
+    visit '/tickets/create'
+    wait_for_form_to_settle('ticket-create')
+  end
+
+  context 'with single select field', authenticated_as: :authenticate do
+    def authenticate
+      create(:object_manager_attribute_select, object_name: object_name, name: 'single_select', display: 'Single Select', screens: screens, additional_data_options: { options: { '1' => 'Option 1', '2' => 'Option 2', '3' => 'Option 3' } })
+
+      ObjectManager::Attribute.migration_execute
+      agent
+    end
+
+    it 'provides test helpers' do
+      el = find_select('Single Select')
+      el.select_option('Option 1')
+      expect(el).to have_selected_option('Option 1')
+      el.clear_selection
+      expect(el).to have_no_selected_option('Option 1')
+    end
+  end
+
+  context 'with multi select field', authenticated_as: :authenticate do
+    def authenticate
+      create(:object_manager_attribute_multiselect, object_name: object_name, name: 'multi_select', display: 'Multi Select', screens: screens, additional_data_options: { options: { '1' => 'Option 1', '2' => 'Option 2', '3' => 'Option 3' } })
+
+      ObjectManager::Attribute.migration_execute
+      agent
+    end
+
+    it 'provides test helpers' do
+      el = find_select('Multi Select')
+      el.select_options(['Option 1', 'Option 2'])
+      expect(el).to have_selected_options(['Option 1', 'Option 2'])
+      el.clear_selection
+      expect(el).to have_no_selected_options(['Option 1', 'Option 2'])
+    end
+  end
+
+  context 'with tree select field', authenticated_as: :authenticate do
+    let(:data_options) do
+      {
+        'options'    => [
+          {
+            'name'     => 'Parent 1',
+            'value'    => '1',
+            'children' => [
+              {
+                'name'  => 'Option A',
+                'value' => '1::a',
+              },
+              {
+                'name'  => 'Option B',
+                'value' => '1::b',
+              },
+            ],
+          },
+          {
+            'name'     => 'Parent 2',
+            'value'    => '2',
+            'children' => [
+              {
+                'name'  => 'Option C',
+                'value' => '2::c'
+              },
+            ],
+          },
+          {
+            'name'  => 'Option 3',
+            'value' => '3'
+          },
+        ],
+        'default'    => '',
+        'null'       => true,
+        'relation'   => '',
+        'maxlength'  => 255,
+        'nulloption' => true,
+      }
+    end
+
+    def authenticate
+      create(:object_manager_attribute_tree_select, object_name: object_name, name: 'tree_select', display: 'Tree Select', screens: screens, additional_data_options: data_options)
+
+      ObjectManager::Attribute.migration_execute
+      agent
+    end
+
+    it 'provides test helpers' do
+      el = find_treeselect('Tree Select')
+      el.select_option('Parent 1::Option A')
+      expect(el).to have_selected_option_with_parent('Parent 1::Option A')
+      el.clear_selection
+      expect(el).to have_no_selected_option_with_parent('Parent 1::Option A')
+      el.search_for_option('Parent 2::Option C')
+      expect(el).to have_selected_option_with_parent('Parent 2::Option C')
+      el.clear_selection.search_for_option('Option C') # chained
+      expect(el).to have_selected_option_with_parent('Parent 2::Option C')
+    end
+  end
+
+  context 'with multi tree select field', authenticated_as: :authenticate do
+    let(:data_options) do
+      {
+        'options'    => [
+          {
+            'name'     => 'Parent 1',
+            'value'    => '1',
+            'children' => [
+              {
+                'name'  => 'Option A',
+                'value' => '1::a',
+              },
+              {
+                'name'  => 'Option B',
+                'value' => '1::b',
+              },
+            ],
+          },
+          {
+            'name'     => 'Parent 2',
+            'value'    => '2',
+            'children' => [
+              {
+                'name'  => 'Option C',
+                'value' => '2::c'
+              },
+            ],
+          },
+          {
+            'name'  => 'Option 3',
+            'value' => '3'
+          },
+        ],
+        'default'    => '',
+        'null'       => true,
+        'relation'   => '',
+        'maxlength'  => 255,
+        'nulloption' => true,
+      }
+    end
+
+    def authenticate
+      create(:object_manager_attribute_multi_tree_select, object_name: object_name, name: 'tree_select', display: 'Multi Tree Select', screens: screens, additional_data_options: data_options)
+
+      ObjectManager::Attribute.migration_execute
+      agent
+    end
+
+    it 'provides test helpers' do
+      el = find_treeselect('Multi Tree Select')
+      el.select_options(['Parent 1::Option A', 'Parent 2::Option C'])
+      expect(el).to have_selected_options_with_parent(['Parent 1::Option A', 'Parent 2::Option C'])
+      el.clear_selection
+      expect(el).to have_no_selected_options_with_parent(['Parent 1::Option A', 'Parent 2::Option C'])
+    end
+  end
+
+  context 'with customer and organization fields' do
+    let(:organization)            { create(:organization) }
+    let(:secondary_organizations) { create_list(:organization, 5) }
+    let!(:customer)               { create(:customer, organization_id: organization.id, organization_ids: secondary_organizations.map(&:id)) }
+
+    it 'provides test helpers' do
+      el = find_autocomplete('Customer')
+      el.search_for_option(customer.email, label: customer.fullname) # search for fullname does not work without ES
+      expect(el).to have_selected_option(customer.fullname)
+
+      el = find_autocomplete('Organization')
+      el.select_option(secondary_organizations.last.name)
+      expect(el).to have_selected_option(secondary_organizations.last.name)
+    end
+  end
+
+  context 'with recipient field' do
+    let(:email_address_1) { Faker::Internet.unique.email }
+    let(:email_address_2) { Faker::Internet.unique.email }
+
+    before do
+      find('[role="tab"]', text: 'Send Email').click
+    end
+
+    it 'provides test helpers' do
+      within_form(form_updater_gql_number: 2) do
+        el = find_autocomplete('CC')
+        el.search_for_options([email_address_1, email_address_2], use_action: true)
+        expect(el).to have_selected_options([email_address_1, email_address_2])
+      end
+    end
+  end
+
+  context 'with tags field', authenticated_as: :authenticate do
+    let(:tag_1) { Faker::Hacker.unique.noun }
+    let(:tag_2) { Faker::Hacker.unique.noun }
+    let(:tag_3) { Faker::Hacker.unique.noun }
+    let(:tags) do
+      [
+        Tag::Item.lookup_by_name_and_create('foo'),
+      ]
+    end
+
+    def authenticate
+      tags
+      agent
+    end
+
+    it 'provides test helpers' do
+      within_form(form_updater_gql_number: 1) do
+        el = find_autocomplete('Tags')
+        el.select_option('foo').search_for_options([tag_1, tag_2, tag_3])
+        expect(el).to have_selected_options(['foo', tag_1, tag_2, tag_3])
+      end
+    end
+  end
+
+  context 'with editor field' do
+    let(:body) { Faker::Hacker.say_something_smart }
+
+    it 'provides test helpers' do
+      within_form(form_updater_gql_number: 1) do
+        el = find_editor('Text')
+        el.type(body)
+        expect(el).to have_data_value(body)
+        el.clear
+        expect(el).to have_no_data_value(body)
+      end
+    end
+  end
+
+  context 'with date and datetime fields', authenticated_as: :authenticate, time_zone: 'Europe/London' do
+    let(:date)     { Date.parse('2022-09-07') }
+    let(:datetime) { DateTime.parse('2023-09-07T08:00:00.000Z') }
+
+    def authenticate
+      create(:object_manager_attribute_date, object_name: object_name, name: 'date', display: 'Date', screens: screens)
+      create(:object_manager_attribute_datetime, object_name: object_name, name: 'datetime', display: 'Date Time', screens: screens)
+
+      ObjectManager::Attribute.migration_execute
+      agent
+    end
+
+    it 'provides test helpers' do
+      el = find_datepicker(nil, exact_text: 'Date')
+      el.select_date(date)
+      expect(el).to have_date(date)
+      el.clear
+      expect(el).to have_no_date(date)
+      el.type_date(date)
+      expect(el).to have_date(date)
+
+      el = find_datepicker('Date Time')
+      el.select_datetime(datetime)
+      expect(el).to have_datetime(datetime)
+      el.clear
+      expect(el).to have_no_datetime(datetime)
+      el.type_datetime(datetime)
+      expect(el).to have_datetime(datetime)
+    end
+  end
+
+  context 'with boolean field', authenticated_as: :authenticate do
+    def authenticate
+      create(:object_manager_attribute_boolean, object_name: object_name, name: 'boolean', display: 'Boolean', screens: screens)
+
+      ObjectManager::Attribute.migration_execute
+      agent
+    end
+
+    it 'provides test helpers' do
+      el = find_toggle('Boolean')
+      el.toggle
+      expect(el).to be_toggled_on
+      el.toggle_off
+      expect(el).to be_toggled_off
+      el.toggle_on
+      expect(el).to be_toggled_on
+    end
+  end
+end

+ 1 - 1
spec/system/apps/desktop/guided_setup_spec.rb

@@ -16,7 +16,7 @@ RSpec.describe 'Desktop > Guided Setup', app: :desktop_view, authenticated_as: f
     Redis.new(driver: :hiredis, url: ENV['REDIS_URL'].presence || 'redis://localhost:6379').del('Zammad::System::Setup')
   end
 
-  it 'Perform the basic system set-up' do # rubocop:disable RSpec/ExampleLength
+  it 'Perform the basic system set-up' do
     visit '/'
 
     click_on 'Set up a new system'

+ 71 - 20
spec/system/apps/desktop/ticket/create_spec.rb

@@ -3,13 +3,75 @@
 require 'rails_helper'
 
 RSpec.describe 'Desktop > Ticket > Create', app: :desktop_view, authenticated_as: :agent, type: :system do
-  let(:agent) { create(:agent, groups: [group]) }
-  let(:group) { create(:group) }
+  let(:agent)         { create(:agent, groups: [group, another_group]) }
+  let(:group)         { create(:group) }
+  let(:another_group) { create(:group) }
+  let(:ticket_type)   { create(:ticket_type) }
+  let(:customer)      { create(:customer, :with_org) }
 
-  context 'when creating a ticket from a template', authenticated_as: :authenticate, db_strategy: :reset do
-    let(:ticket_type) { create(:ticket_type) }
-    let(:customer)    { create(:customer, :with_org) }
+  context 'when creating a ticket' do
+    before do
+      visit '/ticket/create'
+      wait_for_form_to_settle('ticket-create')
+    end
+
+    it 'creates a new ticket' do
+      find('[role="tab"]', text: 'Send Email').click
+
+      within_form(form_updater_gql_number: 2) do
+        expect(page).to have_css('h1', text: 'New Ticket')
+        find_input('Title').type('Example Ticket Title')
+        expect(page).to have_css('h1', text: 'Example Ticket Title')
+
+        find_autocomplete('Customer').search_for_option(customer.email, label: customer.fullname)
+
+        find_autocomplete('CC').search_for_option(Faker::Internet.unique.email, use_action: true)
+
+        text = find_editor('Text')
+        text.type('# ').type('Heading').type(:enter, click: false)
+
+        find('button[aria-label="Format as bold"]').click
+        text.type('Bold Text ', click: false).type(:enter, click: false)
+        find('button[aria-label="Format as bold"]').click
+
+        find('button[aria-label="Format as italic"]').click
+        text.type('Italic Text ', click: false).type(:enter, click: false)
 
+        find('button[aria-label="Add bullet list"]').click
+        text.type('Bullet List ', click: false).type(:enter, click: false).type(:enter, click: false)
+
+        find('button[aria-label="Add ordered list"]').click
+        text.type('Ordered List ', click: false).type(:enter, click: false).type(:enter, click: false)
+
+        find('button[aria-label="Add link"]').click
+
+        # Has to be adjusted as soon as we update to new link implementation
+        prompt = page.driver.browser.switch_to.alert
+        prompt.send_keys('https://zammad.com')
+        prompt.accept
+
+        find_treeselect('Group').search_for_option(another_group.name)
+
+        find_select('Priority').select_option('3 high')
+
+      end
+
+      click_on 'Create'
+
+      expect(page).to have_text('Ticket has been created successfully')
+
+      expect(Ticket.last).to have_attributes(
+        title:    'Example Ticket Title',
+        priority: Ticket::Priority.find_by(name: '3 high'),
+        group:    another_group,
+        customer: customer,
+      )
+
+      expect(Ticket.last.articles.first.body).to eq('<h1>Heading</h1><p><strong>Bold Text </strong><br></p><p><em>Italic Text </em><br></p><ul><li><p>Bullet List </p></li></ul><ol><li><p>Ordered List </p></li></ol><p><a rel="nofollow noreferrer noopener" href="https://zammad.com" target="_blank">https://zammad.com</a><br></p>')
+    end
+  end
+
+  context 'when creating from a template', authenticated_as: :agent, db_strategy: :reset do
     let(:template) do
       create(
         :template,
@@ -25,34 +87,24 @@ RSpec.describe 'Desktop > Ticket > Create', app: :desktop_view, authenticated_as
       end
     end
 
-    def authenticate
-      # NB: Just trying to prove that previous tests are leaving some inconsistent records in the database & cache.
-      Taskbar.destroy_all
-      Rails.cache.clear
-
-      # Activate ticket type field.
+    before do
       ObjectManager::Attribute.get(object: 'Ticket', name: 'type').tap do |oa|
         oa.active = true
         oa.save!
       end
       ObjectManager::Attribute.migration_execute
-
       template
-      agent
-    end
 
-    before do
       visit '/ticket/create'
-      wait_for_subscription_start('userCurrentTaskbarItemStateUpdates')
+      wait_for_form_to_settle('ticket-create')
     end
 
     it 'applies the template correctly' do
-      skip 'Pending fix for https://github.com/zammad/coordination-technical-debt/issues/524'
-
       click_on 'Apply Template'
       click_on template.name
 
-      wait_for_form_updater(3)
+      wait_for_form_updater(2)
+
       wait_for_gql('shared/entities/user/graphql/queries/user.graphql')
 
       expect(page).to have_text(customer.fullname)
@@ -73,5 +125,4 @@ RSpec.describe 'Desktop > Ticket > Create', app: :desktop_view, authenticated_as
       expect(Tag.tag_list(object: 'Ticket', o_id: Ticket.last.id)).to eq(%w[foo bar])
     end
   end
-
 end

+ 145 - 0
spec/system/apps/desktop/ticket/edit_spec.rb

@@ -0,0 +1,145 @@
+# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+require 'rails_helper'
+
+RSpec.describe 'Desktop > Ticket > Edit', app: :desktop_view, authenticated_as: :agent, type: :system do
+  let(:agent)   { create(:agent, password: 'test', groups: [group]) }
+  let(:group)   { create(:group) }
+  let(:article) { create(:ticket_article, :inbound_email, ticket: ticket) }
+  let(:ticket)  { create(:ticket, group:, title: 'Test initial') }
+
+  context 'when editing a ticket', db_strategy: :reset do
+    let(:select_field) { create(:object_manager_attribute_select, :shown_screen, name: 'select_field', display: 'Select field', additional_data_options: { options: { '1' => 'Option 1', '2' => 'Option 2', '3' => 'Option 3' } }) }
+    let(:text_field)   { create(:object_manager_attribute_text, :shown_screen, name: 'text_field', display: 'Text field') }
+
+    let(:hide_field_in_create) do
+      create(:core_workflow,
+             :active_and_screen,
+             object:  'Ticket',
+             screen:  'create_middle',
+             perform: {
+               'ticket.select_field': {
+                 operator: 'hide',
+                 hide:     true
+               },
+             })
+    end
+
+    let(:show_field_in_edit) do
+      create(:core_workflow,
+             :active_and_screen,
+             object:  'Ticket',
+             screen:  'edit',
+             perform: {
+               'ticket.select_field': {
+                 operator:      'set_mandatory',
+                 set_mandatory: true
+               },
+             })
+    end
+
+    before do
+      select_field
+      text_field
+      ObjectManager::Attribute.migration_execute
+      hide_field_in_create
+      show_field_in_edit
+      article
+
+      visit "/ticket/#{ticket.id}"
+      wait_for_form_to_settle('form-ticket-edit')
+    end
+
+    it 'works correctly' do
+
+      #
+      # Ticket attributes
+      #
+      within_form(form_updater_gql_number: 1) do
+        find_input('Text field').type('text content')
+
+        click_on 'Update'
+
+        # Check that select field is mandatory.
+        expect(page).to have_text('This field is required.')
+
+        find_select('Select field').select_option('Option 2')
+      end
+
+      click_on 'Update'
+
+      wait_for_gql('shared/entities/ticket/graphql/mutations/update.graphql', number: 1)
+
+      expect(page).to have_text('Ticket updated successfully')
+      expect(ticket.reload).to have_attributes(select_field: '2', text_field: 'text content')
+
+      #
+      # Tag
+      #
+      click_on 'Add tag'
+      find_autocomplete('Add tag').open.input_element.fill_in(with: 'test_tag').send_keys(:tab)
+      wait_for_gql('shared/entities/tags/graphql/mutations/assignment/add.graphql', number: 1)
+      expect(page).to have_text('Ticket tag added successfully')
+      expect(ticket.tag_list).to include('test_tag')
+
+      #
+      # Title
+      #
+      find('[aria-label="Edit ticket title"]').click
+      send_keys ' changed', :enter
+      wait_for_gql('shared/entities/ticket/graphql/mutations/update.graphql', number: 2)
+      expect(page).to have_text('Ticket updated successfully')
+
+      within 'main' do
+        expect(page).to have_text('Test initial changed')
+      end
+
+      within '#user-taskbar-tabs' do
+        expect(page).to have_text('Test initial changed')
+      end
+
+      #
+      # State
+      #
+      find_select('State').select_option('closed')
+
+      click_on 'Update'
+
+      wait_for_gql('shared/entities/ticket/graphql/mutations/update.graphql', number: 3)
+
+      expect(page).to have_text('Ticket updated successfully')
+      expect(ticket.reload.state.name).to eq('closed')
+
+      within '#user-taskbar-tabs' do
+        expect(page).to have_css("a[href=\"/desktop/tickets/#{ticket.id}\"] svg[aria-label=\"check-circle-outline\"]")
+      end
+
+      #
+      # Reorder taskbar
+      #
+      click_on 'New ticket'
+      expect(page).to have_css('label', text: 'Text field')
+      expect(page).to have_no_css('label', text: 'Select field')
+
+      within '#user-taskbar-tabs' do
+        expect(page).to have_text("Test initial changed\nReceived Call")
+
+        o1 = find('li.draggable', text: 'Test initial changed')
+        o2 = find('li.draggable', text: 'Received Call')
+        o1.drag_to(o2)
+
+        wait_for_gql('apps/desktop/entities/user/current/graphql/mutations/userCurrentTaskbarItemListPrio.graphql')
+
+        expect(page).to have_text("Received Call\nTest initial changed")
+      end
+
+      logout
+
+      login(username: agent.login, password: 'test')
+
+      within '#user-taskbar-tabs' do
+        expect(page).to have_text("Received Call\nTest initial changed")
+      end
+    end
+  end
+end

+ 0 - 0
spec/system/apps/mobile_old/form_helpers_spec.rb → spec/system/apps/mobile/form_helpers_spec.rb