From f290150eb7dd14064e3294adbcdf33955eb4e4b4 Mon Sep 17 00:00:00 2001
From: pkupczyk <piotr.kupczyk@id.ethz.ch>
Date: Mon, 30 Dec 2019 12:44:06 +0100
Subject: [PATCH] SSDM-7583 : ObjectTypeForm - unit tests for more handlers

---
 .../types/objectType/ObjectType.jsx           |   4 +-
 .../objectType/ObjectTypeHandlerRemove.js     |  52 +--
 .../objectType/ObjectTypeHandlerValidate.js   |  11 +-
 .../srcTest/js/common/ComponentState.js       |  12 +-
 .../objectType/ObjectTypeHandlerLoad.test.js  | 225 ++++++++++++
 .../ObjectTypeHandlerOrderChange.test.js      | 134 +++++++
 .../ObjectTypeHandlerRemove.test.js           | 326 ++++++++++++++++++
 .../ObjectTypeHandlerSelectionChange.test.js  |  49 +++
 .../ObjectTypeHandlerValidate.test.js         | 255 ++++++++++++++
 9 files changed, 1039 insertions(+), 29 deletions(-)
 create mode 100644 openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerLoad.test.js
 create mode 100644 openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerOrderChange.test.js
 create mode 100644 openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerRemove.test.js
 create mode 100644 openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerSelectionChange.test.js
 create mode 100644 openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerValidate.test.js

