From 5fc8016dc02a5c521e96d0154fc24f8a38f22bb3 Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Tue, 12 Feb 2019 11:39:31 +0100
Subject: [PATCH] WIP; e2e

---
 e2e/src/iv.e2e-spec.js                        | 98 +++++++++++++++++++
 e2e/src/ivApi.js                              | 11 +++
 e2e/src/niftilayer.e2e-spec.js                | 25 -----
 e2e/src/noErrorLog.js                         | 14 +++
 e2e/src/util.js                               | 20 ++++
 package.json                                  |  3 +-
 .../atlasViewer.constantService.service.ts    | 11 ++-
 .../atlasViewer.urlService.service.ts         | 65 +++++++++---
 src/res/cvtLyon.js                            | 23 +++++
 src/res/ext/test.json                         | 14 +++
 src/res/ext/testNehubaConfig.json             | 66 +++++++++++++
 src/ui/banner/banner.component.ts             |  3 +
 .../nehubaContainer.component.ts              |  9 ++
 .../nehubaViewer/nehubaViewer.component.ts    | 10 +-
 14 files changed, 329 insertions(+), 43 deletions(-)
 create mode 100644 e2e/src/iv.e2e-spec.js
 create mode 100644 e2e/src/ivApi.js
 delete mode 100644 e2e/src/niftilayer.e2e-spec.js
 create mode 100644 e2e/src/noErrorLog.js
 create mode 100644 e2e/src/util.js
 create mode 100644 src/res/cvtLyon.js
 create mode 100644 src/res/ext/test.json
 create mode 100644 src/res/ext/testNehubaConfig.json

