Browse Source

Maintenance: Mobile - new testing setup

Vladimir Sheremet 2 years ago
parent
commit
65901a8938

+ 1 - 0
.eslintrc.js

@@ -123,6 +123,7 @@ module.exports = {
       ],
       rules: {
         'zammad/zammad-detect-translatable-string': 'off',
+        '@typescript-eslint/no-non-null-assertion': 'off',
       },
     },
   ],

+ 2 - 0
app/frontend/common/components/common/CommonLink.vue

@@ -9,6 +9,7 @@
     v-bind:active-class="activeClass"
     v-bind:exact-active-class="exactActiveClass"
     v-bind:target="target"
+    data-test-id="common-link"
     v-on:click="onClick"
   >
     <slot></slot>
@@ -19,6 +20,7 @@
     v-bind:target="target"
     v-bind:rel="rel"
     v-bind:class="linkClass"
+    data-test-id="common-link"
     v-on:click="onClick"
   >
     <slot></slot>

+ 5 - 1
app/frontend/common/components/common/CommonNotifications.vue

@@ -8,7 +8,11 @@
         enter-class="opacity-0"
         leave-active-class="transition-opacity duration-1000 opacity-0"
       >
-        <div v-for="notification in notifications" v-bind:key="notification.id">
+        <div
+          v-for="notification in notifications"
+          v-bind:key="notification.id"
+          data-test-id="notification"
+        >
           <div class="flex justify-center">
             <div
               class="m-1 flex cursor-pointer items-center rounded py-2 px-4"

+ 22 - 1
app/frontend/common/components/form/field/FieldEditor/FieldEditorInner.vue

@@ -1,13 +1,14 @@
 <!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
 
 <template>
-  <EditorContent v-bind:id="context.id" v-bind:editor="editor" />
+  <EditorContent v-bind:editor="editor" />
 </template>
 
 <script setup lang="ts">
 import type { FormFieldContext } from '@common/types/form'
 import { useEditor, EditorContent } from '@tiptap/vue-3'
 import StarterKit from '@tiptap/starter-kit'
