Repository: SAP/luigi (js)

Issue:
LuigiCompoundContainer: showAlert and showConfirmationModal for nested webcomponents
compound 

Handle also the callback for both functions (notifyAlertClosed + notifyConfirmationModalClosed)

Pull request:
Test cases for nested webcomponents
**Description**

Changes proposed in this pull request:

- optional callback for `showAlert` in WC
- test cases for nested webcomponents

**Related issue(s)**

Resolves #4199 


Patch:
diff --git a/container/src/services/webcomponents.service.ts b/container/src/services/webcomponents.service.ts
index 2490164e..d8f225f6 100644
--- a/container/src/services/webcomponents.service.ts
+++ b/container/src/services/webcomponents.service.ts
@@ -78,9 +78,11 @@ export class WebComponentService {
    * @param msg the message to be delivered
    * @param data the data to be sent
    * @param callback the callback function to be called
+   * @param callbackName name of the callback function
    */
-  dispatchLuigiEvent(msg: string, data: object, callback?: () => void) {
-    this.containerService.dispatch(msg, this.thisComponent, data, callback);
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  dispatchLuigiEvent(msg: string, data: object, callback?: (arg?: any) => void, callbackName?: string) {
+    this.containerService.dispatch(msg, this.thisComponent, data, callback, callbackName);
   }
 
   /**
@@ -239,7 +241,14 @@ export class WebComponentService {
             alertSettings.id = this.alertIndex++;
             return new Promise((resolve) => {
               this.alertResolvers[alertSettings.id] = resolve;
-              this.dispatchLuigiEvent(Events.ALERT_REQUEST, alertSettings);
+              this.dispatchLuigiEvent(
+                Events.ALERT_REQUEST,
+                alertSettings,
+                (dismissKey?: boolean | string) => {
+                  this.resolveAlert(alertSettings.id, dismissKey);
+                },
+                'callback'
+              );
             });
           },
           showConfirmationModal: (settings) => {
@@ -714,14 +723,14 @@ export class WebComponentService {
    * and then the resolver is removed from the `alertResolvers` object to avoid future invocations. If no resolver is found
    * for the provided `id`, a message is logged to the console indicating that no matching promise is in the list.
    * @param {string} id - The unique identifier for the alert to resolve.
-   * @param {dismissKey} [dismissKey=true] - An optional key or value passed to the resolver. Defaults to `true` if not provided.
+   * @param {boolean|string} dismissKey - An optional key or value passed to the resolver. Defaults to `true` if not provided.
    *
    * @returns {void}
    *
    */
-  resolveAlert(id, dismissKey = true) {
+  resolveAlert(id: string, dismissKey?: boolean | string) {
     if (this.alertResolvers[id]) {
-      this.alertResolvers[id](dismissKey);
+      this.alertResolvers[id](dismissKey === undefined ? true : dismissKey);
       this.alertResolvers[id] = undefined;
     } else {
       console.log('Promise is not in the list.');


Test patch:
diff --git a/container/cypress/e2e/test-app/compound/wc-compound-nested.cy.js b/container/cypress/e2e/test-app/compound/wc-compound-nested.cy.js
new file mode 100644
index 00000000..42e5e6b3
--- /dev/null
+++ b/container/cypress/e2e/test-app/compound/wc-compound-nested.cy.js
@@ -0,0 +1,125 @@
+describe('Nested Compound Container Tests', () => {
+  const testPage = 'http://localhost:8080/compound/nested.html';
+  const containerSelector = '#nested-wc-compound-container';
+  let consoleInfo;
+  let stub;
+
+  beforeEach(() => {
+    cy.visit(testPage, {
+      onBeforeLoad(win) {
+        // Clear logs in window console
+        if (Object.prototype.toString.call(win.console.clear) === '[object Function]') {
+          win.console.clear();
+        }
+
+        // Set up a spy on console.info
+        cy.stub(win.console, 'info', (value) => {
+          consoleInfo = value;
+        });
+      }
+    });
+    stub = cy.stub();
+  });
+
+  describe('Luigi Client API - showAlert', () => {
+    it('should emit notifyAlertClosed in direct WC', () => {
+      let notifySpy;
+
+      cy.on('window:alert', stub);
+      cy.get(containerSelector).then((container) => {
+        notifySpy = cy.spy(container[0], 'notifyAlertClosed');
+      });
+
+      cy.get(containerSelector)
+        .shadow()
+        .contains('showAlert')
+        .click()
+        .then(() => {
+          expect(stub.getCall(0)).to.be.calledWith('Direct WC - alert message');
+
+          if (notifySpy) {
+            expect(notifySpy).to.be.calledWith(0);
+          }
+        });
+    });
+
+    it('should emit notifyAlertClosed in nested WC', () => {
+      let notifySpy;
+
+      cy.on('window:alert', stub);
+      cy.get(containerSelector).then((container) => {
+        notifySpy = cy.spy(container[0], 'notifyAlertClosed');
+      });
+
+      cy.get(containerSelector)
+        .shadow()
+        .find(
+          'luigi-wc-687474703a2f2f6c6f63616c686f73743a383038302f636f6d706f756e642f68656c6c6f576f726c64574353656c66526567697374657265642e6a73'
+        )
+        .shadow()
+        .contains('showAlert')
+        .click()
+        .then(() => {
+          expect(stub.getCall(0)).to.be.calledWith('Nested WC - alert message');
+
+          if (notifySpy) {
+            expect(notifySpy).to.be.calledWith(0);
+          }
+        });
+    });
+  });
+
+  describe('Luigi Client API - showConfirmationModal', () => {
+    it('should emit notifyConfirmationModalClosed in direct WC', () => {
+      let notifySpy;
+
+      cy.on('window:alert', stub);
+      cy.get(containerSelector).then((container) => {
+        notifySpy = cy.spy(container[0], 'notifyConfirmationModalClosed');
+      });
+
+      cy.get(containerSelector)
+        .shadow()
+        .contains('showConfirmationModal')
+        .click()
+        .then(() => {
+          cy.on('window:confirm', (str) => {
+            expect(str).to.equal('Are you sure you want to do this?');
+          });
+          expect(consoleInfo).to.equal('Direct WC - modal confirmed');
+
+          if (notifySpy) {
+            expect(notifySpy).to.be.calledWith(true);
+          }
+        });
+    });
+
+    it('should emit notifyConfirmationModalClosed in nested WC', () => {
+      let notifySpy;
+
+      cy.on('window:alert', stub);
+      cy.get(containerSelector).then((container) => {
+        notifySpy = cy.spy(container[0], 'notifyConfirmationModalClosed');
+      });
+
+      cy.get(containerSelector)
+        .shadow()
+        .find(
+          'luigi-wc-687474703a2f2f6c6f63616c686f73743a383038302f636f6d706f756e642f68656c6c6f576f726c64574353656c66526567697374657265642e6a73'
+        )
+        .shadow()
+        .contains('showConfirmationModal')
+        .click()
+        .then(() => {
+          cy.on('window:confirm', (str) => {
+            expect(str).to.equal('Are you sure you want to do this?');
+          });
+          expect(consoleInfo).to.equal('Nested WC - modal confirmed');
+
+          if (notifySpy) {
+            expect(notifySpy).to.be.calledWith(true);
+          }
+        });
+    });
+  });
+});
diff --git a/container/test-app/compound/helloWorldWCSelfRegistered.js b/container/test-app/compound/helloWorldWCSelfRegistered.js
index 3cb53318..175088a3 100644
--- a/container/test-app/compound/helloWorldWCSelfRegistered.js
+++ b/container/test-app/compound/helloWorldWCSelfRegistered.js
@@ -5,13 +5,50 @@ class HelloWorldSelfRegistered extends HTMLElement {
   constructor() {
     super();
     const template = document.createElement('template');
-    template.innerHTML = `<section><p>Hello World (self registered)!</p></section>`;
+    template.innerHTML = `
+      <section>
+        <p>Hello World (self registered)!</p>
+        <button id="showAlert">showAlert</button><br>
+        <button id="showAlertWithCallback">showAlert (with callback)</button><br>
+        <button id="confirmationModal">showConfirmationModal</button>
+      </section>
+    `;
 
     this._shadowRoot = this.attachShadow({
       mode: 'open',
       delegatesFocus: false
     });
     this._shadowRoot.appendChild(template.content.cloneNode(true));
+    this.$button = this._shadowRoot.querySelector('#showAlert');
+    this.$button.addEventListener('click', () => {
+      this.LuigiClient.uxManager().showAlert({
+        text: 'Nested WC - alert message',
+        type: 'info'
+      });
+    });
+    this.$button2 = this._shadowRoot.querySelector('#showAlertWithCallback');
+    this.$button2.addEventListener('click', () => {
+      this.LuigiClient.uxManager().showAlert({
+        callback: true,
+        text: 'Nested WC - alert message',
+        type: 'info'
+      });
+    });
+    this.$button3 = this._shadowRoot.querySelector('#confirmationModal');
+    this.$button3.addEventListener('click', () => {
+      const settings = {
+        type: 'confirmation',
+        header: 'Nested confirmation',
+        body: 'Are you sure you want to do this?',
+        buttonConfirm: 'Yes',
+        buttonDismiss: 'No'
+      };
+
+      this.LuigiClient.uxManager()
+        .showConfirmationModal(settings)
+        .then(() => console.info('Nested WC - modal confirmed'))
+        .catch(() => console.info('Nested WC - modal dissmissed'));
+    });
   }
 }
 window.Luigi._registerWebcomponent(
diff --git a/container/test-app/compound/nested.html b/container/test-app/compound/nested.html
index b0de068f..31aa14ac 100644
--- a/container/test-app/compound/nested.html
+++ b/container/test-app/compound/nested.html
@@ -27,60 +27,87 @@
             viewUrl: layoutMFEDomain + '/panelHeader.js',
             context: {
               title: 'My Awesome Grid',
-              description: 'Really awesome'
+              description: 'Really awesome',
             },
             layoutConfig: {
-              slot: 'header'
+              slot: 'header',
             },
             eventListeners: [
               {
                 source: 'input1',
                 name: 'sendInput',
                 action: 'update',
-                dataConverter: data => {
+                dataConverter: (data) => {
                   return 'new text: ' + data;
-                }
-              }
-            ]
+                },
+              },
+            ],
           },
           {
             viewUrl: 'https://luigiwebcomponents.gitlab.io/luigi-wc-mfe/main.js',
             context: {
-              label: 'Another web component'
+              label: 'Another web component',
             },
             layoutConfig: {
-              slot: 'content'
-            }
+              slot: 'content',
+            },
           },
           {
             viewUrl: './helloWorldWCSelfRegistered.js',
             webcomponent: {
-              selfRegistered: true
+              selfRegistered: true,
             },
             layoutConfig: {
-              slot: 'helloWorldSelfRegistered'
-            }
+              slot: 'helloWorldSelfRegistered',
+            },
           },
           {
             viewUrl: layoutMFEDomain + '/panelFooter.js',
             context: {
-              footer: 'This is the end of awesomeness'
+              footer: 'This is the end of awesomeness',
             },
             layoutConfig: {
-              slot: 'footer'
-            }
-          }
-        ]
+              slot: 'footer',
+            },
+          },
+        ],
       };
 
       const INITIALIZED = 'initialized';
