Repository: arenadata/adcm (language: python)

Issue:
Adcm-2795 ui tests for component in cluster



Discussion / hints:

Fix patch:
diff --git a/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.html b/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.html
index dba9323034..7adb2868bc 100644
--- a/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.html
+++ b/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.html
@@ -2,7 +2,7 @@
   <div class="title-container">
     <button mat-button [disabled]="isDisabled()" class="title" [appTooltip]="model.name"
       [appTooltipShowByCondition]="true" (click)="clickToTitle()">
-      <span *ngIf="isError()" class="warn">*</span>&nbsp;<span [matTooltip]="model.notification"
+      <span *ngIf="isMandatory()" class="warn">*</span>&nbsp;<span [matTooltip]="model?.notification" [matTooltipDisabled]="isError()"
         [style.color]="model.color">{{ model.name }}</span>
     </button>
     <button mat-raised-button color="primary"
diff --git a/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.ts b/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.ts
index 1e7c200473..08fb3a99ca 100644
--- a/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.ts
+++ b/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.ts
@@ -9,10 +9,9 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { AfterViewChecked, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
 import { FormGroup } from '@angular/forms';
 import { isNumber } from '@app/core/types';
-
 import { CompTile, Tile } from '../types';
 
 @Component({
@@ -20,7 +19,7 @@ import { CompTile, Tile } from '../types';
   templateUrl: './much-2-many.component.html',
   styleUrls: ['./much-2-many.component.scss'],
 })
-export class Much2ManyComponent {
+export class Much2ManyComponent implements AfterViewChecked {
   isShow = false;
 
   @Output() clickToTitleEvt: EventEmitter<any> = new EventEmitter();
@@ -28,6 +27,12 @@ export class Much2ManyComponent {
   @Input() model: Tile;
   @Input() form: FormGroup;
 
+  constructor(private cdRef : ChangeDetectorRef) {}
+
+  ngAfterViewChecked() {
+    this.cdRef.detectChanges();
+  }
+
   isDisabled() {
     if (this.model.actions) return !this.model.actions.length;
     return this.model.disabled;
@@ -37,10 +42,21 @@ export class Much2ManyComponent {
     if ('service_id' in this.model && Object.keys(this.form.controls).length) {
       const sc = this.model as CompTile;
       const control = this.form.controls[`${sc.service_id}/${sc.id}`];
-      if (!control) return false;
-      sc.notification = control.errors?.error;
-      return control.invalid;
-    } else return false;
+
+      if (!control || !control?.errors?.error) return true;
+
+      sc.notification = control.errors.error || null;
+      return !control.invalid;
+    }
+    return true;
+  }
+
+  isMandatory() {
+    if (this.model?.limit) {
+      return ['+', 'odd', 1].includes(this.model?.limit[0]) && this.model?.relations.length === 0;
+    }
+
+    return false;
   }
 
   clearDisabled(rel: Tile) {
@@ -62,7 +78,7 @@ export class Much2ManyComponent {
   }
 
   setNotify() {
-    const [a, b, c] = this.model.limit,
+    const [a, b] = this.model.limit,
       lim = isNumber(b) ? b : a === 'odd' ? 1 : a === 'depend' ? 0 : a;
     return `${this.model.relations.length}${lim !== 0 ? ` / ${lim}` : ''}`;
   }
diff --git a/web/src/app/shared/host-components-map/take.service.spec.ts b/web/src/app/shared/host-components-map/take.service.spec.ts
index 1d918a5a78..966fae5854 100644
--- a/web/src/app/shared/host-components-map/take.service.spec.ts
+++ b/web/src/app/shared/host-components-map/take.service.spec.ts
@@ -131,14 +131,14 @@ describe('HostComponentsMap :: TakeService', () => {
     });
   });
 
-  it('validateConstraints fn for Component { constrant: [0, odd], relations: [{}, {}]} =>  Total amount should be odd.', () => {
+  it('validateConstraints fn for Component { constrant: [0, odd], relations: [{}, {}]} => Must be installed at least 0 components. Total amount should be odd.', () => {
     const d = { ...ctData, constraint: [0, 'odd'] };
     const mCompTile = new CompTile(d);
     (mCompTile.relations = [
       { id: 0, name: 'test', relations: [], color: 'none', disabled: false },
       { id: 1, name: 'test', relations: [], color: 'none', disabled: false },
     ]),
-      expect(service.validateConstraints(mCompTile, 0)()).toEqual({ error: 'Total amount should be odd.' });
+      expect(service.validateConstraints(mCompTile, 0)()).toEqual({ error: 'Must be installed at least 0 components. Total amount should be odd.' });
   });
 
   it('validateConstraints fn for Component { constrant: [1, odd], relations: [{}]}] tobe null', () => {
diff --git a/web/src/app/shared/host-components-map/take.service.ts b/web/src/app/shared/host-components-map/take.service.ts
index 4dde1ed032..1d2730f6a7 100644
--- a/web/src/app/shared/host-components-map/take.service.ts
+++ b/web/src/app/shared/host-components-map/take.service.ts
@@ -165,10 +165,8 @@ export class TakeService {
           case 'depend':
             return depend();
           case 'odd':
-            return countRelations % 2 && countRelations >= a1
+            return ((a1 === 0 && countRelations === 0) || countRelations % 2) && (countRelations >= a1)
               ? null
-              : a1 === 0
-              ? 'Total amount should be odd.'
               : `Must be installed at least ${a1} components. Total amount should be odd.`;
           case '+':
           default:


Test patch:
diff --git a/tests/ui_tests/test_cluster_list_page.py b/tests/ui_tests/test_cluster_list_page.py
index 51783eba8a..ec3eafd2d7 100644
--- a/tests/ui_tests/test_cluster_list_page.py
+++ b/tests/ui_tests/test_cluster_list_page.py
@@ -743,13 +743,34 @@ def test_check_cluster_components_page_create_host(self, app_fs, bundle_archive,
 
     @pytest.mark.smoke()
     @pytest.mark.include_firefox()
-    def test_check_cluster_components_page_create_components(
-        self, app_fs, create_community_cluster_with_host_and_service
-    ):
-        """Test distribution of components on hosts"""
+    @pytest.mark.parametrize(
+        "bundle_name",
+        [
+            "cluster_with_not_required_component",
+            "cluster_with_required_component",
+            "cluster_with_required_component_short",
+        ],
+    )
+    def test_check_cluster_components_page_create_components(self, app_fs, sdk_client_fs, create_host, bundle_name):
+        """Test adding components on hosts"""
+
         params = {"message": "Successfully saved."}
-        cluster, *_ = create_community_cluster_with_host_and_service
+        is_not_required = "_not_required" in bundle_name
+
+        with allure.step("Add cluster with service and host"):
+            bundle = cluster_bundle(sdk_client_fs, bundle_name)
+            cluster = bundle.cluster_create(name=CLUSTER_NAME)
+            cluster.service_add(name=SERVICE_NAME)
+            cluster.host_add(create_host)
+
         cluster_components_page = ClusterComponentsPage(app_fs.driver, app_fs.adcm.url, cluster.id).open()
+
+        with allure.step("Check that required component has * in name"):
+            component_name = cluster_components_page.get_components_rows()[0].text
+            assert (
+                "*" not in component_name if is_not_required else f"* {COMPONENT_NAME}" in component_name
+            ), f'There are {"" if is_not_required else "no"} * in component name "{component_name}"'
+
         host_row = cluster_components_page.find_host_row_by_name(HOST_NAME)
         component_row = cluster_components_page.find_component_row_by_name(COMPONENT_NAME)
         cluster_components_page.click_host(host_row)
@@ -762,7 +783,9 @@ def test_check_cluster_components_page_create_components(
             host_row = cluster_components_page.get_host_rows()[0]
             check_components_host_info(cluster_components_page.get_row_info(host_row), HOST_NAME, "1")
             component_row = cluster_components_page.get_components_rows()[0]
-            check_components_host_info(cluster_components_page.get_row_info(component_row), COMPONENT_NAME, "1")
+            check_components_host_info(
+                cluster_components_page.get_row_info(component_row), COMPONENT_NAME, "1" if is_not_required else "1 / 1"
+            )
 
     def test_check_cluster_components_page_restore_components(
         self, app_fs, create_community_cluster_with_host_and_service
diff --git a/tests/ui_tests/test_cluster_list_page_data/cluster_with_not_required_component/config.yaml b/tests/ui_tests/test_cluster_list_page_data/cluster_with_not_required_component/config.yaml
new file mode 100644
index 0000000000..c36c71daf3
--- /dev/null
+++ b/tests/ui_tests/test_cluster_list_page_data/cluster_with_not_required_component/config.yaml
@@ -0,0 +1,24 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+- type: cluster
+  name: test_cluster
+  version: 1.5
+  edition: community
+
+- type: service
+  name: test_service
+  description: Test service
+  version: '1.2'
+  components:
+    first:
+        constraint: [0, odd]
diff --git a/tests/ui_tests/test_cluster_list_page_data/cluster_with_required_component/config.yaml b/tests/ui_tests/test_cluster_list_page_data/cluster_with_required_component/config.yaml
new file mode 100644
index 0000000000..b1887a3798
--- /dev/null
+++ b/tests/ui_tests/test_cluster_list_page_data/cluster_with_required_component/config.yaml
@@ -0,0 +1,24 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+- type: cluster
+  name: test_cluster
+  version: 1.5
+  edition: community
+
+- type: service
+  name: test_service
+  description: Test service
+  version: '1.2'
+  components:
+    first:
+        constraint: [1, odd]
diff --git a/tests/ui_tests/test_cluster_list_page_data/cluster_with_required_component_short/config.yaml b/tests/ui_tests/test_cluster_list_page_data/cluster_with_required_component_short/config.yaml
new file mode 100644
index 0000000000..878248b459
--- /dev/null
+++ b/tests/ui_tests/test_cluster_list_page_data/cluster_with_required_component_short/config.yaml
@@ -0,0 +1,24 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+- type: cluster
+  name: test_cluster
+  version: 1.5
+  edition: community
+
+- type: service
+  name: test_service
+  description: Test service
+  version: '1.2'
+  components:
+    first:
+        constraint: [odd]