+import { watch } from 'vue'
 
 interface Props {
   context: FormFieldContext
@@ -19,6 +20,12 @@ const props = defineProps<Props>()
 
 const editor = useEditor({
   extensions: [StarterKit],
+  editorProps: {
+    attributes: {
+      role: 'textbox',
+      'aria-labelledby': props.context.id,
+    },
+  },
   // eslint-disable-next-line no-underscore-dangle
   content: props.context._value,
   onUpdate: ({ editor }) => {
@@ -26,6 +33,20 @@ const editor = useEditor({
   },
 })
 
+watch(
+  () => props.context.id,
+  (id) => {
+    editor.value?.setOptions({
+      editorProps: {
+        attributes: {
+          role: 'textbox',
+          'aria-labelledby': id,
+        },
+      },
+    })
+  },
+)
+
 // Set the new editor value, when it was changed from outside (e.G. form schema update).
 props.context.node.on('input', ({ payload: value }) => {
   if (editor.value && value !== editor.value.getHTML()) {

+ 32 - 1
app/frontend/common/components/form/field/FieldEditor/index.ts

@@ -2,8 +2,39 @@
 
 import FieldEditorInner from '@common/components/form/field/FieldEditor/FieldEditorInner.vue'
 import createInput from '@common/form/core/createInput'
+import { FormKitExtendableSchemaRoot, FormKitNode } from '@formkit/core'
+import { cloneDeep } from 'lodash-es'
 
-const fieldDefinition = createInput(FieldEditorInner)
+function addAriaLabel(node: FormKitNode) {
+  const { props } = node
+
+  if (!props.definition) return
+
+  const definition = cloneDeep(props.definition)
+
+  const originalSchema = definition.schema as FormKitExtendableSchemaRoot
+
+  // 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
+  definition.schema = (definition) => {
+    const localDefinition = {
+      ...definition,
+      label: {
+        attrs: {
+          id: props.id,
+        },
+      },
+    }
+    return originalSchema(localDefinition)
+  }
+
+  props.definition = definition
+}
+
+const fieldDefinition = createInput(FieldEditorInner, [], {
+  features: [addAriaLabel],
+})
 
 export default {
   fieldType: 'editor',

+ 1 - 6
app/frontend/common/router/utils/isRouteLink.ts

@@ -7,11 +7,6 @@ export default function isRouteLink(link: Link): boolean {
   if (typeof link === 'object') return true
 
   const router = useRouter()
-  const resolved = router.resolve(link)
 
-  return (
-    resolved !== null &&
-    resolved.matched.length > 0 &&
-    resolved.name !== 'Error'
-  )
+  return router.hasRoute(link)
 }

+ 1 - 1
app/frontend/tests/apps/mobile/components/transition/TransitionViewNavigation.spec.ts

@@ -7,6 +7,6 @@ describe('TransitionViewNavigation.vue', () => {
   it('renders the component', () => {
     const wrapper = getWrapper(TransitionViewNavigation)
 
-    expect(wrapper.exists()).toBe(true)
+    expect(wrapper.container).toBeInTheDocument()
   })
 })

+ 7 - 9
app/frontend/tests/common/components/common/CommonDateTime.spec.ts

@@ -10,7 +10,6 @@ import { nextTick } from 'vue'
 
 describe('CommonDateTime.vue', () => {
   it('renders DateTime', async () => {
-    expect.assertions(5)
     const wrapper = getWrapper(CommonDateTime, {
       props: {
         dateTime: '2020-10-10T10:10:10Z',
@@ -18,22 +17,21 @@ describe('CommonDateTime.vue', () => {
       },
       store: true,
     })
-    expect(wrapper.find('span').text()).toBe('2020-10-10 10:10')
-    wrapper.setProps({ format: 'relative' })
-    await nextTick()
-    expect(wrapper.find('span').text()).toBe('1 day ago')
+    expect(wrapper.container).toHaveTextContent('2020-10-10 10:10')
+    await wrapper.rerender({ format: 'relative' })
+    expect(wrapper.container).toHaveTextContent('1 day ago')
 
-    wrapper.setProps({ format: 'configured' })
+    await wrapper.rerender({ format: 'configured' })
     useApplicationConfigStore().value.pretty_date_format = 'absolute'
     await nextTick()
-    expect(wrapper.find('span').text()).toBe('2020-10-10 10:10')
+    expect(wrapper.container).toHaveTextContent('2020-10-10 10:10')
 
     useApplicationConfigStore().value.pretty_date_format = 'timestamp'
     await nextTick()
-    expect(wrapper.find('span').text()).toBe('2020-10-10 10:10')
+    expect(wrapper.container).toHaveTextContent('2020-10-10 10:10')
 
     useApplicationConfigStore().value.pretty_date_format = 'relative'
     await nextTick()
-    expect(wrapper.find('span').text()).toBe('1 day ago')
+    expect(wrapper.container).toHaveTextContent('1 day ago')
   })
 })

+ 1 - 1
app/frontend/tests/common/components/common/CommonHelloWorld.spec.ts

@@ -19,6 +19,6 @@ describe('CommonHelloWorld.vue', () => {
         },
       },
     })
-    expect(wrapper.text()).toMatch(msg)
+    expect(wrapper.container).toHaveTextContent(msg)
   })
 })

+ 9 - 8
app/frontend/tests/common/components/common/CommonIcon.spec.ts

@@ -8,35 +8,36 @@ describe('CommonIcon.vue', () => {
     const wrapper = getWrapper(CommonIcon, {
       props: { name: 'arrow-left' },
     })
-    expect(wrapper.classes()).toContain('icon')
+    expect(wrapper.getIconByName('arrow-left')).toHaveClass('icon')
   })
   it('renders icon with animation', () => {
     const wrapper = getWrapper(CommonIcon, {
       props: { name: 'cog', animation: 'spin' },
     })
-    expect(wrapper.classes()).toContain('animate-spin')
+    expect(wrapper.getIconByName('cog')).toHaveClass('animate-spin')
   })
   it('renders icon with small size', () => {
     const wrapper = getWrapper(CommonIcon, {
       props: { name: 'cog', size: 'small' },
     })
 
-    expect(wrapper.attributes().width).toEqual('20')
-    expect(wrapper.attributes().height).toEqual('20')
+    expect(wrapper.getIconByName('cog')).toHaveAttribute('width', '20')
+    expect(wrapper.getIconByName('cog')).toHaveAttribute('height', '20')
   })
   it('renders a decorative icon', () => {
     const wrapper = getWrapper(CommonIcon, {
       props: { name: 'cog', decorative: true },
     })
 
-    expect(wrapper.attributes()['aria-hidden']).toEqual('true')
+    expect(wrapper.getIconByName('cog')).toHaveAttribute('aria-hidden', 'true')
   })
-  it('triggers click handler of icon', () => {
+  it('triggers click handler of icon', async () => {
     const wrapper = getWrapper(CommonIcon, {
       props: { name: 'dashboard' },
     })
 
-    wrapper.trigger('click')
-    expect(wrapper.emitted('click')).toHaveLength(1)
+    await wrapper.events.click(wrapper.getIconByName('dashboard'))
+
+    expect(wrapper.emitted().click).toHaveLength(1)
   })
 })

Some files were not shown because too many files changed in this diff