Repository: ProcessMaker/modeler (js)

Issue:
Error thrown when validating Parallel Gateway
## Description
A console error is thrown when validation is toggled for a process containing a parallel gateway.

## Reproduction steps
1. Drag a parallel gateway onto the grid.
2. Drag 3 tasks onto the grid.
3. Connect 2 of the tasks to the gateway.
4. Connect the gateway to the 3rd task that is not yet connected to anything.
5. Toggle Auto validate.

## Expected behaviour
No console errors are thrown.

## Actual behaviour
The following console error is thrown:
<img width="759" alt="Screen Shot 2020-02-03 at 2 21 18 PM" src="https://user-images.githubusercontent.com/7561061/73684340-fbfbb200-4691-11ea-88c7-362a7a001fa4.png">
```
[Vue warn]: Duplicate keys detected
```

## Software used
Latest modeler `develop` branch on Chrome 79 on macOS Catalina.


Pull request description:
Console error validating parallel gateway
Closes #1086 

Wasn't sure how to assert that the console log didn't contain 'Duplicate keys'.
Was able to assert that there were no errors. But this could catch other site errors (which are bad anyway).

Patch:
diff --git a/package-lock.json b/package-lock.json
index 46c7a1c9..38d07636 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "@processmaker/modeler",
-  "version": "0.18.3",
+  "version": "0.18.4",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -3839,9 +3839,9 @@
       }
     },
     "@processmaker/processmaker-bpmn-moddle": {
-      "version": "0.4.6",
-      "resolved": "https://registry.npmjs.org/@processmaker/processmaker-bpmn-moddle/-/processmaker-bpmn-moddle-0.4.6.tgz",
-      "integrity": "sha512-VSd90lwiD8E7LeDsx5WUefVC0vDv8Ua2Yl6Fm9bvzQghfYcY24J4ZiIJBZU48cgv6IrlBcuc8Wi5AhgyQ9pIww==",
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/@processmaker/processmaker-bpmn-moddle/-/processmaker-bpmn-moddle-0.5.0.tgz",
+      "integrity": "sha512-GuQi4Anruy4flD5fd+CczD3jPKL8db9ZEXbbFe69OVZ8a3Vvw3z8lH9BvdPZcs+DFUcsmmcjMXTLuBPIjubhcg==",
       "dev": true,
       "requires": {
         "min-dash": "^3.0.0"
@@ -10950,7 +10950,8 @@
           "version": "2.1.1",
           "resolved": false,
           "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -10974,13 +10975,15 @@
           "version": "1.0.0",
           "resolved": false,
           "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "resolved": false,
           "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
           "dev": true,
+          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -10997,19 +11000,22 @@
           "version": "1.1.0",
           "resolved": false,
           "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "concat-map": {
           "version": "0.0.1",
           "resolved": false,
           "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "resolved": false,
           "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -11140,7 +11146,8 @@
           "version": "2.0.3",
           "resolved": false,
           "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "ini": {
           "version": "1.3.5",
@@ -11154,6 +11161,7 @@
           "resolved": false,
           "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
           "dev": true,
+          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -11170,6 +11178,7 @@
           "resolved": false,
           "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
           "dev": true,
+          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -11178,13 +11187,15 @@
           "version": "0.0.8",
           "resolved": false,
           "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "minipass": {
           "version": "2.3.5",
           "resolved": false,
           "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
           "dev": true,
+          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.2",
             "yallist": "^3.0.0"
@@ -11205,6 +11216,7 @@
           "resolved": false,
           "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
           "dev": true,
+          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -11293,7 +11305,8 @@
           "version": "1.0.1",
           "resolved": false,
           "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -11307,6 +11320,7 @@
           "resolved": false,
           "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
           "dev": true,
+          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -11402,7 +11416,8 @@
           "version": "5.1.2",
           "resolved": false,
           "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -11444,6 +11459,7 @@
           "resolved": false,
           "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
           "dev": true,
+          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -11465,6 +11481,7 @@
           "resolved": false,
           "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
           "dev": true,
+          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -11513,13 +11530,15 @@
           "version": "1.0.2",
           "resolved": false,
           "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "yallist": {
           "version": "3.0.3",
           "resolved": false,
           "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
-          "dev": true
+          "dev": true,
+          "optional": true
         }
       }
     },
