Support toggle+widget buttons in plugin-framework

Change-Id: Ibf2b80efc1eeda2f51dedf4a7f3cd5ac67325ef9
diff --git a/dev/js/spec/pluginSpec.js b/dev/js/spec/pluginSpec.js
index 32a6e5e..107f44d 100644
--- a/dev/js/spec/pluginSpec.js
+++ b/dev/js/spec/pluginSpec.js
@@ -244,6 +244,457 @@
       KorAP.Panel["result"] = undefined;
     });
 
+    it('should accept valid registrations for setWidget with active', function () {
+      let p = KorAP.Panel["result"] = panelClass.create();
+      
+      let manager = pluginServerClass.create();
+
+      // Attach services container so addService works
+      document.body.appendChild(manager.element());
+
+      manager.register({
+        name : 'Check',
+        embed : [{
+          panel : 'result',
+          title : 'Map',
+          onClick : {
+            template : 'about:blank',
+            action : 'setWidget',
+            active : false
+          }
+        }]
+      });
+
+      let b = p.actions().element().firstChild;
+      expect(b.getAttribute("title")).toEqual("Map");
+
+      // Button should have a check element (marker-box)
+      expect(b.firstChild.classList.contains('check')).toBeTruthy();
+      expect(b.firstChild.classList.contains('button-icon')).toBeTruthy();
+
+      // active state should be set to false initially
+      expect(b.active).toBeDefined();
+      expect(b.active.get()).toBe(false);
+
+      // Check is not checked initially
+      expect(b.firstChild.classList.contains('checked')).toBeFalsy();
+
+      expect(p.element().querySelectorAll("iframe").length).toEqual(0);
+
+      // Click on check (marker-box) - loads background service,
+      // no visible widget in the panel
+      b.firstChild.click();
+
+      // No iframe in the panel - service is in the services container
+      expect(p.element().querySelectorAll("iframe").length).toEqual(0);
+      expect(manager.element().querySelectorAll("iframe").length).toEqual(1);
+
+      // No widget view element should exist in the panel
+      expect(p.element().querySelectorAll("div.view").length).toEqual(0);
+
+      // active state should have toggled to true
+      expect(b.active.get()).toBe(true);
+      expect(b.firstChild.classList.contains('checked')).toBeTruthy();
+
+      // Click on check again - should not add another service, just toggle active
+      b.firstChild.click();
+      expect(manager.element().querySelectorAll("iframe").length).toEqual(1);
+      expect(b.active.get()).toBe(false);
+      expect(b.firstChild.classList.contains('checked')).toBeFalsy();
+
+      // Click on title - should open the widget visibly
+      b.click();
+      expect(p.element().querySelectorAll("iframe").length).toEqual(1);
+      expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+
+      // Click title again - should toggle visibility (hide via state)
+      b.click();
+      expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(0);
+
+      manager.destroy();
+      KorAP.Panel["result"] = undefined;
+    });
+
+    it('should not show any view element when checkbox is clicked', function () {
+      let p = KorAP.Panel["result"] = panelClass.create();
+      let manager = pluginServerClass.create();
+
+      document.body.appendChild(manager.element());
+
+      manager.register({
+        name : 'NoBar',
+        embed : [{
+          panel : 'result',
+          title : 'Map',
+          onClick : {
+            template : 'about:blank',
+            action : 'setWidget',
+            active : false
+          }
+        }]
+      });
+
+      let b = p.actions().element().firstChild;
+
+      // Click checkbox
+      b.firstChild.click();
+
+      // No view element must exist in the panel at all —
+      // not visible, not hidden, not minimized. The checkbox
+      // must only create a background service, never a widget.
+      expect(p.element().querySelectorAll("div.view").length).toEqual(0);
+      expect(p.element().querySelectorAll("iframe").length).toEqual(0);
+
+      // The iframe must be in the services container instead
+      expect(manager.element().querySelectorAll("iframe").length).toEqual(1);
+
+      // Clicking the title afterwards must properly open the widget
+      b.click();
+      expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+      expect(p.element().querySelectorAll("iframe").length).toEqual(1);
+
+      manager.destroy();
+      KorAP.Panel["result"] = undefined;
+    });
+
+    it('should toggle active via checkbox when widget is open', function () {
+      let p = KorAP.Panel["result"] = panelClass.create();
+      let manager = pluginServerClass.create();
+
+      document.body.appendChild(manager.element());
+
+      manager.register({
+        name : 'Check',
+        embed : [{
+          panel : 'result',
+          title : 'Map',
+          onClick : {
+            template : 'about:blank',
+            action : 'setWidget',
+            active : false
+          }
+        }]
+      });
+
+      let b = p.actions().element().firstChild;
+
+      // Open the widget via title-click
+      b.click();
+      expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+      expect(b.active.get()).toBe(false);
+
+      let id = b['widgetID'];
+      expect(id).toBeDefined();
+
+      // Active association should exist on the widget
+      expect(b.active.associates()).toBeGreaterThan(0);
+
+      // Click checkbox to activate - should toggle active state
+      b.firstChild.click();
+      expect(b.active.get()).toBe(true);
+      expect(b.firstChild.classList.contains('checked')).toBeTruthy();
+
+      // Widget should still be visible
+      expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+
+      // widgetID should remain the same (no new service created)
+      expect(b['widgetID']).toEqual(id);
+
+      // Click checkbox again to deactivate
+      b.firstChild.click();
+      expect(b.active.get()).toBe(false);
+      expect(b.firstChild.classList.contains('checked')).toBeFalsy();
+
+      // Widget should still be visible
+      expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+
+      manager.destroy();
+      KorAP.Panel["result"] = undefined;
+    });
+
+    it('should not duplicate pipes when opening widget after checkbox', function () {
+      var tempPipe = KorAP.Pipe;
+      KorAP.Pipe = pipeClass.create();
+
+      let p = KorAP.Panel["result"] = panelClass.create();
+      let manager = pluginServerClass.create();
+
+      document.body.appendChild(manager.element());
+
+      manager.register({
+        name : 'PipeCheck',
+        embed : [{
+          panel : 'result',
+          title : 'Map',
+          onClick : {
+            template : 'about:blank',
+            action : 'setWidget',
+            active : false
+          }
+        }]
+      });
+
+      let b = p.actions().element().firstChild;
+
+      // Click checkbox - background service created, active becomes true
+      b.firstChild.click();
+      expect(b.active.get()).toBe(true);
+      expect(manager.element().querySelectorAll("iframe").length).toEqual(1);
+
+      // Simulate the plugin adding a pipe via the background service
+      let bgId = b['widgetID'];
+      manager._receiveMsg({
+        "data" : {
+          "originID" : bgId,
+          "action" : "pipe",
+          "job" : "add",
+          "service" : "https://mapper.example"
+        }
+      });
+      expect(KorAP.Pipe.toString()).toEqual("https://mapper.example");
+
+      // Click title - opens widget, closes background service
+      b.click();
+      expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+
+      // The new widget's plugin re-initializes and tries to add the
+      // same pipe again — the pipe system must deduplicate.
+      let wId = b['widgetID'];
+      manager._receiveMsg({
+        "data" : {
+          "originID" : wId,
+          "action" : "pipe",
+          "job" : "add",
+          "service" : "https://mapper.example"
+        }
+      });
+
+      // Pipe must still contain only one entry
+      expect(KorAP.Pipe.toString()).toEqual("https://mapper.example");
+      expect(KorAP.Pipe.size()).toEqual(1);
+
+      manager.destroy();
+      KorAP.Pipe = tempPipe;
+      KorAP.Panel["result"] = undefined;
+    });
+
+    it('should toggle checkbox visual when widget is open', function () {
+      let p = KorAP.Panel["result"] = panelClass.create();
+      let manager = pluginServerClass.create();
+
+      document.body.appendChild(manager.element());
+
+      manager.register({
+        name : 'VisualCheck',
+        embed : [{
+          panel : 'result',
+          title : 'Map',
+          onClick : {
+            template : 'about:blank',
+            action : 'setWidget',
+            active : false
+          }
+        }]
+      });
+
+      let b = p.actions().element().firstChild;
+      let check = b.firstChild;
+
+      // Open widget via title
+      b.click();
+      expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+      expect(check.classList.contains('checked')).toBeFalsy();
+
+      // Click checkbox - should visually check
+      check.click();
+      expect(b.active.get()).toBe(true);
+      expect(check.classList.contains('checked')).toBeTruthy();
+
+      // Click checkbox again - should visually uncheck
+      check.click();
+      expect(b.active.get()).toBe(false);
+      expect(check.classList.contains('checked')).toBeFalsy();
+
+      // Widget should still be visible throughout
+      expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+
+      manager.destroy();
+      KorAP.Panel["result"] = undefined;
+    });
+
+    it('should accept valid registrations for addWidget with active', function () {
+      let p = KorAP.Panel["result"] = panelClass.create();
+      
+      let manager = pluginServerClass.create();
+
+      manager.register({
+        name : 'Check',
+        embed : [{
+          panel : 'result',
+          title : 'Export',
+          onClick : {
+            template : 'about:blank',
+            action : 'addWidget',
+            active : true
+          }
+        }]
+      });
+
+      let b = p.actions().element().firstChild;
+
+      // active state should be set to true initially
+      expect(b.active).toBeDefined();
+      expect(b.active.get()).toBe(true);
+      expect(b.firstChild.classList.contains('checked')).toBeTruthy();
+
+      expect(p.element().querySelectorAll("iframe").length).toEqual(0);
+
+      // Click on check - only toggles active, no widget created
+      b.firstChild.click();
+      expect(p.element().querySelectorAll("iframe").length).toEqual(0);
+      expect(b.active.get()).toBe(false);
+      expect(b.firstChild.classList.contains('checked')).toBeFalsy();
+
+      // Click on title - should load a widget visibly
+      b.click();
+      expect(p.element().querySelectorAll("iframe").length).toEqual(1);
+
+      manager.destroy();
+      KorAP.Panel["result"] = undefined;
+    });
+
+    it('should support changeTitle on buttons', function () {
+      let p = KorAP.Panel["result"] = panelClass.create();
+      
+      let manager = pluginServerClass.create();
+
+      manager.register({
+        name : 'Check',
+        embed : [{
+          panel : 'result',
+          title : 'Map',
+          onClick : {
+            template : 'about:blank',
+            action : 'setWidget'
+          }
+        }]
+      });
+
+      let b = p.actions().element().firstChild;
+      expect(b.lastChild.textContent).toEqual("Map");
+
+      b.changeTitle("New Title");
+      expect(b.lastChild.textContent).toEqual("New Title");
+
+      manager.destroy();
+      KorAP.Panel["result"] = undefined;
+    });
+
+    it('should handle Title set message', function () {
+      let p = KorAP.Panel["result"] = panelClass.create();
+      
+      let manager = pluginServerClass.create();
+
+      manager.register({
+        name : 'TitlePlugin',
+        embed : [{
+          panel : 'result',
+          title : 'Original',
+          onClick : {
+            template : 'about:blank',
+            action : 'setWidget'
+          }
+        }]
+      });
+
+      let b = p.actions().element().firstChild;
+      expect(b.lastChild.textContent).toEqual("Original");
+
+      // Click to open widget
+      b.click();
+
+      let id = b['widgetID'];
+      expect(id).toBeDefined();
+
+      // Send Title set message
+      manager._receiveMsg({
+        "data" : {
+          "originID" : id,
+          "action" : "set",
+          "key" : "Title",
+          "value" : "Changed"
+        }
+      });
+
+      expect(b.lastChild.textContent).toEqual("Changed");
+
+      manager.destroy();
+      KorAP.Panel["result"] = undefined;
+    });
+
+    it('should handle Active get/set messages', function () {
+      let p = KorAP.Panel["result"] = panelClass.create();
+      
+      let manager = pluginServerClass.create();
+
+      manager.register({
+        name : 'ActivePlugin',
+        embed : [{
+          panel : 'result',
+          title : 'Map',
+          onClick : {
+            template : 'about:blank',
+            action : 'setWidget',
+            active : false
+          }
+        }]
+      });
+
+      let b = p.actions().element().firstChild;
+      expect(b.active.get()).toBe(false);
+
+      // Click title to open widget
+      b.click();
+
+      let id = b['widgetID'];
+      expect(id).toBeDefined();
+
+      // Get active state
+      let data = {
+        "originID" : id,
+        "action" : "get",
+        "key" : "Active"
+      };
+      manager._receiveMsg({ "data" : data });
+      expect(data.value).toBe(false);
+
+      // Set active state via message
+      manager._receiveMsg({
+        "data" : {
+          "originID" : id,
+          "action" : "set",
+          "key" : "Active",
+          "value" : true
+        }
+      });
+      expect(b.active.get()).toBe(true);
+      expect(b.firstChild.classList.contains('checked')).toBeTruthy();
+
+      // Roll active state via message (no value)
+      manager._receiveMsg({
+        "data" : {
+          "originID" : id,
+          "action" : "set",
+          "key" : "Active"
+        }
+      });
+      expect(b.active.get()).toBe(false);
+      expect(b.firstChild.classList.contains('checked')).toBeFalsy();
+
+      manager.destroy();
+      KorAP.Panel["result"] = undefined;
+    });
+
     it('should accept valid registrations for toggle', function () {
       let p = KorAP.Panel["result"] = panelClass.create();