diff --git a/openbis_ng_ui/src/js/components/types/objectType/ObjectType.jsx b/openbis_ng_ui/src/js/components/types/objectType/ObjectType.jsx
index 679e93880a8..71bcb46cee3 100644
--- a/openbis_ng_ui/src/js/components/types/objectType/ObjectType.jsx
+++ b/openbis_ng_ui/src/js/components/types/objectType/ObjectType.jsx
@@ -140,14 +140,14 @@ class ObjectType extends React.PureComponent {
     new ObjectTypeHandlerRemove(
       this.state,
       this.setState.bind(this)
-    ).executeRemoveConfirm()
+    ).executeRemove(true)
   }
 
   handleRemoveCancel() {
     new ObjectTypeHandlerRemove(
       this.state,
       this.setState.bind(this)
-    ).executeRemoveCancel()
+    ).executeCancel()
   }
 
   handleSave() {
diff --git a/openbis_ng_ui/src/js/components/types/objectType/ObjectTypeHandlerRemove.js b/openbis_ng_ui/src/js/components/types/objectType/ObjectTypeHandlerRemove.js
index 3ab191f52e6..4a7be869ab2 100644
--- a/openbis_ng_ui/src/js/components/types/objectType/ObjectTypeHandlerRemove.js
+++ b/openbis_ng_ui/src/js/components/types/objectType/ObjectTypeHandlerRemove.js
@@ -6,37 +6,39 @@ export default class ObjectTypeHandlerRemove {
     this.setState = setState
   }
 
-  executeRemove() {
+  executeRemove(confirmed = false) {
     const { selection } = this.state
     if (selection.type === 'section') {
-      this.handleRemoveSection(selection.params.id)
+      this.handleRemoveSection(selection.params.id, confirmed)
     } else if (selection.type === 'property') {
-      this.handleRemoveProperty(selection.params.id)
+      this.handleRemoveProperty(selection.params.id, confirmed)
     }
   }
 
-  executeRemoveConfirm() {
-    this.setState({
-      removeSectionDialogOpen: false,
-      removePropertyDialogOpen: false
-    })
-    this.executeRemove()
-  }
-
-  executeRemoveCancel() {
-    this.setState({
-      removeSectionDialogOpen: false,
-      removePropertyDialogOpen: false
-    })
+  executeCancel() {
+    const { selection } = this.state
+    if (selection.type === 'section') {
+      this.setState({
+        removeSectionDialogOpen: false
+      })
+    } else if (selection.type === 'property') {
+      this.setState({
+        removePropertyDialogOpen: false
+      })
+    }
   }
 
-  handleRemoveSection(sectionId) {
-    const { sections, properties, removeSectionDialogOpen } = this.state
+  handleRemoveSection(sectionId, confirmed) {
+    const { sections, properties } = this.state
 
     const sectionIndex = sections.findIndex(section => section.id === sectionId)
     const section = sections[sectionIndex]
 
-    if (this.isSectionUsed(section) && !removeSectionDialogOpen) {
+    if (confirmed) {
+      this.setState({
+        removeSectionDialogOpen: false
+      })
+    } else if (this.isSectionUsed(section)) {
       this.setState({
         removeSectionDialogOpen: true
       })
@@ -60,15 +62,19 @@ export default class ObjectTypeHandlerRemove {
     }))
   }
 
-  handleRemoveProperty(propertyId) {
-    const { sections, properties, removePropertyDialogOpen } = this.state
+  handleRemoveProperty(propertyId, confirmed) {
+    const { sections, properties } = this.state
 
     const propertyIndex = properties.findIndex(
       property => property.id === propertyId
     )
     const property = properties[propertyIndex]
 
-    if (this.isPropertyUsed(property) && !removePropertyDialogOpen) {
+    if (confirmed) {
+      this.setState({
+        removePropertyDialogOpen: false
+      })
+    } else if (this.isPropertyUsed(property)) {
       this.setState({
         removePropertyDialogOpen: true
       })
@@ -114,6 +120,6 @@ export default class ObjectTypeHandlerRemove {
   }
 
   isPropertyUsed(property) {
-    return property.usages !== 0
+    return _.isFinite(property.usages) && property.usages !== 0
   }
 }
diff --git a/openbis_ng_ui/src/js/components/types/objectType/ObjectTypeHandlerValidate.js b/openbis_ng_ui/src/js/components/types/objectType/ObjectTypeHandlerValidate.js
index 4a2ad1c4172..1be4cfa73b8 100644
--- a/openbis_ng_ui/src/js/components/types/objectType/ObjectTypeHandlerValidate.js
+++ b/openbis_ng_ui/src/js/components/types/objectType/ObjectTypeHandlerValidate.js
@@ -24,7 +24,7 @@ export default class ObjectTypeHandlerValidate {
     const { validate, type, properties } = this.getState()
 
     if (!validate) {
-      return
+      return true
     }
 
     const [typeErrors, typeErrorsMap] = this.validateType(type)
@@ -66,11 +66,16 @@ export default class ObjectTypeHandlerValidate {
 
       return {
         type: newType,
-        properties: newProperties,
-        selection: errorSelection ? errorSelection : state.selection
+        properties: newProperties
       }
     })
 
+    if (errorSelection) {
+      this.setState({
+        selection: errorSelection
+      })
+    }
+
     return _.isEmpty(typeErrors) && _.isEmpty(propertiesErrors)
   }
 
diff --git a/openbis_ng_ui/srcTest/js/common/ComponentState.js b/openbis_ng_ui/srcTest/js/common/ComponentState.js
index 1996729c244..c4f443c6c9e 100644
--- a/openbis_ng_ui/srcTest/js/common/ComponentState.js
+++ b/openbis_ng_ui/srcTest/js/common/ComponentState.js
@@ -11,8 +11,14 @@ class ComponentState {
     return this.state
   }
 
+  getGetState() {
+    return () => {
+      return this.getState()
+    }
+  }
+
   getSetState() {
-    return newStateOrFunction => {
+    return (newStateOrFunction, callback) => {
       let changes
 
       if (_.isFunction(newStateOrFunction)) {
@@ -22,6 +28,10 @@ class ComponentState {
       }
 
       this.state = { ...this.state, ...changes }
+
+      if (callback) {
+        callback()
+      }
     }
   }
 
diff --git a/openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerLoad.test.js b/openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerLoad.test.js
new file mode 100644
index 00000000000..20961dd41fb
--- /dev/null
+++ b/openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerLoad.test.js
@@ -0,0 +1,225 @@
+import ObjectTypeHandlerLoad from '../../../../../src/js/components/types/objectType/ObjectTypeHandlerLoad.js'
+import ComponentState from '../../../common/ComponentState.js'
+
+describe('ObjectTypeHandlerLoadTest', () => {
+  test('load success', done => {
+    const componentState = new ComponentState({})
+    const facade = {
+      loadType: jest.fn(),
+      loadUsages: jest.fn()
+    }
+
+    facade.loadType.mockReturnValueOnce(
+      Promise.resolve({
+        code: 'TYPE_CODE',
+        description: 'TYPE_DESCRIPTION',
+        listable: true,
+        showContainer: true,
+        showParents: true,
+        showParentMetadata: true,
+        autoGeneratedCode: true,
+        generatedCodePrefix: 'TYPE_CODE_PREFIX',
+        subcodeUnique: true,
+        validationPlugin: { name: 'TYPE_VALIDATION_PLUGIN' },
+        propertyAssignments: [
+          {
+            propertyType: {
+              code: 'PROPERTY_0_CODE',
+              label: 'PROPERTY_0_LABEL',
+              description: 'PROPERTY_0_DESCRIPTION',
+              dataType: 'VARCHAR',
+              schema: null,
+              transformation: null
+            },
+            plugin: { name: 'PROPERTY_0_PLUGIN' },
+            mandatory: true,
+            section: 'SECTION_0',
+            showInEditView: true,
+            showRawValueInForms: true
+          },
+          {
+            propertyType: {
+              code: 'PROPERTY_1_CODE',
+              label: 'PROPERTY_1_LABEL',
+              description: 'PROPERTY_1_DESCRIPTION',
+              dataType: 'CONTROLLED_VOCABULARY',
+              vocabulary: {
+                code: 'PROPERTY_1_VOCABULARY_CODE'
+              },
+              schema: null,
+              transformation: null
+            },
+            plugin: null,
+            mandatory: false,
+            section: 'SECTION_0',
+            showInEditView: false,
+            showRawValueInForms: false
+          },
+          {
+            propertyType: {
+              code: 'PROPERTY_2_CODE',
+              label: 'PROPERTY_2_LABEL',
+              description: 'PROPERTY_2_DESCRIPTION',
+              dataType: 'MATERIAL',
+              materialType: {
+                code: 'PROPERTY_2_MATERIAL_TYPE_CODE'
+              },
+              schema: null,
+              transformation: null
+            },
+            plugin: null,
+            mandatory: false,
+            section: null,
+            showInEditView: false,
+            showRawValueInForms: false
+          }
+        ]
+      })
+    )
+    facade.loadUsages.mockReturnValueOnce(
+      Promise.resolve({
+        type: 3,
+        property: {
+          PROPERTY_0_CODE: 2,
+          PROPERTY_1_CODE: 1
+        }
+      })
+    )
+
+    execute(123, componentState, facade).finally(() => {
+      expect(facade.loadType).toBeCalledWith(123)
+      expect(facade.loadUsages).toBeCalledWith(123)
+
+      const state = componentState.getState()
+
+      delete state.type.original
+      state.properties.forEach(property => {
+        delete property.original
+      })
+
+      componentState.assertState({
+        loading: false,
+        selection: null,
+        sections: [
+          {
+            id: 'section-0',
+            name: 'SECTION_0',
+            properties: ['property-0', 'property-1']
+          },
+          {
+            id: 'section-1',
+            name: null,
+            properties: ['property-2']
+          }
+        ],
+        properties: [
+          {
+            code: 'PROPERTY_0_CODE',
+            dataType: 'VARCHAR',
+            description: 'PROPERTY_0_DESCRIPTION',
+            errors: {},
+            id: 'property-0',
+            label: 'PROPERTY_0_LABEL',
+            mandatory: true,
+            materialType: null,
+            plugin: 'PROPERTY_0_PLUGIN',
+            schema: null,
+            section: 'section-0',
+            showInEditView: true,
+            showRawValueInForms: true,
+            transformation: null,
+            usages: 2,
+            vocabulary: null
+          },
+          {
+            code: 'PROPERTY_1_CODE',
+            dataType: 'CONTROLLED_VOCABULARY',
+            description: 'PROPERTY_1_DESCRIPTION',
+            errors: {},
+            id: 'property-1',
+            label: 'PROPERTY_1_LABEL',
+            mandatory: false,
+            materialType: null,
+            plugin: null,
+            schema: null,
+            section: 'section-0',
+            showInEditView: false,
+            showRawValueInForms: false,
+            transformation: null,
+            usages: 1,
+            vocabulary: 'PROPERTY_1_VOCABULARY_CODE'
+          },
+          {
+            code: 'PROPERTY_2_CODE',
+            dataType: 'MATERIAL',
+            description: 'PROPERTY_2_DESCRIPTION',
+            errors: {},
+            id: 'property-2',
+            label: 'PROPERTY_2_LABEL',
+            mandatory: false,
+            materialType: 'PROPERTY_2_MATERIAL_TYPE_CODE',
+            plugin: null,
+            schema: null,
+            section: 'section-1',
+            showInEditView: false,
+            showRawValueInForms: false,
+            transformation: null,
+            usages: 0,
+            vocabulary: null
+          }
+        ],
+        type: {
+          autoGeneratedCode: true,
+          code: 'TYPE_CODE',
+          description: 'TYPE_DESCRIPTION',
+          errors: {},
+          generatedCodePrefix: 'TYPE_CODE_PREFIX',
+          listable: true,
+          showContainer: true,
+          showParentMetadata: true,
+          showParents: true,
+          subcodeUnique: true,
+          usages: 3,
+          validationPlugin: 'TYPE_VALIDATION_PLUGIN'
+        },
+        propertiesCounter: 3,
+        sectionsCounter: 2,
+        removePropertyDialogOpen: false,
+        removeSectionDialogOpen: false
+      })
+
+      done()
+    })
+  })
+
+  test('load failure', done => {
+    const componentState = new ComponentState({})
+    const facade = {
+      loadType: jest.fn(),
+      loadUsages: jest.fn(),
+      catch: jest.fn()
+    }
+
+    facade.loadType.mockReturnValueOnce(Promise.reject('Server unavailable'))
+    facade.loadUsages.mockReturnValueOnce(Promise.resolve({}))
+
+    execute(123, componentState, facade).finally(() => {
+      expect(facade.loadType).toBeCalledWith(123)
+      expect(facade.loadUsages).toBeCalledWith(123)
+      expect(facade.catch).toBeCalledWith('Server unavailable')
+
+      componentState.assertState({ loading: false })
+
+      done()
+    })
+  })
+})
+
+const execute = (objectId, componentState, facade) => {
+  return new ObjectTypeHandlerLoad(
+    objectId,
+    componentState.getState(),
+    componentState.getSetState(),
+    facade
+  ).execute()
+}
diff --git a/openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerOrderChange.test.js b/openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerOrderChange.test.js
new file mode 100644
index 00000000000..b20e2b7f39b
--- /dev/null
+++ b/openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerOrderChange.test.js
@@ -0,0 +1,134 @@
+import ObjectTypeHandlerOrderChange from '../../../../../src/js/components/types/objectType/ObjectTypeHandlerOrderChange.js'
+import ComponentState from '../../../common/ComponentState.js'
+
+describe('ObjectTypeHandlerOrderChangeTest', () => {
+  test('section', () => {
+    const componentState = new ComponentState({
+      sections: [
+        {
+          id: 'section-0'
+        },
+        {
+          id: 'section-1'
+        },
+        {
+          id: 'section-2'
+        }
+      ]
+    })
+
+    execute(componentState, 'section', {
+      fromIndex: 2,
+      toIndex: 0
+    })
+
+    componentState.assertState({
+      sections: [
+        {
+          id: 'section-2'
+        },
+        {
+          id: 'section-0'
+        },
+        {
+          id: 'section-1'
+        }
+      ]
+    })
+  })
+
+  test('property', () => {
+    const componentState = new ComponentState({
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0', 'property-1']
+        },
+        {
+          id: 'section-1',
+          properties: []
+        },
+        {
+          id: 'section-2',
+          properties: ['property-2', 'property-3', 'property-4']
+        }
+      ],
+      properties: [
+        { id: 'property-0', section: 'section-0' },
+        { id: 'property-1', section: 'section-0' },
+        { id: 'property-2', section: 'section-2' },
+        { id: 'property-3', section: 'section-2' },
+        { id: 'property-4', section: 'section-2' }
+      ]
+    })
+
+    execute(componentState, 'property', {
+      fromSectionId: 'section-2',
+      toSectionId: 'section-2',
+      fromIndex: 0,
+      toIndex: 1
+    })
+
+    componentState.assertState({
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0', 'property-1']
+        },
+        {
+          id: 'section-1',
+          properties: []
+        },
+        {
+          id: 'section-2',
+          properties: ['property-3', 'property-2', 'property-4']
+        }
+      ],
+      properties: [
+        { id: 'property-0', section: 'section-0' },
+        { id: 'property-1', section: 'section-0' },
+        { id: 'property-2', section: 'section-2' },
+        { id: 'property-3', section: 'section-2' },
+        { id: 'property-4', section: 'section-2' }
+      ]
+    })
+
+    execute(componentState, 'property', {
+      fromSectionId: 'section-2',
+      toSectionId: 'section-0',
+      fromIndex: 1,
+      toIndex: 1
+    })
+
+    componentState.assertState({
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0', 'property-2', 'property-1']
+        },
+        {
+          id: 'section-1',
+          properties: []
+        },
+        {
+          id: 'section-2',
+          properties: ['property-3', 'property-4']
+        }
+      ],
+      properties: [
+        { id: 'property-0', section: 'section-0' },
+        { id: 'property-1', section: 'section-0' },
+        { id: 'property-2', section: 'section-0' },
+        { id: 'property-3', section: 'section-2' },
+        { id: 'property-4', section: 'section-2' }
+      ]
+    })
+  })
+})
+
+const execute = (componentState, type, params) => {
+  new ObjectTypeHandlerOrderChange(
+    componentState.getState(),
+    componentState.getSetState()
+  ).execute(type, params)
+}
diff --git a/openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerRemove.test.js b/openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerRemove.test.js
new file mode 100644
index 00000000000..8b8622681f6
--- /dev/null
+++ b/openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerRemove.test.js
@@ -0,0 +1,326 @@
+import ObjectTypeHandlerRemove from '../../../../../src/js/components/types/objectType/ObjectTypeHandlerRemove.js'
+import ComponentState from '../../../common/ComponentState.js'
+
+describe('ObjectTypeHandlerRemoveTest', () => {
+  test('section not used', () => {
+    const componentState = new ComponentState({
+      selection: {
+        type: 'section',
+        params: {
+          id: 'section-0'
+        }
+      },
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0', 'property-1']
+        },
+        {
+          id: 'section-1',
+          properties: ['property-2', 'property-3', 'property-4']
+        }
+      ],
+      properties: [
+        { id: 'property-0', section: 'section-0' },
+        { id: 'property-1', section: 'section-0' },
+        { id: 'property-2', section: 'section-1' },
+        { id: 'property-3', section: 'section-1' },
+        { id: 'property-4', section: 'section-1' }
+      ]
+    })
+
+    executeRemove(componentState)
+
+    componentState.assertState({
+      selection: null,
+      sections: [
+        {
+          id: 'section-1',
+          properties: ['property-2', 'property-3', 'property-4']
+        }
+      ],
+      properties: [
+        { id: 'property-2', section: 'section-1' },
+        { id: 'property-3', section: 'section-1' },
+        { id: 'property-4', section: 'section-1' }
+      ]
+    })
+  })
+
+  test('section used and confirmed', () => {
+    const componentState = new ComponentState({
+      selection: {
+        type: 'section',
+        params: {
+          id: 'section-0'
+        }
+      },
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0']
+        }
+      ],
+      properties: [{ id: 'property-0', section: 'section-0', usages: 1 }]
+    })
+
+    executeRemove(componentState)
+    executeRemove(componentState)
+
+    componentState.assertState({
+      removeSectionDialogOpen: true,
+      selection: {
+        type: 'section',
+        params: {
+          id: 'section-0'
+        }
+      },
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0']
+        }
+      ],
+      properties: [{ id: 'property-0', section: 'section-0', usages: 1 }]
+    })
+
+    executeRemove(componentState, true)
+
+    componentState.assertState({
+      removeSectionDialogOpen: false,
+      selection: null,
+      sections: [],
+      properties: []
+    })
+  })
+
+  test('section used and cancelled', () => {
+    const componentState = new ComponentState({
+      selection: {
+        type: 'section',
+        params: {
+          id: 'section-0'
+        }
+      },
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0']
+        }
+      ],
+      properties: [{ id: 'property-0', section: 'section-0', usages: 1 }]
+    })
+
+    executeRemove(componentState)
+    executeRemove(componentState)
+
+    componentState.assertState({
+      removeSectionDialogOpen: true,
+      selection: {
+        type: 'section',
+        params: {
+          id: 'section-0'
+        }
+      },
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0']
+        }
+      ],
+      properties: [{ id: 'property-0', section: 'section-0', usages: 1 }]
+    })
+
+    executeCancel(componentState)
+
+    componentState.assertState({
+      removeSectionDialogOpen: false,
+      selection: {
+        type: 'section',
+        params: {
+          id: 'section-0'
+        }
+      },
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0']
+        }
+      ],
+      properties: [{ id: 'property-0', section: 'section-0', usages: 1 }]
+    })
+  })
+
+  test('property not used', () => {
+    const componentState = new ComponentState({
+      selection: {
+        type: 'property',
+        params: {
+          id: 'property-3'
+        }
+      },
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0', 'property-1']
+        },
+        {
+          id: 'section-1',
+          properties: ['property-2', 'property-3', 'property-4']
+        }
+      ],
+      properties: [
+        { id: 'property-0', section: 'section-0' },
+        { id: 'property-1', section: 'section-0' },
+        { id: 'property-2', section: 'section-1' },
+        { id: 'property-3', section: 'section-1' },
+        { id: 'property-4', section: 'section-1' }
+      ]
+    })
+
+    executeRemove(componentState)
+
+    componentState.assertState({
+      selection: null,
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0', 'property-1']
+        },
+        {
+          id: 'section-1',
+          properties: ['property-2', 'property-4']
+        }
+      ],
+      properties: [
+        { id: 'property-0', section: 'section-0' },
+        { id: 'property-1', section: 'section-0' },
+        { id: 'property-2', section: 'section-1' },
+        { id: 'property-4', section: 'section-1' }
+      ]
+    })
+  })
+
+  test('property used and confirmed', () => {
+    const componentState = new ComponentState({
+      selection: {
+        type: 'property',
+        params: {
+          id: 'property-0'
+        }
+      },
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0']
+        }
+      ],
+      properties: [{ id: 'property-0', section: 'section-0', usages: 1 }]
+    })
+
+    executeRemove(componentState)
+    executeRemove(componentState)
+
+    componentState.assertState({
+      removePropertyDialogOpen: true,
+      selection: {
+        type: 'property',
+        params: {
+          id: 'property-0'
+        }
+      },
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0']
+        }
+      ],
+      properties: [{ id: 'property-0', section: 'section-0', usages: 1 }]
+    })
+
+    executeRemove(componentState, true)
+
+    componentState.assertState({
+      removePropertyDialogOpen: false,
+      selection: null,
+      sections: [
+        {
+          id: 'section-0',
+          properties: []
+        }
+      ],
+      properties: []
+    })
+  })
+
+  test('property used and cancelled', () => {
+    const componentState = new ComponentState({
+      selection: {
+        type: 'property',
+        params: {
+          id: 'property-0'
+        }
+      },
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0']
+        }
+      ],
+      properties: [{ id: 'property-0', section: 'section-0', usages: 1 }]
+    })
+
+    executeRemove(componentState)
+    executeRemove(componentState)
+
+    componentState.assertState({
+      removePropertyDialogOpen: true,
+      selection: {
+        type: 'property',
+        params: {
+          id: 'property-0'
+        }
+      },
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0']
+        }
+      ],
+      properties: [{ id: 'property-0', section: 'section-0', usages: 1 }]
+    })
+
+    executeCancel(componentState)
+
+    componentState.assertState({
+      removePropertyDialogOpen: false,
+      selection: {
+        type: 'property',
+        params: {
+          id: 'property-0'
+        }
+      },
+      sections: [
+        {
+          id: 'section-0',
+          properties: ['property-0']
+        }
+      ],
+      properties: [{ id: 'property-0', section: 'section-0', usages: 1 }]
+    })
+  })
+})
+
+const executeRemove = (componentState, confirmed) => {
+  new ObjectTypeHandlerRemove(
+    componentState.getState(),
+    componentState.getSetState()
+  ).executeRemove(confirmed)
+}
+
+const executeCancel = componentState => {
+  new ObjectTypeHandlerRemove(
+    componentState.getState(),
+    componentState.getSetState()
+  ).executeCancel()
+}
diff --git a/openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerSelectionChange.test.js b/openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerSelectionChange.test.js
new file mode 100644
index 00000000000..834e128f3bb
--- /dev/null
+++ b/openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerSelectionChange.test.js
@@ -0,0 +1,49 @@
+import ObjectTypeHandlerSelectionChange from '../../../../../src/js/components/types/objectType/ObjectTypeHandlerSelectionChange.js'
+import ComponentState from '../../../common/ComponentState.js'
+
+describe('ObjectTypeHandlerSelectionChangeTest', () => {
+  test('section', () => {
+    const componentState = new ComponentState({
+      selection: null
+    })
+
+    execute(componentState, 'section', {
+      id: 'section-0'
+    })
+
+    componentState.assertState({
+      selection: {
+        type: 'section',
+        params: {
+          id: 'section-0'
+        }
+      }
+    })
+  })
+
+  test('property', () => {
+    const componentState = new ComponentState({
+      selection: null
+    })
+
+    execute(componentState, 'property', {
+      id: 'property-0'
+    })
+
+    componentState.assertState({
+      selection: {
+        type: 'property',
+        params: {
+          id: 'property-0'
+        }
+      }
+    })
+  })
+})
+
+const execute = (componentState, type, params) => {
+  new ObjectTypeHandlerSelectionChange(
+    componentState.getState(),
+    componentState.getSetState()
+  ).execute(type, params)
+}
diff --git a/openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerValidate.test.js b/openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerValidate.test.js
new file mode 100644
index 00000000000..4e1d143faf9
--- /dev/null
+++ b/openbis_ng_ui/srcTest/js/components/types/objectType/ObjectTypeHandlerValidate.test.js
@@ -0,0 +1,255 @@
+import ObjectTypeHandlerValidate from '../../../../../src/js/components/types/objectType/ObjectTypeHandlerValidate.js'
+import ComponentState from '../../../common/ComponentState.js'
+import { dto } from '../../../../../src/js/services/openbis.js'
+
+jest.mock('../../../../../src/js/services/openbis.js')
+
+beforeEach(() => {
+  jest.resetAllMocks()
+  dto.DataType.CONTROLLEDVOCABULARY = 'CONTROLLEDVOCABULARY'
+  dto.DataType.MATERIAL = 'MATERIAL'
+})
+
+describe('ObjectTypeHandlerValidateTest', () => {
+  test('validation enabled with autofocus fails', done => {
+    const componentState = new ComponentState({
+      validate: false,
+      selection: {
+        type: 'testtype'
+      },
+      type: { usages: 1 },
+      properties: [
+        { id: 'property-0' },
+        { id: 'property-1', dataType: dto.DataType.CONTROLLEDVOCABULARY },
+        { id: 'property-2', dataType: dto.DataType.MATERIAL },
+        { id: 'property-3', mandatory: true }
+      ]
+    })
+
+    execute(componentState, true, true).then(result => {
+      componentState.assertState({
+        validate: true,
+        selection: {
+          type: 'type',
+          params: {
+            part: 'code'
+          }
+        },
+        type: {
+          usages: 1,
+          errors: {
+            code: 'Code cannot be empty',
+            generatedCodePrefix: 'Generated code prefix cannot be empty'
+          }
+        },
+        properties: [
+          {
+            id: 'property-0',
+            errors: {
+              code: 'Code cannot be empty',
+              dataType: 'Data Type cannot be empty',
+              description: 'Description cannot be empty',
+              label: 'Label cannot be empty'
+            }
+          },
+          {
+            id: 'property-1',
+            dataType: dto.DataType.CONTROLLEDVOCABULARY,
+            errors: {
+              code: 'Code cannot be empty',
+              description: 'Description cannot be empty',
+              label: 'Label cannot be empty',
+              vocabulary: 'Vocabulary cannot be empty'
+            }
+          },
+          {
+            id: 'property-2',
+            dataType: dto.DataType.MATERIAL,
+            errors: {
+              code: 'Code cannot be empty',
+              description: 'Description cannot be empty',
+              label: 'Label cannot be empty',
+              materialType: 'Material Type cannot be empty'
+            }
+          },
+          {
+            id: 'property-3',
+            mandatory: true,
+            errors: {
+              code: 'Code cannot be empty',
+              dataType: 'Data Type cannot be empty',
+              description: 'Description cannot be empty',
+              label: 'Label cannot be empty',
+              initialValueForExistingEntities: 'Initial Value cannot be empty'
+            }
+          }
+        ]
+      })
+      expect(result).toBe(false)
+      done()
+    })
+  })
+
+  test('validation enabled without autofocus fails', done => {
+    const componentState = new ComponentState({
+      validate: false,
+      selection: {
+        type: 'testtype'
+      },
+      type: {},
+      properties: [{ id: 'property-0' }]
+    })
+
+    execute(componentState, true, false).then(result => {
+      componentState.assertState({
+        validate: true,
+        selection: {
+          type: 'testtype'
+        },
+        type: {
+          errors: {
+            code: 'Code cannot be empty',
+            generatedCodePrefix: 'Generated code prefix cannot be empty'
+          }
+        },
+        properties: [
+          {
+            id: 'property-0',
+            errors: {
+              code: 'Code cannot be empty',
+              dataType: 'Data Type cannot be empty',
+              description: 'Description cannot be empty',
+              label: 'Label cannot be empty'
+            }
+          }
+        ]
+      })
+      expect(result).toBe(false)
+      done()
+    })
+  })
+
+  test('validation enabled with autofocus succeeds', done => {
+    const componentState = new ComponentState({
+      validate: false,
+      selection: {
+        type: 'testtype'
+      },
+      type: { code: 'TYPE_CODE', generatedCodePrefix: 'TYPE_CODE_PREFIX' },
+      properties: [
+        {
+          id: 'property-0',
+          code: 'PROPERTY_CODE',
+          dataType: 'PROPERTY_DATA_TYPE',
+          description: 'PROPERTY_DESCRIPTION',
+          label: 'PROPERTY_LABEL'
+        }
+      ]
+    })
+
+    execute(componentState, true, true).then(result => {
+      componentState.assertState({
+        validate: true,
+        selection: {
+          type: 'testtype'
+        },
+        type: {
+          code: 'TYPE_CODE',
+          generatedCodePrefix: 'TYPE_CODE_PREFIX',
+          errors: {}
+        },
+        properties: [
+          {
+            id: 'property-0',
+            code: 'PROPERTY_CODE',
+            dataType: 'PROPERTY_DATA_TYPE',
+            description: 'PROPERTY_DESCRIPTION',
+            label: 'PROPERTY_LABEL',
+            errors: {}
+          }
+        ]
+      })
+      expect(result).toBe(true)
+      done()
+    })
+  })
+
+  test('validation enabled without autofocus succeeds', done => {
+    const componentState = new ComponentState({
+      validate: false,
+      selection: {
+        type: 'testtype'
+      },
+      type: { code: 'TYPE_CODE', generatedCodePrefix: 'TYPE_CODE_PREFIX' },
+      properties: [
+        {
+          id: 'property-0',
+          code: 'PROPERTY_CODE',
+          dataType: 'PROPERTY_DATA_TYPE',
+          description: 'PROPERTY_DESCRIPTION',
+          label: 'PROPERTY_LABEL'
+        }
+      ]
+    })
+
+    execute(componentState, true, false).then(result => {
+      componentState.assertState({
+        validate: true,
+        selection: {
+          type: 'testtype'
+        },
+        type: {
+          code: 'TYPE_CODE',
+          generatedCodePrefix: 'TYPE_CODE_PREFIX',
+          errors: {}
+        },
+        properties: [
+          {
+            id: 'property-0',
+            code: 'PROPERTY_CODE',
+            dataType: 'PROPERTY_DATA_TYPE',
+            description: 'PROPERTY_DESCRIPTION',
+            label: 'PROPERTY_LABEL',
+            errors: {}
+          }
+        ]
+      })
+      expect(result).toBe(true)
+      done()
+    })
+  })
+
+  test('validation disabled', done => {
+    const componentState = new ComponentState({
+      validate: false,
+      selection: {
+        type: 'testtype'
+      },
+      type: {},
+      properties: [{ id: 'property-0' }]
+    })
+
+    execute(componentState, false, true).then(result => {
+      componentState.assertState({
+        validate: false,
+        selection: {
+          type: 'testtype'
+        },
+        type: {},
+        properties: [{ id: 'property-0' }]
+      })
+      expect(result).toBe(true)
+      done()
+    })
+  })
+})
+
+const execute = (componentState, enabled, autofocus) => {
+  const handler = new ObjectTypeHandlerValidate(
+    componentState.getGetState(),
+    componentState.getSetState()
+  )
+  return handler.setEnabled(enabled).then(() => {
+    return handler.execute(autofocus)
+  })
+}
-- 
GitLab