-      let skipInitCheckCompoundCtn = document.getElementById(
-        'nested-wc-compound-container'
+      const ALERT_REQUEST = 'show-alert-request';
+      const SHOW_CONFIRMATION_MODAL_REQUEST = 'show-confirmation-modal-request';
+      const skipInitCheckCompoundCtn = document.getElementById(
+        'nested-wc-compound-container',
       );
+
       if (!skipInitCheckCompoundCtn.initialized) {
         skipInitCheckCompoundCtn.addEventListener(INITIALIZED, () => {
           console.log('compoundTest is ready now!');
         });
+        skipInitCheckCompoundCtn.addEventListener(ALERT_REQUEST, (event) => {
+          const data = event.detail;
+
+          alert(data.text);
+
+          if (data.callback && typeof event['callback'] === 'function') {
+            event['callback']();
+          } else {
+            skipInitCheckCompoundCtn.notifyAlertClosed(data.id);
+          }
+        });
+        skipInitCheckCompoundCtn.addEventListener(
+          SHOW_CONFIRMATION_MODAL_REQUEST,
+          (event) => {
+            const data = event.detail;
+            const val = confirm(data.body);
+
+            if (!val && typeof event['callback'] === 'function') {
+              event['callback'](val);
+            } else {
+              skipInitCheckCompoundCtn.notifyConfirmationModalClosed(val);
+            }
+          },
+        );
       }
     </script>
   </body>
diff --git a/container/test-app/compound/nested.js b/container/test-app/compound/nested.js
index 445d4bac..4044431e 100644
--- a/container/test-app/compound/nested.js
+++ b/container/test-app/compound/nested.js
@@ -9,6 +9,9 @@ export default class extends LuigiElement {
         <section style="display: flex; height: 100%; flex-direction: column">
           <button id="addNodeParams">addNodeParams</button>
           <button id="getNodeParams">getNodeParams</button>
+          <button id="showAlert">showAlert</button>
+          <button id="showAlertWithCallback">showAlert (with callback)</button>
+          <button id="confirmationModal">showConfirmationModal</button>
           <header><slot name="header">header</slot></header>
           <main style="flex: auto; overflow: auto">
             <slot name="content">content</slot>
@@ -26,6 +29,36 @@ export default class extends LuigiElement {
     this.$button2.addEventListener('click', () => {
       console.log('getNodeParams', this.LuigiClient.getNodeParams());
     });
+    this.$button3 = this._shadowRoot.querySelector('#showAlert');
+    this.$button3.addEventListener('click', () => {
+      this.LuigiClient.uxManager().showAlert({
+        text: 'Direct WC - alert message',
+        type: 'info'
+      });
+    });
+    this.$button4 = this._shadowRoot.querySelector('#showAlertWithCallback');
+    this.$button4.addEventListener('click', () => {
+      this.LuigiClient.uxManager().showAlert({
+        callback: true,
+        text: 'Direct WC - alert message',
+        type: 'info'
+      });
+    });
+    this.$button5 = this._shadowRoot.querySelector('#confirmationModal');
+    this.$button5.addEventListener('click', () => {
+      const settings = {
+        type: 'confirmation',
+        header: 'Confirmation',
+        body: 'Are you sure you want to do this?',
+        buttonConfirm: 'Yes',
+        buttonDismiss: 'No'
+      };
+
+      this.LuigiClient.uxManager()
+        .showConfirmationModal(settings)
+        .then(() => console.info('Direct WC - modal confirmed'))
+        .catch(() => console.info('Direct WC - modal dissmissed'));
+    });
   }
 
   connectedCallback() {
diff --git a/container/test/services/webcomponents.service.spec.ts b/container/test/services/webcomponents.service.spec.ts
index bcd36d65..dd5cefcb 100644
--- a/container/test/services/webcomponents.service.spec.ts
+++ b/container/test/services/webcomponents.service.spec.ts
@@ -124,10 +124,10 @@ describe('dispatchLuigiEvent', () => {
     const callback = jest.fn();
 
     // Act
-    service.dispatchLuigiEvent(msg, data, callback);
+    service.dispatchLuigiEvent(msg, data, callback, 'callback');
 
     // Assert
-    expect(dispatchSpy).toHaveBeenCalledWith(msg, service.thisComponent, data, callback);
+    expect(dispatchSpy).toHaveBeenCalledWith(msg, service.thisComponent, data, callback, 'callback');
   });
 });
 
@@ -557,7 +557,12 @@ describe('createClientAPI', () => {
       clientAPI.uxManager().showAlert(alertSettings);
 
       // assert
-      expect(dispatchEventSpy).toHaveBeenCalledWith(Events.ALERT_REQUEST, alertSettings);
+      expect(dispatchEventSpy).toHaveBeenCalledWith(
+        Events.ALERT_REQUEST,
+        alertSettings,
+        expect.any(Function),
+        'callback'
+      );
     });
 
     it('test uxManager getCurrentTheme', () => {
@@ -834,7 +839,7 @@ describe('createClientAPI', () => {
 
     // asert
     expect(eventBusPublishEventSpy).toHaveBeenCalledWith(customEvent, node_id, wc_id);
-    expect(dispatchSpy).toHaveBeenCalledWith(Events.CUSTOM_MESSAGE, undefined, expectedPayload, undefined);
+    expect(dispatchSpy).toHaveBeenCalledWith(Events.CUSTOM_MESSAGE, undefined, expectedPayload, undefined, undefined);
   });
 
   it('test publishEvent custom message with UNDEFINED eventBusElement', () => {
@@ -855,7 +860,7 @@ describe('createClientAPI', () => {
     clientAPI.publishEvent(new CustomEvent('test-event', { detail: 1 }));
 
     // assert
-    expect(dispatchSpy).toHaveBeenCalledWith(Events.CUSTOM_MESSAGE, undefined, expectedPayload, undefined);
+    expect(dispatchSpy).toHaveBeenCalledWith(Events.CUSTOM_MESSAGE, undefined, expectedPayload, undefined, undefined);
   });
 
   it('test luigiClientInit', () => {