From ace9713aa8f355cbc73d0bba4722b7b7ccf9efb8 Mon Sep 17 00:00:00 2001
From: Sandro Weber <webers@in.tum.de>
Date: Mon, 1 Feb 2021 23:14:19 +0100
Subject: [PATCH] lots more test coverage

---
 .../experiment-list-element.css               |  26 ++-
 .../experiment-list-element.js                |  24 ++-
 src/components/user-menu/user-menu.js         |  19 +-
 src/mocks/handlers.js                         |  20 ++-
 src/mocks/mock_available-servers.json         |   2 +-
 src/mocks/mock_server-config.json             |  14 ++
 src/mocks/mock_user.json                      |   4 +
 .../experiment-execution-service.test.js      | 166 ++++++++++++++++++
 .../server-resources-service.test.js          |  82 +++++++++
 .../execution/experiment-execution-service.js |  70 +++++---
 .../execution/server-resources-service.js     |  65 ++-----
 .../experiment-storage-service.test.js        |  18 ++
 .../storage/experiment-storage-service.js     |   6 +-
 src/services/nrp-analytics-service.js         |   4 +-
 src/services/proxy/nrp-user-service.js        |   6 +-
 15 files changed, 391 insertions(+), 135 deletions(-)
 create mode 100644 src/mocks/mock_server-config.json
 create mode 100644 src/mocks/mock_user.json
 create mode 100644 src/services/experiments/execution/__tests__/experiment-execution-service.test.js
 create mode 100644 src/services/experiments/execution/__tests__/server-resources-service.test.js

diff --git a/src/components/experiment-list/experiment-list-element.css b/src/components/experiment-list/experiment-list-element.css
index 8ce8b3a..f45970b 100644
--- a/src/components/experiment-list/experiment-list-element.css
+++ b/src/components/experiment-list/experiment-list-element.css
@@ -12,27 +12,15 @@
 }
 
 .flex-container .selected {
-  background: linear-gradient(
-    to right,
-    rgba(255, 255, 255, 0.8) 0%,
-    rgba(245, 245, 245, 0.8) 100%
-  );
+  background: linear-gradient( to right, rgba(255, 255, 255, 0.8) 0%, rgba(245, 245, 245, 0.8) 100%);
 }
 
 .flex-container.hover-wizard {
-  background: linear-gradient(
-    to right,
-    rgba(255, 255, 255, 0.8) 0%,
-    rgba(230, 239, 255, 0.8) 100%
-  );
+  background: linear-gradient( to right, rgba(255, 255, 255, 0.8) 0%, rgba(230, 239, 255, 0.8) 100%);
 }
 
 .flex-container.selected-wizard {
-  background: linear-gradient(
-    to right,
-    rgba(255, 255, 255, 0.8) 0%,
-    rgba(230, 239, 255, 0.8) 100%
-  );
+  background: linear-gradient( to right, rgba(255, 255, 255, 0.8) 0%, rgba(230, 239, 255, 0.8) 100%);
 }
 
 .flex-container.left-right {
@@ -69,12 +57,13 @@
   width: 359px;
 }
 
