From 5804d09a1868c0f367084f87e62c8694bac035b3 Mon Sep 17 00:00:00 2001
From: xgui3783 <xgui3783@gmail.com>
Date: Thu, 25 Jun 2020 20:12:05 +0200
Subject: [PATCH] Chore travisci (#548)

add travis ci
---
 .readthedocs.yml                              | 23 +++++
 .travis.yml                                   | 92 ++++++++++++++++++
 deploy/datasets/testData/ibcDataExpected.js   |  3 +-
 deploy/server.js                              |  9 +-
 deploy/server.spec.js                         | 29 ++++--
 docs-requirements.txt                         |  3 +
 e2e/protractor.conf.js                        | 94 ++++++++++++++++---
 .../advanced/nonAtlasImages.prod.e2e-spec.js  |  4 +-
 .../changeTemplate.prod.e2e-spec.js           |  5 +-
 .../mouseOverNehuba.prod.e2e-spec.js          |  8 +-
 e2e/src/util.js                               | 52 ++++++----
 package.json                                  |  1 +
 src/index.html                                |  2 +-
 ...mplateCoordinatesTransformation.service.ts |  2 +-
 .../splashScreen/splashScreen.template.html   |  2 +-
 typings/index.d.ts                            |  1 +
 webpack.staticassets.js                       |  1 +
 17 files changed, 277 insertions(+), 54 deletions(-)
 create mode 100644 .readthedocs.yml
 create mode 100644 .travis.yml
 create mode 100644 docs-requirements.txt

diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 000000000..8f70f5c03
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,23 @@
+# .readthedocs.yml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Build documentation in the docs/ directory with Sphinx
+# sphinx:
+#   configuration: docs/conf.py
+
+# Build documentation with MkDocs
+mkdocs:
+ configuration: mkdocs.yml
+
+# Optionally build your docs in additional formats such as PDF and ePub
+formats: all
+
+# Optionally set the version of Python and requirements required to build your docs
+python:
+  version: 3.7
+  install:
+    - requirements: docs-requirements.txt
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..953c2eb07
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,92 @@
+language: node_js
+
+os: linux
+dist: xenial
+node_js:
+- 12
+
+git:
+  depth: 3
+
+script: 'echo Running ${TEST_SUITE} against $(node -v)'
+
+jobs:
+  include:
+
+    - stage: Unit test frontend
+      name: frontend unit test
+
+      # env(TEST_SUITE) = 'unit-frontend' - for parallel runs in TravisCI
+      # NOT type = pull_request - only run on branch, so it is not run on PR (duplicated runs)
+      # NOT branch in (master, staging, dev) - all changes done to these branches should go through PR, so do not run unit tests
+      if: env(TEST_SUITE) = 'unit-frontend' AND NOT type = pull_request AND NOT branch in (master, staging, dev)
+      install:
+      - npm i
+      script:
+      - npm run lint
+      - npm test
+      env:
+      - NODE_ENV=test
+
+    - stage: Unit test backend
+      name: backend unit test
+
+      # env(TEST_SUITE) = 'unit-backend' - for parallel runs in TravisCI
+      # NOT type = pull_request - only run on branch, so it is not run on PR (duplicated runs)
+      # NOT branch in (master, staging, dev) - all changes done to these branches should go through PR, so do not run unit tests
+      if: env(TEST_SUITE) = 'unit-backend' AND NOT type = pull_request AND NOT branch in (master, staging, dev)
+      before_install:
+      - cd deploy
+      install:
+      - npm i
+      script:
+      - npm test
+      env:
+      - NODE_ENV=test
+      - PORT=12234
+
+    - stage: local e2e
+      # type = api - should only be activated by API
+      # when send API, ensure to set env TEST_SUITE=e2e-local to explicitly trigger this build
+      if: env(TEST_SUITE) = 'e2e-local' AND branch IN (master, staging, dev) AND type = api
+      name: e2e (local)
+      install:
+      - npm i
+      - npm run wd -- update --versions.chrome=${CHROMIUM_VERSION}
+      - npm i --no-save puppeteer@${PPTR_VERSION}
+      script:
+      - npm run e2e
+      env:
+      - CHROMIUM_VERSION=80.0.3987.106
+      - PPTR_VERSION=2.1.0
+      - PROTRACTOR_SPECS=./src/navigating/*.e2e-spec.js
+
+
+    # Temporarily disabling browserstack e2e tests. They seem to fail without any reason
+
+    # - stage: browserstack e2e
+    #   # only triggered via API, where env can be overwritten
+    #   if: NOT env(LOCAL_TEST_E2E) = 1
+    #   name: e2e (with browserstack)
+    #   install:
+    #   - npm i
+    #   script:
+    #   - PROTRACTOR_SPECS=./src/navigating/*.e2e-spec.js BROWSERSTACK_TEST_NAME=e2e_navigating npm run e2e
+    #   env:
+    #   - ATLAS_URL=https://interactive-viewer-next.apps-dev.hbp.eu/
+    #   - BROWSERSTACK_USERNAME=xiao33
+    #   - secure: "YD2hDBnWzcMs9mTJCsKkJimd+mYKP8V1lTaCnxNvspJUxTuBWFmr8cvryIs9G9DhwgxkC3YL7hugsGkwMg6OIq27vLlo8mgoKS7/qrkWAJApGvDW4jc4CHpI2iE/ryrwG1bI3u9TuG0kSw+2sN/786LBgArlf5NbmwB9zmW4zyzjXXzSME34cwYdfEP96L2cob/uGiIj9YdaA1k3zfBhQQlp328i/xzYbIAcwfIia1AKYh/wgCzj+yfWDQ0Rtg9VcI2JiNfcbzMCgvDEBzshgeXuubFd9GPqPsc8zJhYqAi/15ge+WiB8R50MnZsYHO39JJihQzKz6WxIZQDeOQ2xd600NhFFLg6WPdE3jxAyENouTAd+0zJgXEeUU71YBDBl6RViagf8k7eOe9oMPW5ZlevdD3vcI8BC/qUL6Evye8QDDNi0s8gbIvcnJl5QMRBpeYcm/QaRUow1YeJobpccj/3tb7qTbc7T4Rha/NRBNhbhp/WzDSO/BUSEtpgJ3YwSEPTiEeSocTRT8ylnhEtBB70h4vQSClV73lW4vn7WjdZUTRACdxFNJ1MteQJ+3bgzyWMhDtdQo6BSz2UxF0mQFayAu2p9j0+MbB7x2zW9tksSw+6B6EjzPhQw6eOs2K0+syxWg09MTW1Fy6n0Zgchn0RWSnEPqPvss6kB2pkAR4="
+
+# addons:
+#   browserstack:
+#     username: "xiao33"
+#     access_key:
+#       secure: "j0NdVLyNwm1gDclEeE/xYrXAYiYAlx3HQxNRHMFhJyFml5R22spEMTwrTRl/vzyhv1FwfJAKfh4qbOn99cZ5Dzm7fWc8+Kq1zpp/1PRTzbFaLluJkV1wCwoODZkzmSVPj43M6070FhCJvOfe5VRUV440CgZH8IWRm7xaxRnN/MVyFMErMV/GIczEBB7D7E4mMhe6c9pBxjmojDDP4rGvKLGOYU7oVQKgZtbHtP/BxjQ7uzMysdTHZGZ/2c/XW/2bKVSADi4vFzge5PMVF3nSH+vzA09ro180Q5aoaek4XQPoIza0s0cqtqkbvkbJ+lWRE+Q7wJDhQLM4WNx5GX3fegJiqJRT7272EgGAUy6C+e2F+D5nPucf3w6Uov9vBn5zZjbfXdNah3GZEXOTRNAVzstySiwiZe7/f4bk0vWIiEhHC+iutjn8skMxFnuw2eM3SJ5ayjxskHOdRux+1fuDya32ctx8y9a3XLhuFcuGTaeMSAn5Dw5qOlI5Qoc+xRSARoRWKmlEuxTUudD0e+b8xqfZgmOP7D3GZ6QX2W4yFrOLGqUzEySHr8hxxzhIlfwSvVdJ15AtN2AtPFQYQXb7M+XX1L7fr39Z/5ctr7DDgljSE3F2U5ofyWV2hh54aGMBQe76cZfVzF4bi98X3r6u0b3Knyti2pvx5jIoxP46nOA="
+
+
+# Parallelising tests in Travis
+
+env:
+  jobs:
+  - TEST_SUITE=unit-frontend
+  - TEST_SUITE=unit-backend
diff --git a/deploy/datasets/testData/ibcDataExpected.js b/deploy/datasets/testData/ibcDataExpected.js
index 24beaf359..0dfcc930b 100644
--- a/deploy/datasets/testData/ibcDataExpected.js
+++ b/deploy/datasets/testData/ibcDataExpected.js
@@ -40,8 +40,7 @@ module.exports = {
     {
       name: 'Jülich Cytoarchitechtonic Brain Atlas (human)',
       fullId:
-        'https://nexus.humanbrainproject.org/v0/data/minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579',
-      id: [Array]
+        'https://nexus.humanbrainproject.org/v0/data/minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579'
     }],
   parcellationRegion: [
     {
diff --git a/deploy/server.js b/deploy/server.js
index b803a0958..fdc0fbbbc 100644
--- a/deploy/server.js
+++ b/deploy/server.js
@@ -58,7 +58,6 @@ if (process.env.FLUENT_HOST) {
 
 const server = require('express')()
 
-const app = require('./app')
 const PORT = process.env.PORT || 3000
 
 // e.g. HOST_PATHNAME=/viewer
@@ -82,5 +81,9 @@ if (HOST_PATHNAME && HOST_PATHNAME !== '') {
   })
 }
 
-server.use(`${HOST_PATHNAME}/`, app)
-server.listen(PORT, () => console.log(`listening on port ${PORT}`))
\ No newline at end of file
+if (process.env.NODE_ENV !== 'test') {
+  const app = require('./app')
+  server.use(`${HOST_PATHNAME}/`, app)
+}
+
+server.listen(PORT, () => console.log(`listening on port ${PORT}`))
diff --git a/deploy/server.spec.js b/deploy/server.spec.js
index 502cce97c..6e8e36df8 100644
--- a/deploy/server.spec.js
+++ b/deploy/server.spec.js
@@ -1,8 +1,10 @@
 const { spawn } = require("child_process")
-const { expect } = require('chai')
+const { expect, assert } = require('chai')
 const path = require('path')
 const got = require('got')
 
+const PORT = process.env.PORT || 3000
+
 describe('> server.js', () => {
   const cwdPath = path.join(__dirname)
 
@@ -13,17 +15,19 @@ describe('> server.js', () => {
         cwd: cwdPath,
         env: {
           ...process.env,
+          NODE_ENV: 'test',
           HOST_PATHNAME: 'viewer'
         }
       })
   
       const timedKillSig = setTimeout(() => {
-        childProcess.kill(0)
+        childProcess.kill()
       }, 500)
       
       childProcess.on('exit', (code) => {
+        childProcess.kill()
         clearTimeout(timedKillSig)
-        expect(code).not.to.equal(0)
+        expect(code).to.be.greaterThan(0)
         done()
       })
     })
@@ -34,6 +38,7 @@ describe('> server.js', () => {
         cwd: cwdPath,
         env: {
           ...process.env,
+          NODE_ENV: 'test',
           HOST_PATHNAME: '/viewer/'
         }
       })
@@ -44,7 +49,7 @@ describe('> server.js', () => {
       
       childProcess.on('exit', (code) => {
         clearTimeout(timedKillSig)
-        expect(code).not.to.equal(0)
+        expect(code).to.be.greaterThan(0)
         done()
       })
     })
@@ -53,19 +58,23 @@ describe('> server.js', () => {
   
       const childProcess = spawn('node', ['server.js'],  {
         cwd: cwdPath,
+        stdio: 'inherit',
         env: {
           ...process.env,
+          NODE_ENV: 'test',
+          PORT,
           HOST_PATHNAME: '/viewer'
         }
       })
-  
+
       const timedKillSig = setTimeout(() => {
-        childProcess.kill(2)
+        console.log('killing on timeout')
+        childProcess.kill()
       }, 500)
       
-      childProcess.on('exit', (code) => {
+      childProcess.on('exit', (code, signal) => {
         clearTimeout(timedKillSig)
-        expect(code).to.equal(null)
+        expect(signal).to.equal('SIGTERM')
         done()
       })
     })
@@ -80,6 +89,8 @@ describe('> server.js', () => {
         cwd: cwdPath,
         env: {
           ...process.env,
+          NODE_ENV: 'test',
+          PORT,
           HOST_PATHNAME: '/viewer'
         }
       })
@@ -87,7 +98,7 @@ describe('> server.js', () => {
     })
   
     it('> redirects as expected', async () => {
-      const { statusCode } = await got(`http://localhost:3000/viewer`, {
+      const { statusCode } = await got(`http://localhost:${PORT}/viewer`, {
         followRedirect: false
       })
       expect(statusCode).to.be.greaterThan(300)
diff --git a/docs-requirements.txt b/docs-requirements.txt
new file mode 100644
index 000000000..ccba8f4dc
--- /dev/null
+++ b/docs-requirements.txt
@@ -0,0 +1,3 @@
+mkdocs>=1.1,<2
+mkdocs-material
+mdx_truly_sane_lists
diff --git a/e2e/protractor.conf.js b/e2e/protractor.conf.js
index c04080983..4169f0e15 100644
--- a/e2e/protractor.conf.js
+++ b/e2e/protractor.conf.js
@@ -1,27 +1,24 @@
 
 // n.b. to start selenium, run npm run wd -- update && npm run wd -- start
 // n.b. you will need to run `npm i --no-save puppeteer`, so that normal download script does not download chrome binary
-const pptr = require('puppeteer')
 const chromeOpts = require('./chromeOpts')
 const SELENIUM_ADDRESS = process.env.SELENIUM_ADDRESS
 
-const PROD_FLAG = process.env.NODE_ENV === 'production'
+const bsTestname = process.env.BROWSERSTACK_TEST_NAME
+const bsUsername = process.env.BROWSERSTACK_USERNAME
+const bsAccessKey = process.env.BROWSERSTACK_ACCESS_KEY
+const directConnect = !!process.env.DIRECT_CONNECT
 
-exports.config = {
+const PROTRACTOR_SPECS = process.env.PROTRACTOR_SPECS
+
+const localConfig = bsUsername && bsAccessKey
+  ? {}
+  : {
   ...(SELENIUM_ADDRESS
     ? { seleniumAddress: SELENIUM_ADDRESS }
     : { directConnect: true } 
   ),
-  specs: [
-    PROD_FLAG
-      ? './src/**/*.prod.e2e-spec.js'
-      : './src/**/*.e2e-spec.js'
-  ],
-  jasmineNodeOpts: {
-    defaultTimeoutInterval: 1000 * 60 * 10
-  },
-  capabilities: { 
-
+  capabilities: {
     // Use headless chrome
     browserName: 'chrome',
     'goog:chromeOptions': {
@@ -31,8 +28,77 @@ exports.config = {
       ...(
         SELENIUM_ADDRESS
           ? {}
-          : { binary: pptr.executablePath() }
+          : { binary: (() => require('puppeteer').executablePath())() }
       )
     }
   }
+}
+
+
+const { Local } = require('browserstack-local');
+
+let bsLocal
+/**
+ * config adapted from
+ * https://github.com/browserstack/protractor-browserstack
+ * 
+ * MIT licensed
+ */
+const bsConfig = {
+  'browserstackUser': bsUsername,
+  'browserstackKey': bsAccessKey,
+  
+  'capabilities': {
+    'build': 'protractor-browserstack',
+    'name': bsTestname || 'iav_e2e',
+    "os" : "Windows",
+    "osVersion" : "10",
+    'browserName': 'chrome',
+    // 'browserstack.local': false,
+    "seleniumVersion" : "4.0.0-alpha-2",
+    'browserstack.debug': 'true'
+  },
+  "browserName" : "Chrome",
+  "browserVersion" : "83.0",
+
+  // // Code to start browserstack local before start of test
+  // beforeLaunch: function(){
+  //   console.log("Connecting local");
+  //   return new Promise(function(resolve, reject){
+  //     bsLocal = new Local();
+  //     bsLocal.start({'key': bsAccessKey }, function(error) {
+  //       if (error) return reject(error);
+  //       console.log('Connected. Now testing...');
+
+  //       resolve();
+  //     });
+  //   });
+  // },
+
+  // // Code to stop browserstack local after end of test
+  // afterLaunch: function(){
+  //   return new Promise(function(resolve, reject){
+  //     if (bsLocal) bsLocal.stop(resolve)
+  //     else resolve()
+  //   });
+  // }
+}
+
+exports.config = {
+  specs: [
+    (PROTRACTOR_SPECS && PROTRACTOR_SPECS) || './src/**/*.prod.e2e-spec.js'
+  ],
+  jasmineNodeOpts: {
+    defaultTimeoutInterval: 1000 * 60 * 10
+  },
+  
+  ...(
+    bsAccessKey && bsUsername
+    ? bsConfig
+    : localConfig
+  ),
+
+  ...(
+    (directConnect && { directConnect }) || {}
+  )
 }
\ No newline at end of file
diff --git a/e2e/src/advanced/nonAtlasImages.prod.e2e-spec.js b/e2e/src/advanced/nonAtlasImages.prod.e2e-spec.js
index 4cd98cdbf..71fc460e0 100644
--- a/e2e/src/advanced/nonAtlasImages.prod.e2e-spec.js
+++ b/e2e/src/advanced/nonAtlasImages.prod.e2e-spec.js
@@ -114,7 +114,7 @@ describe('> non-atlas images', () => {
         )
       )
 
-      const arr = [
+      const arrPli = [
         "https://neuroglancer.humanbrainproject.eu/precomputed/PLI_FOM/BI-FOM-HSV_R",
         "https://neuroglancer.humanbrainproject.eu/precomputed/PLI_FOM/BI-FOM-HSV_G",
         "https://neuroglancer.humanbrainproject.eu/precomputed/PLI_FOM/BI-FOM-HSV_B",
@@ -135,7 +135,7 @@ describe('> non-atlas images', () => {
         )
       )
 
-      for (const url of arr) {
+      for (const url of arrPli) {
         expect(
           interceptedCalls
         ).toContain(
diff --git a/e2e/src/navigating/changeTemplate.prod.e2e-spec.js b/e2e/src/navigating/changeTemplate.prod.e2e-spec.js
index 8bfa3a70e..5c7a9c278 100644
--- a/e2e/src/navigating/changeTemplate.prod.e2e-spec.js
+++ b/e2e/src/navigating/changeTemplate.prod.e2e-spec.js
@@ -17,13 +17,16 @@ describe('trans template navigation', () => {
     await iavPage.goto(`/?${searchParam.toString()}`, { interceptHttp: true, doNotAutomate: true })
     await iavPage.wait(200)
     await iavPage.dismissModal()
+
     await iavPage.waitUntilAllChunksLoaded()
 
     await iavPage.selectDropdownTemplate('Big Brain (Histology)')
     await iavPage.wait(2000)
     const interceptedCalls = await iavPage.getInterceptedHttpCalls()
+    
+    expect(interceptedCalls).toBeTruthy()
 
-    const found = interceptedCalls.find(({ method, url }) => {
+    const found = interceptedCalls && interceptedCalls.find(({ method, url }) => {
       return method === 'POST' && /transform-points/.test(url)
     })
     expect(!!found).toBe(true)
diff --git a/e2e/src/navigating/mouseOverNehuba.prod.e2e-spec.js b/e2e/src/navigating/mouseOverNehuba.prod.e2e-spec.js
index eaa2458b4..d446ea1ed 100644
--- a/e2e/src/navigating/mouseOverNehuba.prod.e2e-spec.js
+++ b/e2e/src/navigating/mouseOverNehuba.prod.e2e-spec.js
@@ -97,8 +97,12 @@ const dictionary = {
           expectedLabelName: 'rh_SP-SM_0',
         },
         {
-          position: [642, 541],
-          expectedLabelName: 'lh_PoCi-PrCu_0',
+          /**
+           * Changed from [677, 579] as it is extremely unfortunate in that
+           * it is literally the connecting point between lh_CAC-PrCu_0 and lh_PoCi-PrCu_0
+           */
+          position: [677, 579],
+          expectedLabelName: 'lh_ST-TT_0',
         }
       ]
     }
diff --git a/e2e/src/util.js b/e2e/src/util.js
index c96a61803..217e00978 100644
--- a/e2e/src/util.js
+++ b/e2e/src/util.js
@@ -1,14 +1,15 @@
 const chromeOpts = require('../chromeOpts')
-const pptr = require('puppeteer')
+// const pptr = require('puppeteer')
 const ATLAS_URL = (process.env.ATLAS_URL || 'http://localhost:3000').replace(/\/$/, '')
 const USE_SELENIUM = !!process.env.SELENIUM_ADDRESS
 if (ATLAS_URL.length === 0) throw new Error(`ATLAS_URL must either be left unset or defined.`)
 if (ATLAS_URL[ATLAS_URL.length - 1] === '/') throw new Error(`ATLAS_URL should not trail with a slash: ${ATLAS_URL}`)
-const { By, WebDriver, Key } = require('selenium-webdriver')
+const { By, Key, until } = require('selenium-webdriver')
 const CITRUS_LIGHT_URL = `https://unpkg.com/citruslight@0.1.0/citruslight.js`
 const { polyFillClick } = require('./material-util')
 
 const { ARIA_LABELS } = require('../../common/constants')
+const { retry } = require('../../common/util')
 
 function getActualUrl(url) {
   return /^http\:\/\//.test(url) ? url : `${ATLAS_URL}/${url.replace(/^\//, '')}`
@@ -327,7 +328,7 @@ class WdBase{
   }
 
   // it seems if you set intercept http to be true, you might also want ot set do not automat to be true
-  async goto(url = '/', { interceptHttp, doNotAutomate, forceTimeout } = {}){
+  async goto(url = '/', { interceptHttp, doNotAutomate, forceTimeout = 20 * 1000 } = {}){
     const actualUrl = getActualUrl(url)
     if (interceptHttp) {
       this._browser.get(actualUrl)
@@ -356,7 +357,17 @@ class WdBase{
 
   async wait(ms) {
     if (!ms) throw new Error(`wait duration must be specified!`)
-    await this._browser.sleep(ms)
+    return new Promise(rs => {
+      setTimeout(rs, ms)
+    })
+  }
+
+  async waitForCss(cssSelector) {
+    if (!cssSelector) throw new Error(`css selector must be defined`)
+    await this._browser.wait(
+      until.elementLocated( By.css(cssSelector) ),
+      1e3 * 60 * 10
+    )
   }
 
   async waitFor(animation = false, async = false){
@@ -434,9 +445,12 @@ class WdBase{
 }
 
 class WdLayoutPage extends WdBase{
-
   constructor(){
     super()
+    WdLayoutPage.TagNames = {
+      ...(WdBase.TagNames || {} ),
+      sideNav: 'search-side-nav'
+    }
   }
 
   _getModal(){
@@ -532,7 +546,7 @@ class WdLayoutPage extends WdBase{
 
   // SideNav
   _getSideNav() {
-    return this._browser.findElement( By.tagName('search-side-nav') )
+    return this._browser.findElement( By.tagName(WdLayoutPage.TagNames.sideNav) )
   }
 
   async getSideNavTag(){
@@ -760,19 +774,12 @@ class WdIavPage extends WdLayoutPage{
   }
 
   async waitUntilAllChunksLoaded(){
-    const checkReady = async () => {
-      const el = await this._browser.findElements(
+    await this._browser.wait(async () => {
+      const els = await this._browser.findElements(
         By.css('div.loadingIndicator')
       )
-      return !el.length
-    }
-
-    do {
-      // Do nothing, until ready
-    } while (
-      await this.wait(1000),
-      !(await checkReady())
-    )
+      return els.length === 0
+    }, 1e3 * 60 * 10)
   }
 
   async getFloatingCtxInfoAsText(){
@@ -790,11 +797,20 @@ class WdIavPage extends WdLayoutPage{
       .findElement( By.css('[aria-label="Select a new template"]') )
     await templateBtn.click()
 
+    await this._browser.wait(
+      until.elementLocated( By.tagName('mat-option') ),
+      1e3 * 60 * 10
+    )
+
     const options = await this._browser.findElements(
       By.tagName('mat-option')
     )
     const idx = await _getIndexFromArrayOfWebElements(title, options)
-    if (idx >= 0) await options[idx].click()
+    if (idx >= 0) {
+      retry(async () => {
+        await options[idx].click()
+      }, { timeout: 1000, retries: 3 })
+    }
     else throw new Error(`${title} is not found as one of the dropdown templates`)
   }
 
diff --git a/package.json b/package.json
index 29ba287e6..f3a7aff77 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
     "@typescript-eslint/eslint-plugin": "^2.12.0",
     "@typescript-eslint/parser": "^2.12.0",
     "angular2-template-loader": "^0.6.2",
+    "browserstack-local": "^1.4.5",
     "codelyzer": "^5.0.1",
     "core-js": "^3.0.1",
     "css-loader": "^3.2.0",
diff --git a/src/index.html b/src/index.html
index 65ddf0e75..ff91888f3 100644
--- a/src/index.html
+++ b/src/index.html
@@ -35,7 +35,7 @@
 </head>
 <body>
   <atlas-viewer>
-    <h1 class="text-center" id="iav-inner">
+    <h1 class="text-center loadingIndicator" id="iav-inner">
       <span class="homeAnimationDots loadingAnimationDots">&bull;</span>
       <span class="homeAnimationDots loadingAnimationDots">&bull;</span>
       <span class="homeAnimationDots loadingAnimationDots">&bull;</span>
diff --git a/src/services/templateCoordinatesTransformation.service.ts b/src/services/templateCoordinatesTransformation.service.ts
index 1329fe942..6aaf98b99 100644
--- a/src/services/templateCoordinatesTransformation.service.ts
+++ b/src/services/templateCoordinatesTransformation.service.ts
@@ -16,7 +16,7 @@ export class TemplateCoordinatesTransformation {
 
   constructor(private httpClient: HttpClient) {}
 
-  public url = 'https://hbp-spatial-backend.apps-dev.hbp.eu/v1/transform-points'
+  public url = `${SPATIAL_TRANSFORM_BACKEND.replace(/\/$/, '')}/v1/transform-points`
 
   // jasmine marble cannot test promise properly
   // see https://github.com/ngrx/platform/issues/498#issuecomment-337465179
diff --git a/src/ui/nehubaContainer/splashScreen/splashScreen.template.html b/src/ui/nehubaContainer/splashScreen/splashScreen.template.html
index af7271b05..56c9e61fe 100644
--- a/src/ui/nehubaContainer/splashScreen/splashScreen.template.html
+++ b/src/ui/nehubaContainer/splashScreen/splashScreen.template.html
@@ -44,7 +44,7 @@
       </mat-card-footer>
     </mat-card>
     <ng-container *ngIf="stillLoadingTemplates$ | async as stillLoadingTemplates">
-      <div class="d-flex align-items-center p-4" *ngFor="let t of stillLoadingTemplates">
+      <div class="d-flex align-items-center p-4 loadingIndicator" *ngFor="let t of stillLoadingTemplates">
         <h1 class="mat-h1">
           <div class="spinnerAnimationCircle"></div>
         </h1>
diff --git a/typings/index.d.ts b/typings/index.d.ts
index 2fb1dc3a1..912053cac 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -21,3 +21,4 @@ declare var MATOMO_URL: string
 declare var MATOMO_ID: string
 declare var STRICT_LOCAL: boolean
 declare var KIOSK_MODE: boolean
+declare var SPATIAL_TRANSFORM_BACKEND: string
diff --git a/webpack.staticassets.js b/webpack.staticassets.js
index b58015543..163b00926 100644
--- a/webpack.staticassets.js
+++ b/webpack.staticassets.js
@@ -72,6 +72,7 @@ module.exports = {
       PRODUCTION: !!process.env.PRODUCTION,
       BACKEND_URL: (process.env.BACKEND_URL && JSON.stringify(process.env.BACKEND_URL)) || 'null',
       DATASET_PREVIEW_URL: JSON.stringify(process.env.DATASET_PREVIEW_URL || 'https://hbp-kg-dataset-previewer.apps.hbp.eu/datasetPreview'),
+      SPATIAL_TRANSFORM_BACKEND: JSON.stringify(process.env.SPATIAL_TRANSFORM_BACKEND || 'https://hbp-spatial-backend.apps.hbp.eu'),
       MATOMO_URL: JSON.stringify(process.env.MATOMO_URL || null),
       MATOMO_ID: JSON.stringify(process.env.MATOMO_ID || null),
       USE_LOGO: JSON.stringify(process.env.USE_LOGO || 'hbp' || 'ebrains' || 'fzj'),
-- 
GitLab