diff --git a/package.json b/package.json
index dac4f1f2..a7b5eb0f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@processmaker/modeler",
-  "version": "0.18.3",
+  "version": "0.18.4",
   "scripts": {
     "serve": "vue-cli-service serve",
     "open-cypress": "TZ=UTC cypress open",
@@ -59,7 +59,7 @@
     "@babel/core": "^7.7.7",
     "@cypress/code-coverage": "^1.10.1",
     "@panter/vue-i18next": "^0.15.1",
-    "@processmaker/processmaker-bpmn-moddle": "0.4.6",
+    "@processmaker/processmaker-bpmn-moddle": "0.5.0",
     "@processmaker/screen-builder": "^0.21.5",
     "@processmaker/vue-form-elements": "^0.13.2",
     "@types/jest": "^24.0.25",
diff --git a/src/components/validationStatus/ValidationStatus.vue b/src/components/validationStatus/ValidationStatus.vue
index 97238c61..d4f3a0bf 100644
--- a/src/components/validationStatus/ValidationStatus.vue
+++ b/src/components/validationStatus/ValidationStatus.vue
@@ -38,7 +38,7 @@
             <div v-if="isProblemsPanelDisplayed" class="validation-container position-absolute text-left">
               <dl class="validation-container__list align-items-baseline" data-test="validation-list">
                 <template v-for="error in errorList">
-                  <dt class="text-capitalize" :key="`${error.id}_${error.errorKey}`">
+                  <dt class="text-capitalize" :key="error.errorId">
                     <font-awesome-icon
                       class="status-bar-container__status-icon ml-1 mr-1 mt-1"
                       :style="{ color: isErrorCategory(error) ? errorColor : warningColor }"
@@ -46,7 +46,7 @@
                     />
                     {{ error.errorKey }}
                   </dt>
-                  <dd :key="`${error.id}_${error.errorKey}_dd`">
+                  <dd :key="`${ error.errorId }_dd`">
                     <p class="pl-4 mb-0 font-italic">{{ error.message }}.</p>
                     <p class="pl-4 mb-0" v-if="error.id"><span class="font-weight-bold">{{ $t('Node ID') }}:</span> {{
                       error.id }}</p>
@@ -80,6 +80,7 @@ import {
 } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
 import store from '@/store';
+import validationErrorList from './errorListUtil';
 
 export default {
   components: {
@@ -108,12 +109,7 @@ export default {
       },
     },
     errorList() {
-      return Object.entries(this.validationErrors)
-        .flatMap(([errorKey, errors]) => {
-          return errors.flatMap(error => {
-            return { ...error, errorKey };
-          });
-        });
+      return validationErrorList(this.validationErrors);
     },
     numberOfProblemsToDisplay() {
       return this.errorList.length + this.warnings.length;
diff --git a/src/components/validationStatus/errorListUtil.js b/src/components/validationStatus/errorListUtil.js
new file mode 100644
index 00000000..7f982823
--- /dev/null
+++ b/src/components/validationStatus/errorListUtil.js
@@ -0,0 +1,11 @@
+export default function errorList(validationErrors) {
+  return Object.entries(validationErrors).flatMap(([errorKey, errors]) => {
+    return errors.flatMap(error => {
+      return {
+        ...error,
+        errorKey,
+        ...{ 'errorId': `${ error.id }_${ error.message.split(' ').join('_') }` },
+      };
+    });
+  });
+}


Test patch:
diff --git a/tests/e2e/specs/Modeler.spec.js b/tests/e2e/specs/Modeler.spec.js
index 0ef1b31e..33c81852 100644
--- a/tests/e2e/specs/Modeler.spec.js
+++ b/tests/e2e/specs/Modeler.spec.js
@@ -502,4 +502,36 @@ describe('Modeler', () => {
       expect($list).to.contain('references a non-existent element and was not parsed');
     });
   });
+
+  it('Does not display a console error on multiple validation errors for one node', () => {
+    cy.window().then((win) => {
+      cy.spy(win.console, 'error');
+    });
+
+    const taskPosition1 = { x: 300, y: 250 };
+    const taskPosition2 = { x: 300, y: 350 };
+    const taskPosition3 = { x: 300, y: 450 };
+    const parallelGatewayPosition = { x: 200, y: 200 };
+
+    addNodeTypeToPaper(parallelGatewayPosition, nodeTypes.exclusiveGateway, 'switch-to-parallel-gateway');
+    dragFromSourceToDest(nodeTypes.task, taskPosition1);
+    dragFromSourceToDest(nodeTypes.task, taskPosition2);
+    dragFromSourceToDest(nodeTypes.task, taskPosition3);
+
+    connectNodesWithFlow('sequence-flow-button', taskPosition1, parallelGatewayPosition);
+    connectNodesWithFlow('sequence-flow-button', taskPosition2, parallelGatewayPosition);
+    connectNodesWithFlow('sequence-flow-button', parallelGatewayPosition, taskPosition3);
+
+    cy.get('[data-test="validation-toggle"]').click({ force: true });
+    cy.get('[data-test="validation-list-toggle"]').click();
+
+    cy.get('[data-test=validation-list]')
+      .should('contain.text', 'Gateway must have multiple outgoing Sequence Flows.Node ID: node_3');
+    cy.get('[data-test=validation-list]')
+      .should('contain.text', 'Gateway must not have multiple incoming Sequence Flows.Node ID: node_3');
+
+    cy.window().then((win) => {
+      expect(win.console.error).to.have.callCount(0);
+    });
+  });
 });
diff --git a/tests/unit/validationStatus.spec.js b/tests/unit/validationStatus.spec.js
new file mode 100644
index 00000000..815dff4a
--- /dev/null
+++ b/tests/unit/validationStatus.spec.js
@@ -0,0 +1,16 @@
+import errorList from '@/components/validationStatus/errorListUtil';
+import uniqBy from 'lodash/uniqBy';
+
+describe('Validation Status', () => {
+  it('Should contain unique errorIds', () => {
+    const errors = {
+      'processmaker/gateway-direction': [
+        {'id': 'node_1', 'message': 'Gateway must have multiple outgoing Sequence Flows.'},
+        {'id': 'node_1', 'message': 'Gateway must not have multiple incoming Sequence Flows.'},
+      ],
+    };
+
+    const list = errorList(errors);
+    expect(uniqBy(list, 'errorId')).toHaveLength(list.length);
+  });
+});