Improve test coverage for the tutorial

Change-Id: Ibefcbcc019fee4acaa2f83ec3030e534509035ae
diff --git a/Changes b/Changes
index 32d216a..73d083d 100755
--- a/Changes
+++ b/Changes
@@ -1,4 +1,4 @@
-0.40 2020-10-13
+0.40 2020-10-20
         - Modernize ES and fix in-loops.
         - add roll() method to state object.
         - Fix wrong hint-mirror behaviour in Firefox.
@@ -6,6 +6,7 @@
         - Modernize ES and improve variable declarations.
         - Improve JS test coverage for Datepicker.
         - Fix character errors in hint helper at position 0.
+        - Improve JS test coverage for tutorial.
 
 0.39 2020-10-07
         - Add information on secret file to Readme.
diff --git a/dev/js/runner/all.html b/dev/js/runner/all.html
index 912fabd..99d4799 100644
--- a/dev/js/runner/all.html
+++ b/dev/js/runner/all.html
@@ -51,7 +51,8 @@
         'spec/utilSpec',
         'spec/stateSpec',
         'spec/pipeSpec',
-        'spec/sessionSpec'
+        'spec/sessionSpec',
+        'spec/tutSpec'
       ],
       function () {
         window.onload();
diff --git a/dev/js/spec/tutSpec.js b/dev/js/spec/tutSpec.js
new file mode 100644
index 0000000..a37900c
--- /dev/null
+++ b/dev/js/spec/tutSpec.js
@@ -0,0 +1,294 @@
+define(['tutorial','util'], function (tutClass) {
+  describe('KorAP.Tutorial', function () {
+
+    const clean = function () {
+      ['tutorial','q-field','ql-field'].forEach(
+        function (id) {
+          const el = document.getElementById(id);
+          if (el != null) {
+            el.parentNode.removeChild(el);
+          };
+        }
+      );
+    };
+    
+    beforeEach(clean);
+    afterEach(clean);
+
+    function queryFactory (query, ql = 'poliqarp', cutoff = true) {
+      const q = document.createElement('div');
+      q.setAttribute('data-query', query);
+      q.setAttribute('data-query-language', ql);
+      q.setAttribute('data-query-cutoff', cutoff);
+      return q;
+    };
+    
+    it('should be initializable', function () {
+      tutObj = document.createElement('div');
+      let tut = tutClass.create(tutObj);
+      expect(tut).toBeFalsy();
+
+      tutObj = document.createElement('div');
+      tutObj.setAttribute('href', '/doc/ql');
+      tut = tutClass.create(tutObj);
+      expect(tut).toBeTruthy();
+      tut._session.clear();
+    });
+
+    it('should rewrite to JS open', function () {
+      tutObj = document.createElement('div');
+      tutObj.setAttribute('href', 'http://example.com');
+      expect(tutObj.getAttribute('href')).toBeTruthy();
+      expect(tutObj.onclick).toBeNull();
+      var tut = tutClass.create(tutObj);
+      expect(tutObj.getAttribute('href')).toBeFalsy();
+      expect(tutObj.onclick).not.toBeNull();
+      tut._session.clear();
+    });
+
+    it('should create an embedded tutorial', function () {      
+      expect(document.getElementById('tutorial')).toBeFalsy();
+      
+      tutObj = document.createElement('div');
+      tutObj.setAttribute('href', '/doc/ql');
+      let tut = tutClass.create(tutObj);
+
+      let tutEmb = document.getElementById('tutorial');
+      expect(tutEmb).toBeTruthy();
+
+      expect(tutEmb.style.display).toEqual('none');
+      tut._session.clear();
+    });
+
+    it('should be visible', function () {
+      const tutE = document.createElement('div');
+      tutE.setAttribute('href', '/doc/ql');
+      let tut = tutClass.create(tutE);
+      tut._session.clear();
+      let tutEmb = document.getElementById('tutorial');
+
+      expect(tutEmb.style.display).toEqual('none');
+      expect(tutEmb.getElementsByTagName('IFRAME')[0]).toBeUndefined();
+
+      tut.show();
+
+      expect(tutEmb.style.display).toEqual('block');
+
+      let iframe = tutEmb.getElementsByTagName('IFRAME')[0];
+      expect(iframe).not.toBeUndefined();
+      expect(iframe.getAttribute('src')).toEqual('/doc/ql?embedded=true');
+      tut._session.clear();
+    });
+
+    it('should be visible by click', function () {
+      const tutE = document.createElement('div');
+      tutE.setAttribute('href', '/doc/ql');
+      let tut = tutClass.create(tutE);
+      tut._session.clear();
+      let tutEmb = document.getElementById('tutorial');
+
+      expect(tutEmb.style.display).toEqual('none');
+      expect(tutEmb.getElementsByTagName('IFRAME')[0]).toBeUndefined();
+
+      tutE.click();
+
+      expect(tutEmb.style.display).toEqual('block');
+
+      let iframe = tutEmb.getElementsByTagName('IFRAME')[0];
+      expect(iframe).not.toBeUndefined();
+      expect(iframe.getAttribute('src')).toEqual('/doc/ql?embedded=true');
+      tut._session.clear();
+    });
+    
+    it('should be hidable', function () {
+      const tutE = document.createElement('div');
+      tutE.setAttribute('href', '/doc/ql');
+      let tut = tutClass.create(tutE);
+      tut._session.clear();
+      let tutEmb = document.getElementById('tutorial');
+
+      tut.show();
+      expect(tutEmb.style.display).toEqual('block');
+
+      let iframe = tutEmb.getElementsByTagName('IFRAME')[0];
+      expect(iframe).not.toBeUndefined();
+
+      tut.hide();
+      expect(tutEmb.style.display).toEqual('none');
+
+      iframe = tutEmb.getElementsByTagName('IFRAME')[0];
+      expect(iframe).not.toBeUndefined();
+      tut._session.clear();
+    });
+
+    it('should remember page', function () {
+      const tutE = document.createElement('div');
+      tutE.setAttribute('href', '/doc/ql');
+      let tut = tutClass.create(tutE);
+      tut._session.clear();
+
+      expect(tut.getPage()).toBeUndefined();
+
+      tut.setPage('poliqarp');
+      expect(tut.getPage()).toEqual('poliqarp');
+
+      // Remember section
+      let sec = document.createElement('SECTION');
+      sec.setAttribute('id', 'cosmas-ii');    
+
+      tut.setPage(sec);
+      expect(tut.getPage().endsWith('#cosmas-ii')).toBeTruthy();
+
+      // Check an inner obj
+      let doc = document.createElement('div');
+
+      // Create wrappers
+      let docP = document.createElement('div');
+      docP.appendChild(sec);
+      let pre = sec = sec.addE('pre');
+      sec = sec.addE('section');
+      sec.appendChild(doc);
+
+      expect(docP.outerHTML).toEqual(
+        '<div><section id="cosmas-ii"><pre><section><div>' +
+          '</div></section></pre></section></div>'
+      );
+
+      tut.setPage(doc);
+      expect(tut.getPage().endsWith('#cosmas-ii')).toBeTruthy();
+
+      pre.setAttribute('id', 'middle');
+
+      tut.setPage(doc);
+      expect(tut.getPage().endsWith('#middle')).toBeTruthy();
+      tut._session.clear();
+    });
+
+    it('should enable embedded queries', function () {
+
+      // qField
+      const qField = document.createElement('input');
+      qField.setAttribute('id','q-field');
+      qField.value = 'xxx';
+      document.body.appendChild(qField);
+
+      // qlSelect
+      const qlField = document.createElement('select');
+      qlField.setAttribute('id','ql-field');
+      let opt = qlField.addE('option');
+      opt.setAttribute('value', 'poliqarp');
+      opt = qlField.addE('option');
+      opt.setAttribute('value', 'cosmas-ii');
+      opt.selected = true;
+      document.body.appendChild(qlField);
+
+      expect(qlField.options[qlField.selectedIndex].value).toEqual('cosmas-ii');
+
+      const tutE = document.createElement('div');
+      tutE.setAttribute('href', '/doc/ql');
+
+      let tut = tutClass.create(tutE);
+      tut._session.clear();
+
+      let tutEmb = document.getElementById('tutorial');
+      expect(tutEmb).toBeTruthy();
+
+      expect(tutEmb.style.display).toEqual('none');
+
+      tut.show();
+
+      expect(tutEmb.style.display).toEqual('block');
+      expect(qField.value).toEqual("xxx");
+      
+      let q = queryFactory("[orth=works]");
+      tut.useQuery(q);
+      expect(qField.value).toEqual("[orth=works]");
+      expect(qlField.options[qlField.selectedIndex].value).toEqual('poliqarp');
+      expect(tutEmb.style.display).toEqual('none');
+      tut._session.clear();
+    });
+
+    it('should initialize queries', function () {
+      const tutE = document.createElement('div');
+      tutE.setAttribute('href', '/doc/ql');
+      let tut = tutClass.create(tutE);
+      tut._session.clear();
+      let tutEmb = document.getElementById('tutorial');
+
+      let queries = document.createElement('div');
+
+      let pre0 = document.createElement('div');
+      queries.appendChild(pre0);
+      
+      let pre1 = document.createElement('pre');
+      pre1.classList.add('query','tutorial');
+      queries.appendChild(pre1);
+      
+      let pre2 = document.createElement('pre');
+      pre2.classList.add('query','tutorial','unsupported');
+      queries.appendChild(pre2);
+
+      let pre3 = document.createElement('div');
+      queries.appendChild(pre3);
+
+      let pre4 = document.createElement('pre');
+      pre4.classList.add('query','tutorial');
+      queries.appendChild(pre4);
+
+      expect(pre0.onclick).toBeNull();
+      expect(pre1.onclick).toBeNull();
+      expect(pre2.onclick).toBeNull();
+      expect(pre3.onclick).toBeNull();
+      expect(pre4.onclick).toBeNull();
+      
+      tut.initQueries(queries);
+
+      expect(pre0.onclick).toBeNull();
+      expect(pre1.onclick).not.toBeNull();
+      expect(pre2.onclick).toBeNull();
+      expect(pre3.onclick).toBeNull();
+      expect(pre4.onclick).not.toBeNull();      
+      tut._session.clear();
+    });
+
+    it('should initialize doc-links', function () {
+      const tutE = document.createElement('div');
+      tutE.setAttribute('href', '/doc/ql');
+      let tut = tutClass.create(tutE);
+      tut._session.clear();
+      let tutEmb = document.getElementById('tutorial');
+
+      let docLinks = document.createElement('div');
+
+      let a;
+      let l1 = a = docLinks.addE('a');
+      a.setAttribute('href','example:1');
+      let l2 = a = docLinks.addE('a');
+      a.setAttribute('href','example:2');
+      a.classList.add('doc-link');
+      let l3 = a = docLinks.addE('a');
+      a.setAttribute('href','example:3');
+      let l4 = a = docLinks.addE('a');
+      a.setAttribute('href','example:4');
+      a.classList.add('doc-link');
+
+      expect(l1.onclick).toBeNull();
+      expect(l2.onclick).toBeNull();
+      expect(l3.onclick).toBeNull();
+      expect(l4.onclick).toBeNull();
+
+      tut.initDocLinks(docLinks);
+
+      expect(l1.onclick).toBeNull();
+      expect(l2.onclick).not.toBeNull();
+      expect(l3.onclick).toBeNull();
+      expect(l4.onclick).not.toBeNull();
+
+      // Click
+      expect(tut.getPage()).toEqual(undefined);
+      l2.onclick();
+      expect(tut.getPage()).toEqual('example:2');
+      tut._session.clear();
+    });
+  });
+});
diff --git a/dev/js/src/tutorial.js b/dev/js/src/tutorial.js
index c2f7c69..ee311dd 100644
--- a/dev/js/src/tutorial.js
+++ b/dev/js/src/tutorial.js
@@ -6,14 +6,18 @@
 // TODO: Add query mechanism!
 // TODO: Highlight current section:
 //       http://stackoverflow.com/questions/24887258/highlight-navigation-link-as-i-scroll-down-the-page
+"use strict";
+
 define(['session','buttongroup','util'], function (sessionClass, buttonGroupClass) {
-  "use strict";
 
   // Localization values
   const loc   = KorAP.Locale;
   loc.CLOSE = loc.CLOSE || 'Close';
 
+  const d = document;
+  
   return {
+
     /**
      * Create new tutorial object.
      * Accepts an element to bind the tutorial window to.
@@ -24,41 +28,48 @@
       return Object.create(this)._init(obj,session);
     },
 
+
     // Initialize Tutorial object
     _init : function (obj, session) {
+      const t = this;
 
       if (session === undefined) {
-	      this._session = sessionClass.create();
+	      t._session = sessionClass.create();
       }
       else {
-	      this._session = session;
+	      t._session = session;
       };
     
-
       if (obj) {
-	      this._show = obj;
-	      this.start = obj.getAttribute('href');
+	      t._show = obj;
+	      t.start = obj.getAttribute('href');
+
+        // Unknown which tutorial to show
+        if (!t.start)
+          return null;
+        
 	      obj.removeAttribute('href');
-	      var that = this;
+
 	      obj.onclick = function () {
-	        that.show();
-	      };
+	        this.show();
+	      }.bind(t);
 
 	      // Injects a tutorial div to the body
-	      var div = document.createElement('div');
+	      const div = d.createElement('div');
 	      div.setAttribute('id', 'tutorial');
 	      div.style.display = 'none';
-	      document.getElementsByTagName('body')[0].appendChild(div);
-	      this._iframe = null;
-	      this._element = div;
+	      d.getElementsByTagName('body')[0].appendChild(div);
+
+	      t._iframe = null;
+	      t._element = div;
 
 	      // Some fields
-	      this._ql     = document.getElementById("ql-field");
-	      this._q      = document.getElementById("q-field")
-	      this._cutoff = document.getElementById("q-cutoff-field");
+	      t._ql     = d.getElementById("ql-field");
+	      t._q      = d.getElementById("q-field")
+	      t._cutoff = d.getElementById("q-cutoff-field");
       };
 
-      return this;
+      return t;
     },
 
 
@@ -66,41 +77,43 @@
      * Initialize a search with a defined query.
      */
     useQuery : function (e) {
-      var q  = e.getAttribute("data-query");
-      var ql = e.getAttribute("data-query-language");
-      var qc = e.getAttribute("data-query-cutoff");
+      const t = this;
+      const q  = e.getAttribute("data-query"),
+            ql = e.getAttribute("data-query-language"),
+            qc = e.getAttribute("data-query-cutoff");
+
       if (qc !== 0 && qc !== "0" && qc !== "off" && qc !== null) {
-	      this._cutoff.checked = true;
+        if (t._cuttoff)
+	        t._cutoff.checked = true;
       };
 
       if (KorAP.QLmenu) {
         KorAP.QLmenu.selectValue(ql);
-      };
-      /*
-      var qlf = this._ql.options;
-      for (var i in qlf) {
-	      if (qlf[i].value == ql) {
-	        qlf[i].selected = true;
-          break;
-	      };
-      };
-      */
+      }
 
-      this._q.value = q;
-      this.setPage(e);
-      this.hide();
+      else if (t._ql) {
+        let found = Array.from(t._ql.options).find(o => o.value === ql);
+        if (found)
+          found.selected = true;
+      };
+
+      if (t._q)
+        t._q.value = q;
+
+      t.setPage(e);
+      t.hide();
     },
 
+
     /**
      * Decorate a page with query event handler.
      */
     initQueries : function (d) {
-      let that = this;
       Array.from(d.querySelectorAll('pre.query.tutorial:not(.unsupported)')).forEach(
         i =>
 	        i.onclick = function (e) {
-	          that.useQuery(this,e);
-	        }
+	          this.useQuery(this,e);
+	        }.bind(this)
       );
     },
 
@@ -108,14 +121,14 @@
      * Decorate a page with documentation links
      */
     initDocLinks : function (d) {
-      let that = this;
+      const that = this;
       Array.from(d.getElementsByClassName('doc-link')).forEach(
 	      i =>
-          i.onclick = function (e) {
+          i.onclick = function () {
 	          that.setPage(this.getAttribute('href'));
 	          return true;
 	        }
-      );    
+      );
     },
 
 
@@ -123,25 +136,26 @@
      * Show the tutorial page embedded.
      */
     show : function () {
-      var element = this._element;
+      const t = this;
+      const element = t._element;
       if (element.style.display === 'block')
 	      return;
 
-      if (this._iframe === null) {
-	      this._iframe = document.createElement('iframe');
-	      this._iframe.setAttribute(
+      if (t._iframe === null) {
+	      t._iframe = d.createElement('iframe');
+	      t._iframe.setAttribute(
 	        'src',
-	        (this.getPage() || this.start) + '?embedded=true'
+	        (t.getPage() || t.start) + '?embedded=true'
 	      );
 
-        var btn = buttonGroupClass.create(
+        const btn = buttonGroupClass.create(
           ['action','button-view']
         );
 
-        var that = this;
         btn.add(loc.CLOSE, {'cls':['button-icon','close']}, function () {
           element.style.display = 'none';
         });
+
         element.appendChild(btn.element());
 
 	      // Add open in new window button
@@ -152,19 +166,17 @@
 	        .appendChild(document.createTextNode(loc.SHOWINFO));
 	        info.classList.add('info');
 	        info.setAttribute('title', loc.SHOWINFO);
-	      */
 
-        /*
-	      ul.appendChild(close);
-
-	      element.appendChild(ul);
+	        ul.appendChild(close);
+	        element.appendChild(ul);
         */
-	      element.appendChild(this._iframe);
+	      element.appendChild(t._iframe);
       };
 
       element.style.display = 'block';
     },
 
+
     /**
      * Close tutorial window.
      */
@@ -172,34 +184,36 @@
       this._element.style.display = 'none';
     },
 
+
     /**
      * Set a page to be the current tutorial page.
      * Expects either a string or an element.
      */
     setPage : function (obj) {
-      var page = obj;
+      let page = obj;
+
       if (typeof page != 'string') {
-	      var l = this._iframe !== null ? window.frames[0].location : window.location;
+	      const l = this._iframe !== null ? window.frames[0].location : window.location;
+
 	      page = l.pathname + l.search;
 
-	      for (var i = 1; i < 5; i++) {
-	        if (obj.nodeName === 'SECTION') {
-	          if (obj.hasAttribute('id'))
-	            page += '#' + obj.getAttribute('id');
-	          break;
-	        }
-	        else if (obj.nodeName === 'PRE' && obj.hasAttribute('id')) {
+	      for (let i = 1; i < 5; i++) {
+	        if ((obj.nodeName === 'SECTION' || obj.nodeName === 'PRE') && obj.hasAttribute('id')) {
 	          page += '#' + obj.getAttribute('id');
 	          break;
 	        }
 	        else {
 	          obj = obj.parentNode;
+            if (obj === null)
+              break;
 	        };
 	      };
       };
+
       this._session.set('tutpage', page);
     },
 
+
     /**
      * Get the current tutorial URL
      */