diff --git a/e2e/src/iv.e2e-spec.js b/e2e/src/iv.e2e-spec.js
new file mode 100644
index 000000000..34fdb7c38
--- /dev/null
+++ b/e2e/src/iv.e2e-spec.js
@@ -0,0 +1,98 @@
+const url = 'http://localhost:8081/'
+
+const noErrorLog = require('./noErrorLog')
+const { getSelectedTemplate, getSelectedParcellation } = require('./ivApi')
+const { getSearchParam, wait } = require('./util')
+
+describe('protractor works', () => {
+  it('protractor works', () => {
+    expect(true).toEqual(true)
+  })
+})
+
+describe('Home screen', () => {
+  beforeEach(() => {
+    browser.waitForAngularEnabled(false)
+    browser.get(url)
+  })
+
+  it('get title works', () => {
+    browser.getTitle()
+      .then(title => {
+        expect(title).toEqual('Interactive Atlas Viewer')
+      })
+      .catch(error => {
+        console.error('error', error)
+      })
+
+    browser.executeScript('window.interactiveViewer')
+      .then(result => expect(result).toBeDefined())
+    browser.executeScript('window.viewer')
+      .then(result => expect(result).toBeNull())
+
+    noErrorLog(browser)
+  })
+})
+
+describe('Query param: ', () => {
+
+  it('correctly defined templateSelected and selectedParcellation works', async () => {
+
+    const searchParam = '?templateSelected=MNI+Colin+27&parcellationSelected=JuBrain+Cytoarchitectonic+Atlas'
+    browser.get(url + searchParam)
+    browser.executeScript('window.interactiveViewer').then(result => expect(result).toBeDefined())
+    browser.executeScript('window.viewer').then(result => expect(result).toBeDefined())
+    
+    await wait(browser)
+
+    getSelectedTemplate(browser)
+      .then(template => {
+        expect(template.name).toBe('MNI Colin 27')
+      })
+
+    getSelectedParcellation(browser)
+      .then(parcellation => {
+        expect(parcellation.name).toBe('JuBrain Cytoarchitectonic Atlas')
+      })
+
+    noErrorLog(browser)
+  })
+
+  it('correctly defined templateSelected but incorrectly defined selectedParcellation work', async () => {
+    const searchParam = '?templateSelected=MNI+Colin+27&parcellationSelected=NoName'
+    browser.get(url + searchParam)
+
+    await wait(browser)
+
+    getSelectedTemplate(browser)
+      .then(template => {
+        expect(template.name).toBe('MNI Colin 27')
+      })
+      .catch(fail)
+
+    Promise.all([
+      getSelectedTemplate(browser),
+      getSelectedParcellation(browser)  
+    ])
+      .then(([template, parcellation]) => {
+        expect(parcellation.name).toBe(template.parcellations[0].name)
+      })
+      .catch(fail)
+      
+    noErrorLog(browser)
+  })
+
+  it('incorrectly defined templateSelected should clear searchparam', async () => {
+    const searchParam = '?templateSelected=NoName2&parcellationSelected=NoName'
+    browser.get(url + searchParam)
+
+    await wait(browser)
+
+    getSearchParam(browser)
+      .then(searchParam => {
+        const templateSelected = searchParam.get('templateSelected')
+        expect(templateSelected).toBeNull()
+      })
+      .catch(fail)
+  })
+})
\ No newline at end of file
diff --git a/e2e/src/ivApi.js b/e2e/src/ivApi.js
new file mode 100644
index 000000000..825bee506
--- /dev/null
+++ b/e2e/src/ivApi.js
@@ -0,0 +1,11 @@
+exports.getSelectedTemplate = (browser) => new Promise((resolve, reject) => {
+  browser.executeAsyncScript('let sub = window.interactiveViewer.metadata.selectedTemplateBSubject.subscribe(obj => arguments[arguments.length - 1](obj));sub.unsubscribe()')
+    .then(resolve)
+    .catch(reject)
+})
+
+exports.getSelectedParcellation = (browser) => new Promise((resolve, reject) => {
+  browser.executeAsyncScript('let sub = window.interactiveViewer.metadata.selectedParcellationBSubject.subscribe(obj => arguments[arguments.length - 1](obj));sub.unsubscribe()')
+    .then(resolve)
+    .catch(reject)
+})
\ No newline at end of file
diff --git a/e2e/src/niftilayer.e2e-spec.js b/e2e/src/niftilayer.e2e-spec.js
deleted file mode 100644
index 0fab4ed54..000000000
--- a/e2e/src/niftilayer.e2e-spec.js
+++ /dev/null
@@ -1,25 +0,0 @@
-const url = 'http://localhost:8080/'
-
-describe('protractor works', () => {
-  it('getting title works', () => {
-    browser.get(url)
-    browser.getTitle().then(function(title){
-      expect(title).toBe('Interactive Atlas Viewer')
-      browser.executeScript('window.interactiveViewer').then(result => expect(result).toBeDefined())
-      browser.executeScript('window.viewer').then(result => expect(result).toBeNull())
-    })
-  })
-})
-
-describe('URL works', () => {
-
-  it('templateSelected existing template works', () => {
-
-    const searchParam = '?templateSelected=MNI+Colin+27'
-    browser.get(url + searchParam)
-    browser.executeScript('window.interactiveViewer').then(result => expect(result).toBeDefined())
-    browser.executeScript('window.viewer').then(result => expect(result).toBeDefined())
-    browser.executeAsyncScript('window.interactiveViewer.metadata.selectedTemplateBSubject.subscribe(obj => arguments[arguments.length - 1](obj))')
-      .then(obj => expect(obj.name).toBe('MNI Colin 27'))
-  })
-})
\ No newline at end of file
diff --git a/e2e/src/noErrorLog.js b/e2e/src/noErrorLog.js
new file mode 100644
index 000000000..d7bc1ef7d
--- /dev/null
+++ b/e2e/src/noErrorLog.js
@@ -0,0 +1,14 @@
+module.exports = (browser) => {
+  if (!browser) {
+    fail('browser not defined')
+    throw new Error('browser not defined')
+  }
+  browser.manage().logs().get('browser')
+    .then(browserLogs => {
+      const errorLogs = browserLogs
+        .filter(log => !(/favicon/.test(log.message)))
+        .filter(log => log.level.value > 900)
+      expect(errorLogs.length).toEqual(0)
+    })
+    .catch(fail)
+}
\ No newline at end of file
diff --git a/e2e/src/util.js b/e2e/src/util.js
new file mode 100644
index 000000000..030f2ccad
--- /dev/null
+++ b/e2e/src/util.js
@@ -0,0 +1,20 @@
+const {URLSearchParams} = require('url')
+
+exports.getSearchParam = (browser) => {
+  const script = `
+  const search = window.location.search;
+  return search
+  `
+  return browser.executeScript(script)
+    .then(search => new URLSearchParams(search))
+}
+
+exports.wait = (browser) => new Promise(resolve => {
+
+  /**
+   * TODO figure out how to wait until the select parcellation is populated
+   */
+    
+  browser.sleep(1000)
+    .then(resolve)
+})
\ No newline at end of file
diff --git a/package.json b/package.json
index 1ed24602b..de42925f1 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,8 @@
     "dev-server-aot": "PRODUCTION=true GIT_HASH=`git log --pretty=format:'%h' --invert-grep --grep=^.ignore -1` webpack-dev-server --config webpack.aot.js",
     "dev-server-all-interfaces": "webpack-dev-server --config webpack.dev.js --mode development --hot --host 0.0.0.0",
     "serve-plugins": "node src/plugin_examples/server.js",