-.list-entry-left > img {
+.list-entry-left>img {
   width: 100%;
   object-fit: contain;
   object-position: top;
   height: 100%;
 }
+
 .list-entry-middle {
   flex: 1;
 }
@@ -124,4 +113,9 @@
 .exp-title-sim-info {
   padding-left: 20px;
   font-style: italic;
+}
+
+.entity-thumbnail {
+  max-width: 230px;
+  max-height: 160px;
 }
\ No newline at end of file
diff --git a/src/components/experiment-list/experiment-list-element.js b/src/components/experiment-list/experiment-list-element.js
index 907d5af..5dd6543 100644
--- a/src/components/experiment-list/experiment-list-element.js
+++ b/src/components/experiment-list/experiment-list-element.js
@@ -53,10 +53,8 @@ export default class ExperimentListElement extends React.Component {
   }
 
   getAvailabilityInfo() {
-    const clusterAvailability = ExperimentServerService.instance.getClusterAvailability();
-
     let status;
-    if (clusterAvailability && clusterAvailability.free > CLUSTER_THRESHOLDS.AVAILABLE) {
+    if (this.props.availableServers && this.props.availableServers.length > CLUSTER_THRESHOLDS.AVAILABLE) {
       status = 'Available';
     }
     else if (!this.props.availableServers || this.props.availableServers.length === 0) {
@@ -66,17 +64,14 @@ export default class ExperimentListElement extends React.Component {
       status = 'Restricted';
     }
 
-    let cluster = `Cluster availability: ${clusterAvailability.free} / ${clusterAvailability.total}`;
     let backends = `Backends: ${this.props.availableServers.length}`;
 
-    return `${status}\n${cluster}\n${backends}`;
+    return `${status}\n${backends}`;
   }
 
   getServerStatusClass() {
-    const clusterAvailability = ExperimentServerService.instance.getClusterAvailability();
-
     let status = '';
-    if (clusterAvailability && clusterAvailability.free > CLUSTER_THRESHOLDS.AVAILABLE) {
+    if (this.props.availableServers && this.props.availableServers.length > CLUSTER_THRESHOLDS.AVAILABLE) {
       status = 'server-status-available';
     }
     else if (!this.props.availableServers || this.props.availableServers.length === 0) {
@@ -171,12 +166,13 @@ export default class ExperimentListElement extends React.Component {
               <div className='btn-group' role='group' >
                 {this.canLaunchExperiment &&
                   exp.configuration.experimentFile && exp.configuration.bibiConfSrc ?
-                  <button onClick={() => {
-                    ExperimentExecutionService.instance.startNewExperiment(exp, false);
-                  }}
-                  disabled={this.isLaunchDisabled()}
-                  className='btn btn-default'
-                  title={this.launchButtonTitle} >
+                  <button
+                    onClick={() => {
+                      ExperimentExecutionService.instance.startNewExperiment(exp, false);
+                    }}
+                    disabled={this.isLaunchDisabled()}
+                    className='btn btn-default'
+                    title={this.launchButtonTitle} >
                     <i className='fa fa-plus'></i> Launch
                   </button>
                   : null}
diff --git a/src/components/user-menu/user-menu.js b/src/components/user-menu/user-menu.js
index 5182fe1..d9cda55 100644
--- a/src/components/user-menu/user-menu.js
+++ b/src/components/user-menu/user-menu.js
@@ -17,21 +17,18 @@ export default class UserMenu extends React.Component {
     };
   }
 
-  componentDidMount() {
-    this._userRequest = NrpUserService.instance
-      .getCurrentUser()
-      .then((currentUser) => {
-        this._userRequest = null;
-        this.setState(() => ({
+  async componentDidMount() {
+    NrpUserService.instance.getCurrentUser().then((currentUser) => {
+      if (!this.cancelGetCurrentUser) {
+        this.setState({
           user: currentUser
-        }));
-      });
+        });
+      }
+    });
   }
 
   componentWillUnmount() {
-    if (this._userRequest) {
-      this._userRequest.cancel();
-    }
+    this.cancelGetCurrentUser = true;
   }
 
   onClickLogout() {
diff --git a/src/mocks/handlers.js b/src/mocks/handlers.js
index 3ceb2fb..aeb98a0 100644
--- a/src/mocks/handlers.js
+++ b/src/mocks/handlers.js
@@ -5,6 +5,9 @@ import endpoints from '../services/proxy/data/endpoints';
 import MockExperiments from './mock_experiments.json';
 import MockAvailableServers from './mock_available-servers.json';
 import MockSimulationResources from './mock_simulation-resources.json';
+import MockServerConfig from './mock_server-config.json';
+import MockUser from './mock_user.json';
+import MockSimulations from './mock_simulations.json';
 
 import ImageAI from '../assets/images/Artificial_Intelligence_2.jpg';
 
@@ -13,14 +16,10 @@ const experiments = MockExperiments;
 
 export const handlers = [
   rest.get(`${config.api.proxy.url}${endpoints.proxy.storage.experiments.url}`, (req, res, ctx) => {
-    return res(
-      ctx.json(experiments)
-    );
+    return res(ctx.json(experiments));
   }),
   rest.get(`${config.api.proxy.url}${endpoints.proxy.availableServers.url}`, (req, res, ctx) => {
-    return res(
-      ctx.json(availableServers)
-    );
+    return res(ctx.json(availableServers));
   }),
   rest.get(`${config.api.proxy.url}${endpoints.proxy.storage.url}/:experimentName/:thumbnailFilename`,
     (req, res, ctx) => {
@@ -35,5 +34,14 @@ export const handlers = [
     else {
       throw new Error('Simulation resource error');
     }
+  }),
+  rest.get('http://:serverIP/simulation/', (req, res, ctx) => {
+    return res(ctx.json(MockSimulations[0]));
+  }),
+  rest.get(`${config.api.proxy.url}${endpoints.proxy.server.url}/:serverID`, (req, res, ctx) => {
+    return res(ctx.json(MockServerConfig));
+  }),
+  rest.get(`${config.api.proxy.url}${endpoints.proxy.identity.me.url}`, (req, res, ctx) => {
+    return res(ctx.json(MockUser));
   })
 ];
\ No newline at end of file
diff --git a/src/mocks/mock_available-servers.json b/src/mocks/mock_available-servers.json
index 070ca4f..eb2935a 100644
--- a/src/mocks/mock_available-servers.json
+++ b/src/mocks/mock_available-servers.json
@@ -25,6 +25,6 @@
             "websocket": "ws://1.2.3.4:8080/rosbridge"
         },
         "serverJobLocation": "1.2.3.4",
-        "id": "1.2.3.4:8080"
+        "id": "1.2.3.4-port-8080"
     }
 ]
\ No newline at end of file
diff --git a/src/mocks/mock_server-config.json b/src/mocks/mock_server-config.json
new file mode 100644
index 0000000..e19865e
--- /dev/null
+++ b/src/mocks/mock_server-config.json
@@ -0,0 +1,14 @@
+{
+    "internalIp": "http://localhost:8080",
+    "gzweb": {
+        "assets": "http://localhost:8080/assets",
+        "nrp-services": "http://localhost:8080",
+        "videoStreaming": "http://localhost:8080/webstream/",
+        "websocket": "ws://localhost:8080/gzbridge"
+    },
+    "rosbridge": {
+        "websocket": "ws://localhost:8080/rosbridge"
+    },
+    "serverJobLocation": "local",
+    "id": "localhost"
+}
\ No newline at end of file
diff --git a/src/mocks/mock_user.json b/src/mocks/mock_user.json
new file mode 100644
index 0000000..8225c18
--- /dev/null
+++ b/src/mocks/mock_user.json
@@ -0,0 +1,4 @@
+{
+    "id": "nrpuser",
+    "displayName": "nrpuser"
+}
\ No newline at end of file
diff --git a/src/services/experiments/execution/__tests__/experiment-execution-service.test.js b/src/services/experiments/execution/__tests__/experiment-execution-service.test.js
new file mode 100644
index 0000000..d49c61d
--- /dev/null
+++ b/src/services/experiments/execution/__tests__/experiment-execution-service.test.js
@@ -0,0 +1,166 @@
+/**
+ * @jest-environment jsdom
+*/
+import '@testing-library/jest-dom';
+import 'jest-fetch-mock';
+
+import MockExperiments from '../../../../mocks/mock_experiments.json';
+import MockAvailableServers from '../../../../mocks/mock_available-servers.json';
+import MockServerConfig from '../../../../mocks/mock_server-config.json';
+import MockSimulations from '../../../../mocks/mock_simulations.json';
+
+import ExperimentExecutionService from '../../../../services/experiments/execution/experiment-execution-service';
+import ServerResourcesService from '../../../../services/experiments/execution/server-resources-service';
+import SimulationService from '../../../../services/experiments/execution/simulation-service';
+
+//jest.setTimeout(10000);
+
+afterEach(() => {
+  jest.restoreAllMocks();
+});
+
+test('makes sure that invoking the constructor fails with the right message', () => {
+  expect(() => {
+    new ExperimentExecutionService();
+  }).toThrow(Error);
+  expect(() => {
+    new ExperimentExecutionService();
+  }).toThrowError(Error('Use ExperimentExecutionService.instance'));
+});
+
+test('the service instance always refers to the same object', () => {
+  const instance1 = ExperimentExecutionService.instance;
+  const instance2 = ExperimentExecutionService.instance;
+  expect(instance1).toBe(instance2);
+});
+
+test('should emit an event on starting an experiment', async () => {
+  jest.spyOn(ExperimentExecutionService.instance, 'launchExperimentOnServer').mockImplementation(() => {
+    return Promise.resolve();
+  });
+  let experiment = MockExperiments[0];
+
+  let confirmStartingExperiment = (startingExperiment) => {
+    expect(startingExperiment).toEqual(experiment);
+  };
+  ExperimentExecutionService.instance.addListener(
+    ExperimentExecutionService.EVENTS.START_EXPERIMENT,
+    confirmStartingExperiment
+  );
+  await ExperimentExecutionService.instance.startNewExperiment(experiment);
+  ExperimentExecutionService.instance.removeListener(
+    ExperimentExecutionService.EVENTS.START_EXPERIMENT,
+    confirmStartingExperiment
+  );
+});
+
+test('should go through the list of available servers when trying to start an experiment', (done) => {
+  jest.spyOn(console, 'error').mockImplementation();
+  jest.spyOn(ServerResourcesService.instance, 'getServerConfig');
+  jest.spyOn(ExperimentExecutionService.instance, 'launchExperimentOnServer').mockImplementation(
+    // only the last server in the list will return a successful launch
+    (expID, isPrivate, numBrainProc, serverID) => {
+      if (serverID !== MockAvailableServers[MockAvailableServers.length - 1].id) {
+        return Promise.reject({
+          error: {
+            data: 'test rejection for launch on server ' + serverID
+          }
+        });
+      }
+
+      return Promise.resolve();
+    }
+  );
+
+  let experiment = MockExperiments[0];
+  ExperimentExecutionService.instance.startNewExperiment(experiment).then(() => {
+    MockAvailableServers.forEach(server => {
+      expect(ServerResourcesService.instance.getServerConfig).toHaveBeenCalledWith(server.id);
+    });
+    expect(console.error).toHaveBeenCalled();
+    done();
+  });
+});
+
+test('starting an experiment should abort early if a fatal error occurs', (done) => {
+  jest.spyOn(ExperimentExecutionService.instance, 'launchExperimentOnServer').mockImplementation(
+    () => {
+      return Promise.reject({
+        isFatal: true
+      });
+    }
+  );
+
+  let experiment = MockExperiments[0];
+  ExperimentExecutionService.instance.startNewExperiment(experiment).catch(error => {
+    expect(error).toEqual(ExperimentExecutionService.ERRORS.LAUNCH_FATAL_ERROR);
+    done();
+  });
+});
+
+test('starting an experiment should fail if no server is ready', (done) => {
+  jest.spyOn(ExperimentExecutionService.instance, 'launchExperimentOnServer').mockImplementation(
+    () => {
+      return Promise.reject({});
+    }
+  );
+
+  let experiment = MockExperiments[0];
+  ExperimentExecutionService.instance.startNewExperiment(experiment).catch(error => {
+    expect(error).toEqual(ExperimentExecutionService.ERRORS.LAUNCH_NO_SERVERS_LEFT);
+    done();
+  });
+});
+
+test('respects settings for specific dev server to launch and single brain process mode', async () => {
+  jest.spyOn(ExperimentExecutionService.instance, 'launchExperimentOnServer').mockImplementation(() => {
+    return Promise.resolve();
+  });
+
+  let mockExperiment = {
+    id: 'test-experiment-id',
+    devServer: 'test-dev-server-url'
+  };
+  await ExperimentExecutionService.instance.startNewExperiment(mockExperiment, true);
+  expect(ExperimentExecutionService.instance.launchExperimentOnServer).toHaveBeenCalledWith(
+    mockExperiment.id,
+    undefined,
+    1,
+    mockExperiment.devServer,
+    expect.any(Object),
+    undefined,
+    undefined,
+    undefined,
+    expect.any(Function)
+  );
+});
+
+test('can launch an experiment given a specific server + configuration', async () => {
+  jest.spyOn(ExperimentExecutionService.instance, 'httpRequestPOST').mockImplementation();
+  jest.spyOn(SimulationService.instance, 'registerForRosStatusInformation').mockImplementation();
+  jest.spyOn(SimulationService.instance, 'simulationReady').mockImplementation(() => {
+    return Promise.resolve(MockSimulations[0]);
+  });
+  jest.spyOn(SimulationService.instance, 'initConfigFiles').mockImplementation(() => {
+    return Promise.resolve();
+  });
+
+  let experimentID = 'test-experiment-id';
+  let privateExperiment = true;
+  let brainProcesses = 2;
+  let serverID = 'test-server-id';
+  let serverConfiguration = MockServerConfig;
+  let reservation = {};
+  let playbackRecording = {};
+  let profiler = {};
+  let progressCallback = jest.fn();
+  let callParams = [experimentID, privateExperiment, brainProcesses, serverID, serverConfiguration, reservation,
+    playbackRecording, profiler, progressCallback];
+
+  let result = await ExperimentExecutionService.instance.launchExperimentOnServer(...callParams);
+  expect(ExperimentExecutionService.instance.httpRequestPOST)
+    .toHaveBeenLastCalledWith(serverConfiguration.gzweb['nrp-services'] + '/simulation', expect.any(String));
+  expect(progressCallback).toHaveBeenCalled();
+  expect(result).toBe('esv-private/experiment-view/' + serverID + '/' + experimentID + '/' +
+  privateExperiment + '/' + MockSimulations[0].simulationID);
+});
diff --git a/src/services/experiments/execution/__tests__/server-resources-service.test.js b/src/services/experiments/execution/__tests__/server-resources-service.test.js
new file mode 100644
index 0000000..2345da7
--- /dev/null
+++ b/src/services/experiments/execution/__tests__/server-resources-service.test.js
@@ -0,0 +1,82 @@
+/**
+ * @jest-environment jsdom
+*/
+import '@testing-library/jest-dom';
+import 'jest-fetch-mock';
+
+import MockServerconfig from '../../../../mocks/mock_server-config.json';
+
+import ServerResourcesService from '../../../../services/experiments/execution/server-resources-service';
+import ErrorHandlerService from '../../../error-handler-service';
+
+jest.setTimeout(10000);
+
+let onWindowBeforeUnloadCb = undefined;
+beforeEach(() => {
+  jest.spyOn(window, 'addEventListener').mockImplementation((event, cb) => {
+    if (event === 'beforeunload') {
+      onWindowBeforeUnloadCb = cb;
+    }
+  });
+});
+
+afterEach(() => {
+  jest.restoreAllMocks();
+});
+
+test('makes sure that invoking the constructor fails with the right message', () => {
+  expect(() => {
+    new ServerResourcesService();
+  }).toThrow(Error);
+  expect(() => {
+    new ServerResourcesService();
+  }).toThrowError(Error('Use ServerResourcesService.instance'));
+});
+
+test('the service instance always refers to the same object', () => {
+  const instance1 = ServerResourcesService.instance;
+  const instance2 = ServerResourcesService.instance;
+  expect(instance1).toBe(instance2);
+});
+
+test('does automatic poll updates for server availability', (done) => {
+  jest.spyOn(ServerResourcesService.instance, 'getServerAvailability');
+
+  // check that getExperiments is periodically called after poll interval
+  let numCallsServerAvailabilityT0 = ServerResourcesService.instance.getServerAvailability.mock.calls.length;
+  setTimeout(() => {
+    let numCallsServerAvailabilityT1 = ServerResourcesService.instance.getServerAvailability.mock.calls.length;
+    expect(numCallsServerAvailabilityT1 > numCallsServerAvailabilityT0).toBe(true);
+
+    // stop updates and check that no more calls occur after poll interval
+    ServerResourcesService.instance.stopUpdates();
+    setTimeout(() => {
+      let numCallsServerAvailabilityT2 = ServerResourcesService.instance.getServerAvailability.mock.calls.length;
+      expect(numCallsServerAvailabilityT2 === numCallsServerAvailabilityT1).toBe(true);
+      done();
+    }, ServerResourcesService.CONSTANTS.INTERVAL_POLL_SERVER_AVAILABILITY);
+  }, ServerResourcesService.CONSTANTS.INTERVAL_POLL_SERVER_AVAILABILITY);
+});
+
+test('can get a server config', async () => {
+  // regular call with proper json
+  let config = await ServerResourcesService.instance.getServerConfig('test-server-id');
+  expect(config).toEqual(MockServerconfig);
+
+  // rejected promise on GET
+  jest.spyOn(ServerResourcesService.instance, 'httpRequestGET').mockImplementation(() => {
+    return Promise.reject();
+  });
+  jest.spyOn(ErrorHandlerService.instance, 'displayServerHTTPError').mockImplementation();
+  config = await ServerResourcesService.instance.getServerConfig('test-server-id');
+  expect(ErrorHandlerService.instance.displayServerHTTPError).toHaveBeenCalled();
+});
+
+test('should stop polling updates when window is unloaded', async () => {
+  let service = ServerResourcesService.instance;
+  expect(onWindowBeforeUnloadCb).toBeDefined();
+
+  jest.spyOn(service, 'stopUpdates');
+  onWindowBeforeUnloadCb({});
+  expect(service.stopUpdates).toHaveBeenCalled();
+});
diff --git a/src/services/experiments/execution/experiment-execution-service.js b/src/services/experiments/execution/experiment-execution-service.js
index 2ec73f5..193982e 100644
--- a/src/services/experiments/execution/experiment-execution-service.js
+++ b/src/services/experiments/execution/experiment-execution-service.js
@@ -1,6 +1,6 @@
 import _ from 'lodash';
 
-import NrpAnalyticsService from '../../nrp-analytics-service.js';
+//import NrpAnalyticsService from '../../nrp-analytics-service.js';
 import ServerResourcesService from './server-resources-service.js';
 import SimulationService from './simulation-service.js';
 import { HttpService } from '../../http-service.js';
@@ -39,22 +39,23 @@ class ExperimentExecutionService extends HttpService {
    * @param {object} playbackRecording - a recording of a previous execution
    * @param {*} profiler - a profiler option
    */
-  startNewExperiment(
+  async startNewExperiment(
     experiment,
     launchSingleMode,
     reservation,
     playbackRecording,
     profiler
   ) {
-    NrpAnalyticsService.instance.eventTrack('Start', { category: 'Experiment' });
-    NrpAnalyticsService.instance.tickDurationEvent('Server-initialization');
+    //TODO: implement NrpAnalyticsService functionality
+    //NrpAnalyticsService.instance.eventTrack('Start', { category: 'Experiment' });
+    //NrpAnalyticsService.instance.tickDurationEvent('Server-initialization');
 
     ExperimentExecutionService.instance.emit(ExperimentExecutionService.EVENTS.START_EXPERIMENT, experiment);
 
     let fatalErrorOccurred = false;
     let serversToTry = experiment.devServer
       ? [experiment.devServer]
-      : ServerResourcesService.instance.getServerAvailability(true).map(s => s.id);
+      : (await ServerResourcesService.instance.getServerAvailability(true)).map(s => s.id);
 
     let brainProcesses = launchSingleMode ? 1 : experiment.configuration.brainProcesses;
 
@@ -64,20 +65,23 @@ class ExperimentExecutionService extends HttpService {
     };
 
     let launchOnNextServer = async () => {
-      let nextServer = serversToTry.splice(0, 1);
-      if (fatalErrorOccurred || !nextServer.length) {
-        //no more servers to retry, we have failed to start experiment
-        return Promise.reject(fatalErrorOccurred);
+      if (!serversToTry.length) {
+        //TODO: GUI feedback
+        return Promise.reject(ExperimentExecutionService.ERRORS.LAUNCH_NO_SERVERS_LEFT);
+      }
+      if (fatalErrorOccurred) {
+        //TODO: GUI feedback
+        return Promise.reject(ExperimentExecutionService.ERRORS.LAUNCH_FATAL_ERROR);
       }
 
-      let server = nextServer[0];
-      let serverConfig = await ServerResourcesService.instance.getServerConfig(server);
+      let serverID = serversToTry.splice(0, 1)[0];
+      let serverConfig = await ServerResourcesService.instance.getServerConfig(serverID);
 
       return await this.launchExperimentOnServer(
         experiment.id,
         experiment.private,
         brainProcesses,
-        server,
+        serverID,
         serverConfig,
         reservation,
         playbackRecording,
@@ -85,6 +89,7 @@ class ExperimentExecutionService extends HttpService {
         progressCallback
       ).catch((failure) => {
         if (failure.error && failure.error.data) {
+          //TODO: proper ErrorHandlerService callback
           console.error('Failed to start simulation: ' + JSON.stringify(failure.error.data));
         }
         fatalErrorOccurred = fatalErrorOccurred || failure.isFatal;
@@ -101,7 +106,7 @@ class ExperimentExecutionService extends HttpService {
    * @param {string} experimentID - ID of the experiment to launch
    * @param {boolean} privateExperiment - whether the experiment is private or not
    * @param {number} brainProcesses - number of brain processes to start with
-   * @param {string} server - server ID
+   * @param {string} serverID - server ID
    * @param {object} serverConfiguration - configuration of server
    * @param {object} reservation - server reservation
    * @param {object} playbackRecording - recording
@@ -112,7 +117,7 @@ class ExperimentExecutionService extends HttpService {
     experimentID,
     privateExperiment,
     brainProcesses,
-    server,
+    serverID,
     serverConfiguration,
     reservation,
     playbackRecording,
@@ -158,7 +163,7 @@ class ExperimentExecutionService extends HttpService {
         .then((simulation) => {
           SimulationService.instance.initConfigFiles(serverURL, simulation.simulationID)
             .then(() => {
-              let simulationURL = 'esv-private/experiment-view/' + server + '/' + experimentID + '/' +
+              let simulationURL = 'esv-private/experiment-view/' + serverID + '/' + experimentID + '/' +
                 privateExperiment + '/' + simulation.simulationID;
               resolve(simulationURL);
               ExperimentExecutionService.instance.emit(ExperimentExecutionService.EVENTS.START_EXPERIMENT, undefined);
@@ -205,18 +210,34 @@ class ExperimentExecutionService extends HttpService {
               if (!data || !data.state) {
                 return Promise.reject();
               }
-              switch (data.state) {
-              case EXPERIMENT_STATE.CREATED: //CREATED --(initialize)--> PAUSED --(stop)--> STOPPED
+
+              // CREATED --(initialize)--> PAUSED --(stop)--> STOPPED
+              if (data.state === EXPERIMENT_STATE.CREATED) {
                 return updateSimulationState(EXPERIMENT_STATE.INITIALIZED).then(
                   _.partial(updateSimulationState, EXPERIMENT_STATE.STOPPED)
                 );
-              case EXPERIMENT_STATE.STARTED: //STARTED --(stop)--> STOPPED
-              case EXPERIMENT_STATE.PAUSED: //PAUSED  --(stop)--> STOPPED
-              case EXPERIMENT_STATE.HALTED: //HALTED  --(stop)--> FAILED
+              }
+              // STARTED/PAUSED/HALTED --(stop)--> STOPPED
+              else if (data.state === EXPERIMENT_STATE.STARTED ||
+                data.state === EXPERIMENT_STATE.PAUSED ||
+                data.state === EXPERIMENT_STATE.HALTED) {
                 return updateSimulationState(EXPERIMENT_STATE.STOPPED);
-              default:
-                return Promise.reject();
               }
+
+              return Promise.reject();
+
+              /*switch (data.state) {
+                case EXPERIMENT_STATE.CREATED: //CREATED --(initialize)--> PAUSED --(stop)--> STOPPED
+                  return updateSimulationState(EXPERIMENT_STATE.INITIALIZED).then(
+                    _.partial(updateSimulationState, EXPERIMENT_STATE.STOPPED)
+                  );
+                case EXPERIMENT_STATE.STARTED: //STARTED --(stop)--> STOPPED
+                case EXPERIMENT_STATE.PAUSED: //PAUSED  --(stop)--> STOPPED
+                case EXPERIMENT_STATE.HALTED: //HALTED  --(stop)--> FAILED
+                  return updateSimulationState(EXPERIMENT_STATE.STOPPED);
+                default:
+                  return Promise.reject();
+              }*/
             });
           /*eslint-enable camelcase*/
         })
@@ -231,4 +252,9 @@ ExperimentExecutionService.EVENTS = Object.freeze({
   STOP_EXPERIMENT: 'STOP_EXPERIMENT'
 });
 
+ExperimentExecutionService.ERRORS = Object.freeze({
+  LAUNCH_FATAL_ERROR: 'failed to launch experiment, encountered a fatal error',
+  LAUNCH_NO_SERVERS_LEFT: 'failed to launch experiment, no available server could successfully start it'
+});
+
 export default ExperimentExecutionService;
diff --git a/src/services/experiments/execution/server-resources-service.js b/src/services/experiments/execution/server-resources-service.js
index 57c2abf..708a46b 100644
--- a/src/services/experiments/execution/server-resources-service.js
+++ b/src/services/experiments/execution/server-resources-service.js
@@ -1,23 +1,14 @@
-import _ from 'lodash';
-import { Subject, timer } from 'rxjs';
-import { switchMap, filter, map, multicast } from 'rxjs/operators';
-
 import ErrorHandlerService from '../../error-handler-service.js';
 import { HttpService } from '../../http-service.js';
 
 import endpoints from '../../proxy/data/endpoints.json';
 import config from '../../../config.json';
 const proxyServerURL = `${config.api.proxy.url}${endpoints.proxy.server.url}`;
-const slurmMonitorURL = `${config.api.slurmmonitor.url}/api/v1/partitions/interactive`;
 const availableServersURL = `${config.api.proxy.url}${endpoints.proxy.availableServers.url}`;
 
 let _instance = null;
 const SINGLETON_ENFORCER = Symbol();
 
-const INTERVAL_POLL_SLURM_MONITOR = 5000;
-const INTERVAL_POLL_SERVER_AVAILABILITY = 3000;
-let clusterAvailability = { free: 'N/A', total: 'N/A' };
-
 /**
  * Service handling server resources for simulating experiments.
  */
@@ -31,9 +22,10 @@ class ServerResourcesService extends HttpService {
     this.availableServers = [];
 
     this.startUpdates();
-    window.onbeforeunload = () => {
+    window.addEventListener('beforeunload', (event) => {
       this.stopUpdates();
-    };
+      event.returnValue = '';
+    });
   }
 
   static get instance() {
@@ -48,17 +40,11 @@ class ServerResourcesService extends HttpService {
    * Start polling updates.
    */
   startUpdates() {
-    this.clusterAvailabilityObservable = this._createSlurmMonitorObservable();
-    this.clusterAvailabilitySubscription = this.clusterAvailabilityObservable.subscribe(
-      availability => (clusterAvailability = availability)
-    );
-
-    this.getServerAvailability(true);
     this.intervalGetServerAvailability = setInterval(
       () => {
         this.getServerAvailability(true);
       },
-      INTERVAL_POLL_SERVER_AVAILABILITY
+      ServerResourcesService.CONSTANTS.INTERVAL_POLL_SERVER_AVAILABILITY
     );
   }
 
@@ -66,30 +52,17 @@ class ServerResourcesService extends HttpService {
    * Stop polling updates.
    */
   stopUpdates() {
-    this.clusterAvailabilitySubscription && this.clusterAvailabilitySubscription.unsubscribe();
     this.intervalGetServerAvailability && clearInterval(this.intervalGetServerAvailability);
   }
 
-  /**
-   * Get available cluster server info.
-   * @returns {object} cluster availability info
-   */
-  getClusterAvailability() {
-    return clusterAvailability;
-  }
-
   /**
    * Return a list of available servers for starting simulations.
    * @param {boolean} forceUpdate force an update
    * @returns {Array} A list of available servers.
    */
-  getServerAvailability(forceUpdate = false) {
+  async getServerAvailability(forceUpdate = false) {
     if (!this.availableServers || forceUpdate) {
-      let update = async () => {
-        let response = await this.httpRequestGET(availableServersURL);
-        this.availableServers = await response.json();
-      };
-      update();
+      this.availableServers = await (await this.httpRequestGET(availableServersURL)).json();
       this.emit(ServerResourcesService.EVENTS.UPDATE_SERVER_AVAILABILITY, this.availableServers);
     }
 
@@ -108,32 +81,14 @@ class ServerResourcesService extends HttpService {
       })
       .catch(ErrorHandlerService.instance.displayServerHTTPError);
   }
-
-  _createSlurmMonitorObservable() {
-    return timer(0, INTERVAL_POLL_SLURM_MONITOR)
-      .pipe(switchMap(() => {
-        try {
-          return this.httpRequestGET(slurmMonitorURL);
-        }
-        catch (error) {
-          _.once(error => {
-            if (error.status === -1) {
-              error = Object.assign(error, {
-                data: 'Could not probe vizualization cluster'
-              });
-            }
-            ErrorHandlerService.instance.displayServerHTTPError(error);
-          });
-        }
-      }))
-      .pipe(filter(e => e))
-      .pipe(map(({ free, nodes }) => ({ free, total: nodes[3] })))
-      .pipe(multicast(new Subject())).refCount();
-  }
 }
 
 ServerResourcesService.EVENTS = Object.freeze({
   UPDATE_SERVER_AVAILABILITY: 'UPDATE_SERVER_AVAILABILITY'
 });
 
+ServerResourcesService.CONSTANTS = Object.freeze({
+  INTERVAL_POLL_SERVER_AVAILABILITY: 3000
+});
+
 export default ServerResourcesService;
diff --git a/src/services/experiments/storage/__tests__/experiment-storage-service.test.js b/src/services/experiments/storage/__tests__/experiment-storage-service.test.js
index 2176a82..c87014c 100644
--- a/src/services/experiments/storage/__tests__/experiment-storage-service.test.js
+++ b/src/services/experiments/storage/__tests__/experiment-storage-service.test.js
@@ -15,6 +15,15 @@ const experimentsUrl = `${config.api.proxy.url}${proxyEndpoint.storage.experimen
 
 jest.setTimeout(3 * ExperimentStorageService.CONSTANTS.INTERVAL_POLL_EXPERIMENTS);
 
+let onWindowBeforeUnloadCb = undefined;
+beforeEach(() => {
+  jest.spyOn(window, 'addEventListener').mockImplementation((event, cb) => {
+    if (event === 'beforeunload') {
+      onWindowBeforeUnloadCb = cb;
+    }
+  });
+});
+
 afterEach(() => {
   jest.restoreAllMocks();
 });
@@ -82,6 +91,15 @@ test('does automatic poll updates of experiment list which can be stopped', (don
   }, ExperimentStorageService.CONSTANTS.INTERVAL_POLL_EXPERIMENTS);
 });
 
+test('should stop polling updates when window is unloaded', async () => {
+  let service = ExperimentStorageService.instance;
+  expect(onWindowBeforeUnloadCb).toBeDefined();
+
+  jest.spyOn(service, 'stopUpdates');
+  onWindowBeforeUnloadCb({});
+  expect(service.stopUpdates).toHaveBeenCalled();
+});
+
 test('gets a thumbnail image for experiments', async () => {
   let experiment = MockExperiments[0];
   const imageBlob = await ExperimentStorageService.instance.getThumbnail(experiment.name,
diff --git a/src/services/experiments/storage/experiment-storage-service.js b/src/services/experiments/storage/experiment-storage-service.js
index c3daae8..08199b9 100644
--- a/src/services/experiments/storage/experiment-storage-service.js
+++ b/src/services/experiments/storage/experiment-storage-service.js
@@ -7,8 +7,6 @@ const storageExperimentsURL = `${config.api.proxy.url}${endpoints.proxy.storage.
 let _instance = null;
 const SINGLETON_ENFORCER = Symbol();
 
-const INTERVAL_POLL_EXPERIMENTS = 3000;
-
 /**
  * Service that fetches the template experiments list from the proxy given
  * that the user has authenticated successfully.
@@ -44,7 +42,7 @@ class ExperimentStorageService extends HttpService {
       () => {
         this.getExperiments(true);
       },
-      INTERVAL_POLL_EXPERIMENTS
+      ExperimentStorageService.CONSTANTS.INTERVAL_POLL_EXPERIMENTS
     );
   }
 
@@ -126,7 +124,7 @@ ExperimentStorageService.EVENTS = Object.freeze({
 });
 
 ExperimentStorageService.CONSTANTS = Object.freeze({
-  INTERVAL_POLL_EXPERIMENTS: INTERVAL_POLL_EXPERIMENTS
+  INTERVAL_POLL_EXPERIMENTS: 3000
 });
 
 export default ExperimentStorageService;
diff --git a/src/services/nrp-analytics-service.js b/src/services/nrp-analytics-service.js
index f00751e..2b3c3af 100644
--- a/src/services/nrp-analytics-service.js
+++ b/src/services/nrp-analytics-service.js
@@ -30,10 +30,10 @@ class NrpAnalyticsService {
       options.value = _.toInteger(options.value);
     }
     return NrpUserService.instance.getCurrentUser().then((user) => {
-      var extendedOptions = _.extend(options, {
+      /*var extendedOptions = _.extend(options, {
         label: user.displayName
       });
-      //$analytics.eventTrack(actionName, extendedOptions);
+      $analytics.eventTrack(actionName, extendedOptions);*/
       console.error('implement $analytics.eventTrack(actionName, extendedOptions)');
     });
   }
diff --git a/src/services/proxy/nrp-user-service.js b/src/services/proxy/nrp-user-service.js
index e528d34..a81f2e7 100644
--- a/src/services/proxy/nrp-user-service.js
+++ b/src/services/proxy/nrp-user-service.js
@@ -40,8 +40,7 @@ class NrpUserService extends HttpService {
    * @returns {promise} Request for the user
    */
   async getUser(userID) {
-    let response = await this.httpRequestGET(this.IDENTITY_BASE_URL + '/' + userID);
-    return response.json();
+    return await (await this.httpRequestGET(this.IDENTITY_BASE_URL + '/' + userID)).json();
   }
 
   /**
@@ -62,8 +61,7 @@ class NrpUserService extends HttpService {
    */
   async getCurrentUser() {
     if (!this.currentUser) {
-      let response = await this.httpRequestGET(this.IDENTITY_ME_URL);
-      this.currentUser = response.json();
+      this.currentUser = await (await this.httpRequestGET(this.IDENTITY_ME_URL)).json();
     }
 
     return this.currentUser;
-- 
GitLab