Skip to content

Commit

Permalink
Feature: Mobile - Capybara test helpers for form fields in the mobile…
Browse files Browse the repository at this point in the history
… view.
  • Loading branch information
dvuckovic committed Dec 19, 2022
1 parent cbc718a commit 43f37f9
Show file tree
Hide file tree
Showing 21 changed files with 1,570 additions and 97 deletions.
Expand Up @@ -9,6 +9,7 @@ import stopEvent from '@shared/utils/events'
import { onClickOutside, onKeyDown, useVModel } from '@vueuse/core'
import type { Ref } from 'vue'
import { computed, nextTick, ref } from 'vue'
import testFlags from '@shared/utils/testFlags'
import CommonSelectItem from './CommonSelectItem.vue'
export interface Props {
Expand Down Expand Up @@ -63,6 +64,10 @@ const openDialog = () => {
)
const focusElement = selected || focusableElements[0]
focusElement?.focus()
nextTick(() => {
testFlags.set('common-select.opened')
})
})
}
Expand All @@ -72,6 +77,10 @@ const closeDialog = () => {
if (!props.noRefocus) {
nextTick(() => lastFocusableOutsideElement?.focus())
}
nextTick(() => {
testFlags.set('common-select.closed')
})
}
defineExpose({
Expand Down Expand Up @@ -139,14 +148,17 @@ const select = (option: SelectOption) => {
closeDialog()
}
}
const duration = VITE_TEST_MODE ? undefined : { enter: 300, leave: 200 }
</script>

<template>
<slot :open="openDialog" :close="closeDialog" />
<Teleport to="body">
<Transition :duration="{ enter: 300, leave: 200 }">
<Transition :duration="duration">
<div
v-if="showDialog"
id="common-select"
class="fixed inset-0 z-10 flex overflow-y-auto"
:aria-label="$t('Dialog window with selections')"
role="dialog"
Expand Down
4 changes: 2 additions & 2 deletions app/frontend/apps/mobile/pages/ticket/views/TicketCreate.vue
Expand Up @@ -128,14 +128,14 @@ const ticketTitleSection = getFormSchemaGroupSection(
object: EnumObjectManagerObjects.Ticket,
screen: 'create_top',
outerClass:
'$reset w-full grow justify-center flex items-center flex-col',
'$reset formkit-outer w-full grow justify-center flex items-center flex-col',
wrapperClass: '$reset flex w-full',
labelClass: '$reset sr-only',
blockClass: '$reset flex w-full',
innerClass: '$reset flex justify-center items-center px-8 w-full',
messagesClass: 'pt-2',
inputClass:
'$reset block bg-transparent grow border-b-[0.5px] border-white outline-none text-center text-xl placeholder:text-white placeholder:text-opacity-50',
'$reset formkit-input block bg-transparent grow border-b-[0.5px] border-white outline-none text-center text-xl placeholder:text-white placeholder:text-opacity-50',
props: {
placeholder: __('Title'),
onSubmit,
Expand Down
Expand Up @@ -280,6 +280,7 @@ useTraverseOptions(autocompleteList)
:aria-label="$t('Select…')"
class="flex grow flex-col items-start self-stretch overflow-y-auto"
role="listbox"
:aria-multiselectable="context.multiple"
>
<div
v-for="(option, index) in filter || context.defaultFilter
Expand Down
Expand Up @@ -14,10 +14,12 @@ import {
watch,
watchEffect,
computed,
nextTick,
} from 'vue'
import { useEventListener } from '@vueuse/core'
import type { RouteLocationRaw } from 'vue-router'
import { useRawHTMLIcon } from '@shared/components/CommonIcon'
import testFlags from '@shared/utils/testFlags'
import type { FormFieldContext } from '../../types/field'
import useValue from '../../composables/useValue'
Expand Down Expand Up @@ -194,6 +196,7 @@ const createFlatpickr = () => {
weekNumbers: application.config.datepicker_show_calendar_weeks === true,
prevArrow: iconPrevArrow,
nextArrow: iconNextArrow,
animate: !VITE_TEST_MODE,
formatDate(date) {
const isoDate = date.toISOString()
if (time.value) return i18n.dateTime(isoDate)
Expand Down Expand Up @@ -278,6 +281,10 @@ watchEffect(async () => {
todayButton?.setAttribute('tabindex', '-1')
calendar.setAttribute('aria-hidden', 'true')
calendar.style.height = '0px'
nextTick(() => {
testFlags.set(`field-date-time-${props.context.id}.opened`)
})
} else {
if (!flatpickrHeight) {
// if form was initially rendered as hidden, the height will be 0
Expand All @@ -287,6 +294,10 @@ watchEffect(async () => {
calendar.removeAttribute('aria-hidden')
clearButton?.removeAttribute('tabindex')
todayButton?.removeAttribute('tabindex')
nextTick(() => {
testFlags.set(`field-date-time-${props.context.id}.closed`)
})
}
})
Expand Down Expand Up @@ -340,10 +351,13 @@ span.flatpickr-weekday {
.flatpickr-calendar {
box-shadow: none;
transition: height 0.5s;
overflow: hidden;
}
.flatpickr-calendar.animate {
transition: height 0.5s;
}
.flatpickr-calendar:not([aria-hidden]) {
@apply mb-2;
}
Expand Down
17 changes: 1 addition & 16 deletions app/frontend/shared/components/Form/fields/FieldEditor/index.ts
@@ -1,29 +1,14 @@
// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/

import type { FormKitNode } from '@formkit/core'
import createInput from '@shared/form/core/createInput'
import formUpdaterTrigger from '@shared/form/features/formUpdaterTrigger'
import extendSchemaDefinition from '@shared/form/utils/extendSchemaDefinition'
import FieldEditorWrapper from './FieldEditorWrapper.vue'

const addAriaLabel = (node: FormKitNode) => {
const { props } = node

// Specification doesn't allow accessing non-labeled elements, which Editor is (<div />)
// (https://html.spec.whatwg.org/multipage/forms.html#category-label)
// So, editor has `aria-labelledby` attribute and a label with the same ID
extendSchemaDefinition(node, 'label', {
attrs: {
id: props.id,
},
})
}

const fieldDefinition = createInput(
FieldEditorWrapper,
['groupId', 'ticketId', 'customerId', 'meta'],
{
features: [addAriaLabel, formUpdaterTrigger('delayed')],
features: [formUpdaterTrigger('delayed')],
},
)

Expand Down
Expand Up @@ -178,6 +178,7 @@ onMounted(() => {
class="flex h-[58px] cursor-pointer items-center self-stretch py-5 px-4 text-base leading-[19px] text-white focus:bg-blue-highlight focus:outline-none"
tabindex="0"
role="button"
:aria-label="$t('Back to previous page')"
@click="goToPreviousPage()"
@keypress.space.prevent="goToPreviousPage()"
>
Expand Down
108 changes: 108 additions & 0 deletions doc/developer_manual/cookbook/how-to-test-with-rspec-and-capybara.md
Expand Up @@ -177,3 +177,111 @@ Example usage: `find :active_ticket_article, 123`
`find` also allows to manually check elements before returning

`find('.popular_class') { |elem| process(elem) }`

### Form helpers for the new stack

#### Finding fields

FormKit-based fields have a custom implementation, so a number of helpers is provided to make it easier to find them via their labels:

```ruby
find_input('Title')
find_select('Owner')
find_treeselect('Category')
find_autocomplete('Customer')
find_editor('Text')
find_datepicker('Pending till')
```

Radio fields do not have textual labels, so they can be found via their identifiers instead:

```ruby
find_radio('articleSenderType')
```

In case of ambiguous labels, make sure to pass `exact_text` option:

```ruby
find_datepicker(nil, exact_text: 'Date')
```

### Executing actions on fields

Returned form field elements have some special syntactic sugar that provide actions depending on the type of the field:

```ruby
find_input('Title').type('Foo Bar')
find_editor('Text').type('Lorem ipsum dolor sit amet.')

find_radio('articleSenderType').select_choice('Outbound Call')

find_datepicker('Date Picker').select_date(Date.tomorrow)
find_datepicker('Pending till').select_datetime('2023-01-01T09:00:00.000Z')
find_datepicker('Date').type_date(Date.today)
find_datepicker('Date Time').type_datetime(DateTime.now)

find_select('Owner').select_option('Test Admin Agent')
find_select('Multi Select').select_options(['Option 1', 'Option 2'])
find_treeselect('Tree Select').select_option('Parent 1::Option A')
find_treeselect('Multi Tree Select').select_options(['Parent 1::Option A', 'Parent 2::Option C'])

find_treeselect('Tree Select').search_for_option('Parent 1::Option A')
find_autocomplete('Customer').search_for_option(customer.lastname)
find_autocomplete('Tags').search_for_options([tag_1, tag_2, tag_3])

find_toggle('Boolean').toggle
find_toggle('Boolean').toggle_on
find_toggle('Boolean').toggle_off
```

To wait for a custom GraphQL response in autocomplete fields, you can provide expected `gql_filename` and/or `gql_number` arguments:

```ruby
find_autocomplete('Custom').search_for_option('foo', gql_filename: 'apps/mobile/entities/user/graphql/queries/user.graphql', gql_number: 4)
```

Clearing selections and input is also possible, if the field supports it:

```ruby
find_select('Select').clear_selection
find_treeselect('Tree Select').clear_selection
find_autocomplete('Auto Complete').clear_selection
find_editor('Text').clear
find_datepicker('Date Picker').clear
```

All custom actions are chainable, in the same way as other Capybara actions:

```ruby
find_treeselect('Tree Select').clear_selection.search_for_option('Option C')
find_autocomplete('Tags').search_for_options([tag_1, tag_2, tag_3]).select_options(%w[foo bar])
```

### Form context

In order to stabilize multiple field interactions, actions can be executed within the same form context:

```ruby
within_form(form_updater_gql_number: 2) do
find_autocomplete('CC').search_for_options([email_address_1, email_address_2])
find_autocomplete('Tags').search_for_options([tag_1, tag_2, tag_3]).select_options(%w[foo bar])
find_editor('Text').type(body)
end
```

Within the same context all form updater responses (Core Workflow) are automatically tracked and waited on, as well as multiple types of GraphQL responses behind the autocomplete fields. To define a custom starting form updater response number, use the `form_updater_gql_number` argument.

### Custom matchers

A number of useful test matchers is also available, including their negated versions:

```ruby
expect(find_select('Select')).to have_selected_option('Option 1')
expect(find_select('Select')).to have_no_selected_option('Option 2')
expect(find_select('Multi Select')).to have_selected_options(['Option 1', 'Option 2'])
expect(find_treeselect('Tree Select')).to have_selected_option_with_parent('Parent 1::Option A')
expect(find_editor('Text')).to have_text('foo bar')
expect(find_datepicker('Date')).to have_date(Date.today)
expect(find_datepicker('Date Time')).to have_datetime(DateTime.now)
expect(find_toggle('Boolean')).to be_toggled_on
```
4 changes: 4 additions & 0 deletions i18n/zammad.pot
Expand Up @@ -1500,6 +1500,10 @@ msgstr ""
msgid "Back to overview"
msgstr ""

#: app/frontend/shared/components/Form/fields/FieldTreeSelect/FieldTreeSelectInputDialog.vue
msgid "Back to previous page"
msgstr ""

#: app/assets/javascripts/app/views/knowledge_base/reader.jst.eco
msgid "Back to search results"
msgstr ""
Expand Down

0 comments on commit 43f37f9

Please sign in to comment.