-    "test": "karma start spec/karma.conf.js"
+    "test": "karma start spec/karma.conf.js",
+    "e2e": "protractor e2e/protractor.conf"
   },
   "keywords": [],
   "author": "",
diff --git a/src/atlasViewer/atlasViewer.constantService.service.ts b/src/atlasViewer/atlasViewer.constantService.service.ts
index 594b28c22..cc1e6b0bf 100644
--- a/src/atlasViewer/atlasViewer.constantService.service.ts
+++ b/src/atlasViewer/atlasViewer.constantService.service.ts
@@ -55,7 +55,8 @@ export class AtlasViewerConstantsServices{
     'res/json/colin.json',
     'res/json/MNI152.json',
     'res/json/waxholmRatV2_0.json',
-    'res/json/allenMouse.json'
+    'res/json/allenMouse.json',
+    'res/json/test.json'
   ]
 
   /* to be provided by KG in future */
@@ -189,6 +190,14 @@ Interactive atlas viewer requires **webgl2.0**, and the \`EXT_color_buffer_float
       raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}">${this.repoUrl}</a>`
   }
 
+  incorrectParcellationNameSearchParam(title) {
+    return `The selected parcellation - ${title} - is not available. The the first parcellation of the template is selected instead.`
+  }
+
+  incorrectTemplateNameSearchParam(title) {
+    return `The selected template - ${title} - is not available.`
+  }
+
   private supportEmailAddress = `x.gui@fz-juelich.de`
   private repoUrl = `https://github.com/HumanBrainProject/interactive-viewer`
 
diff --git a/src/atlasViewer/atlasViewer.urlService.service.ts b/src/atlasViewer/atlasViewer.urlService.service.ts
index 9087930a3..5aa63426f 100644
--- a/src/atlasViewer/atlasViewer.urlService.service.ts
+++ b/src/atlasViewer/atlasViewer.urlService.service.ts
@@ -6,6 +6,7 @@ import { filter, map, scan, distinctUntilChanged, skipWhile, take } from "rxjs/o
 import { getActiveColorMapFragmentMain } from "../ui/nehubaContainer/nehubaContainer.component";
 import { PluginServices } from "./atlasViewer.pluginService.service";
 import { AtlasViewerConstantsServices } from "./atlasViewer.constantService.service";
+import { ToastService } from "src/services/toastService.service";
 
 declare var window
 
@@ -21,7 +22,8 @@ export class AtlasViewerURLService{
   constructor(
     private store : Store<ViewerStateInterface>,
     private pluginService : PluginServices,
-    private constantService:AtlasViewerConstantsServices
+    private constantService:AtlasViewerConstantsServices,
+    private toastService: ToastService
   ){
 
     this.pluginState$ = this.store.pipe(
@@ -85,28 +87,65 @@ export class AtlasViewerURLService{
       /* first, check if any template and parcellations are to be loaded */
       const searchedTemplatename = searchparams.get('templateSelected')
       const searchedParcellationName = searchparams.get('parcellationSelected')
+
+      if (!searchedTemplatename) {
+        const urlString = window.location.href
+        /**
+         * TODO think of better way of doing this
+         */
+        history.replaceState(null, '', urlString.split('?')[0])
+        return
+      }
       
       const templateToLoad = fetchedTemplates.find(template=>template.name === searchedTemplatename)
-      if(!templateToLoad)
+      if (!templateToLoad) {
+        this.toastService.showToast(
+          this.constantService.incorrectTemplateNameSearchParam(searchedTemplatename),
+          {
+            timeout: 5000
+          }
+        )
+        const urlString = window.location.href
+        /**
+         * TODO think of better way of doing this
+         */
+        history.replaceState(null, '', urlString.split('?')[0])
         return
-      const parcellationToLoad = templateToLoad ? 
-        templateToLoad.parcellations.find(parcellation=>parcellation.name === searchedParcellationName) :
-        templateToLoad.parcellations[0]
+      }
+
+      /**
+       * TODO if search param of either template or parcellation is incorrect, wrong things are searched
+       */
+      const parcellationToLoad = templateToLoad.parcellations.find(parcellation=>parcellation.name === searchedParcellationName)
+
+      if (!parcellationToLoad) {
+        this.toastService.showToast(
+          this.constantService.incorrectParcellationNameSearchParam(searchedParcellationName),
+          {
+            timeout: 5000
+          }
+        )
+      }
       
       this.store.dispatch({
         type : NEWVIEWER,
         selectTemplate : templateToLoad,
-        selectParcellation : parcellationToLoad
+        selectParcellation : parcellationToLoad || templateToLoad.parcellations[0]
       })
 
       /* selected regions */
-      const labelIndexMap = getLabelIndexMap(parcellationToLoad.regions)
-      const selectedRegions = searchparams.get('regionsSelected')
-      if(selectedRegions){
-        this.store.dispatch({
-          type : SELECT_REGIONS,
-          selectRegions : selectedRegions.split('_').map(labelIndex=>labelIndexMap.get(Number(labelIndex)))
-        })
+      if (parcellationToLoad && parcellationToLoad.regions) {
+        /**
+         * either or both parcellationToLoad and .regions maybe empty
+         */
+        const labelIndexMap = getLabelIndexMap(parcellationToLoad.regions)
+        const selectedRegions = searchparams.get('regionsSelected')
+        if(selectedRegions){
+          this.store.dispatch({
+            type : SELECT_REGIONS,
+            selectRegions : selectedRegions.split('_').map(labelIndex=>labelIndexMap.get(Number(labelIndex)))
+          })
+        }
       }
       
       /* now that the parcellation is loaded, load the navigation state */
diff --git a/src/res/cvtLyon.js b/src/res/cvtLyon.js
new file mode 100644
index 000000000..073d04bca
--- /dev/null
+++ b/src/res/cvtLyon.js
@@ -0,0 +1,23 @@
+const fs = require('fs')
+fs.readFile(
+  './ext/***REMOVED***.json', 
+  // './ext/***REMOVED***.json', 
+  'utf-8', (err, data) => {
+  const json = JSON.parse(data)
+  const newFile = json.map(item => {
+    return {
+      name: item.name.replace('DESj', 'item2'),
+      // name: item.name.replace('BATg', 'item1'),
+      templateSpace: item.templateSpace,
+      geometry: item.geometry
+    }
+  })
+
+  fs.writeFile(
+    './raw/exportForOliver/item2.json', 
+    // './raw/exportForOliver/item1.json', 
+    JSON.stringify(newFile), 'utf-8', err => {
+    if (err) throw err
+    console.log('done')
+  })
+})
\ No newline at end of file
diff --git a/src/res/ext/test.json b/src/res/ext/test.json
new file mode 100644
index 000000000..9d9e02858
--- /dev/null
+++ b/src/res/ext/test.json
@@ -0,0 +1,14 @@
+{
+  "name": "test",
+  "type": "template",
+  "species": "Human",
+  "useTheme": "dark",
+  "ngId": "greyscale",
+  "nehubaConfigURL": "res/json/testNehubaConfig.json",
+  "parcellations": [
+  ],
+  "properties": {
+    "name": "test",
+    "description": "test description"
+  }
+}
\ No newline at end of file
diff --git a/src/res/ext/testNehubaConfig.json b/src/res/ext/testNehubaConfig.json
new file mode 100644
index 000000000..2a42671d7
--- /dev/null
+++ b/src/res/ext/testNehubaConfig.json
@@ -0,0 +1,66 @@
+{
+  "globals": {
+    "hideNullImageValues": true,
+    "useNehubaLayout": true,
+    "useNehubaMeshLayer": true,
+    "useCustomSegmentColors": true
+  },
+  "zoomWithoutCtrl": true,
+  "hideNeuroglancerUI": true,
+  "rightClickWithCtrl": true,
+  "rotateAtViewCentre": true,
+  "zoomAtViewCentre": true,
+  "enableMeshLoadingControl": true,
+  "layout": {
+    "useNehubaPerspective": {
+      "fixedZoomPerspectiveSlices": {
+        "sliceViewportWidth": 300,
+        "sliceViewportHeight": 300,
+        "sliceZoom": 724698.1843689409,
+        "sliceViewportSizeMultiplier": 2
+      },
+      "centerToOrigin": true,
+      "mesh": {
+        "removeBasedOnNavigation": true,
+        "flipRemovedOctant": true,
+        "surfaceParcellation": true
+      },
+      "removePerspectiveSlicesBackground": {
+        "mode": "=="
+      },
+      "waitForMesh": true,
+      "drawSubstrates": {
+        "color": [
+          0.5,
+          0.5,
+          1,
+          0.2
+        ]
+      },
+      "drawZoomLevels": {
+        "cutOff": 150000
+      },
+      "restrictZoomLevel": {
+        "minZoom": 2500000,
+        "maxZoom": 3500000
+      }
+    }
+  },
+  "dataset": {
+    "imageBackground": [
+      0,
+      0,
+      0,
+      1
+    ],
+    "initialNgState": {
+      "showDefaultAnnotations": false,
+      "layers": {
+        "greyscale": {
+          
+          "source": "dvid://http://dvid-dev.humanbrainproject.org:/5b666a3cc19344f29025c65e829f98f2/grayscale"
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/ui/banner/banner.component.ts b/src/ui/banner/banner.component.ts
index 29088073b..fc75046e2 100644
--- a/src/ui/banner/banner.component.ts
+++ b/src/ui/banner/banner.component.ts
@@ -132,6 +132,9 @@ export class AtlasBanner implements OnDestroy, OnInit {
   }
 
   handleParcellationChange(parcellation) {
+    if (!(parcellation && parcellation.regions)) {
+      return
+    }
     this.selectedParcellation = parcellation
     this.regionsLabelIndexMap = getLabelIndexMap(parcellation.regions)
   }
diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts
index 560b8c825..f97f44518 100644
--- a/src/ui/nehubaContainer/nehubaContainer.component.ts
+++ b/src/ui/nehubaContainer/nehubaContainer.component.ts
@@ -353,6 +353,9 @@ export class NehubaContainer implements OnInit, OnDestroy{
         map(state => {
           const deepCopiedState = JSON.parse(JSON.stringify(state))
           const navigation = deepCopiedState.templateSelected.nehubaConfig.dataset.initialNgState.navigation
+          if (!navigation) {
+            return deepCopiedState
+          }
           navigation.zoomFactor = calculateSliceZoomFactor(navigation.zoomFactor)
           deepCopiedState.templateSelected.nehubaConfig.dataset.initialNgState.navigation = navigation
           return deepCopiedState
@@ -646,6 +649,12 @@ export class NehubaContainer implements OnInit, OnDestroy{
   }
 
   private handleParcellation(parcellation:any){
+    /**
+     * parcellaiton may be undefined
+     */
+    if ( !(parcellation && parcellation.regions)) {
+      return
+    }
     this.regionsLabelIndexMap = getLabelIndexMap(parcellation.regions)
     this.nehubaViewer.regionsLabelIndexMap = this.regionsLabelIndexMap
 
diff --git a/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts b/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts
index 3d46a0226..b20c2edb8 100644
--- a/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts
+++ b/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts
@@ -80,9 +80,12 @@ export class NehubaViewerUnit implements OnDestroy{
 
     this.constantService.loadExportNehubaPromise
       .then(() => {
-        const { sliceZoom, sliceViewportWidth, sliceViewportHeight } = this.config.layout.useNehubaPerspective.fixedZoomPerspectiveSlices
-        const dim = Math.min(sliceZoom * sliceViewportWidth, sliceZoom * sliceViewportHeight)
-        this._dim = [dim, dim, dim]
+        const fixedZoomPerspectiveSlices = this.config && this.config.layout && this.config.layout.useNehubaPerspective && this.config.layout.useNehubaPerspective.fixedZoomPerspectiveSlices
+        if (fixedZoomPerspectiveSlices) {
+          const { sliceZoom, sliceViewportWidth, sliceViewportHeight } = fixedZoomPerspectiveSlices
+          const dim = Math.min(sliceZoom * sliceViewportWidth, sliceZoom * sliceViewportHeight)
+          this._dim = [dim, dim, dim]
+        }
         this.patchNG()
         this.loadNehuba()
       })
@@ -300,6 +303,7 @@ export class NehubaViewerUnit implements OnDestroy{
   loadNehuba(){
     this.nehubaViewer = window['export_nehuba'].createNehubaViewer(this.config, (err)=>{
       /* print in debug mode */
+      console.log(err)
     })
 
     if(this.regionsLabelIndexMap){
-- 
GitLab