diff --git a/beat/editor/templates/algorithm.jinja2 b/beat/editor/templates/algorithm.jinja2
index 39e9b89961c295db0ada85c3390633cc9aa6b433..ab9eb1c8c399777a7bc7a1788fab4099f45743c9 100644
--- a/beat/editor/templates/algorithm.jinja2
+++ b/beat/editor/templates/algorithm.jinja2
@@ -21,7 +21,7 @@ class Algorithm:
     # this will be called each time the sync'd input has more data available to be processed
     def process(self, inputs, outputs):
         # Groups available:
-        {% for group in contents.groups %}# Group {{ loop.index - 1 }}:
+{% for group in contents.groups %}        # Group {{ loop.index - 1 }}:
         {% for iName, input in group.inputs.items() %}#   Input "{{ iName }}" with type  "{{ input.type }}"
         {% endfor %}{% if 'outputs' in group %}{% for oName, output in group.outputs.items() %}#   Output "{{ oName }}" with type  "{{ output.type }}"
         {% endfor %}{% endif %}{% endfor %}{% if 'splittable' not in contents %}
diff --git a/beat/editor/templates/database.jinja2 b/beat/editor/templates/database.jinja2
index 03c50a1411d80447bc81e186c0f30359274f697d..dd148f219d253e2f09d0cb61598dbc5670a0885c 100644
--- a/beat/editor/templates/database.jinja2
+++ b/beat/editor/templates/database.jinja2
@@ -18,7 +18,7 @@ class {{ view }}(View):
     def get(self, output, index):
         # to get the current object referenced by the given index:
         #       obj = self.objs[index]
-	# note that this object is a named tuple, with fields equivalent to your keys from
-	# the objects returned from the index function
+        # note that this object is a named tuple, with fields equivalent to your keys from
+        # the objects returned from the index function
         pass
 {% endfor %}
diff --git a/beat/editor/test/__init__.py b/beat/editor/test/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..99a1554aa681ba9408a2c5e415a5c011005aba2d
--- /dev/null
+++ b/beat/editor/test/__init__.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+
+###############################################################################
+#                                                                             #
+# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/           #
+# Contact: beat.support@idiap.ch                                              #
+#                                                                             #
+# This file is part of the beat.cmdline module of the BEAT platform.          #
+#                                                                             #
+# Commercial License Usage                                                    #
+# Licensees holding valid commercial BEAT licenses may use this file in       #
+# accordance with the terms contained in a written agreement between you      #
+# and Idiap. For further information contact tto@idiap.ch                     #
+#                                                                             #
+# Alternatively, this file may be used under the terms of the GNU Affero      #
+# Public License version 3 as published by the Free Software and appearing    #
+# in the file LICENSE.AGPL included in the packaging of this file.            #
+# The BEAT platform is distributed in the hope that it will be useful, but    #
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY  #
+# or FITNESS FOR A PARTICULAR PURPOSE.                                        #
+#                                                                             #
+# You should have received a copy of the GNU Affero Public License along      #
+# with the BEAT platform. If not, see http://www.gnu.org/licenses/.           #
+#                                                                             #
+###############################################################################
+
diff --git a/beat/editor/test/test_resources.py b/beat/editor/test/test_resources.py
new file mode 100644
index 0000000000000000000000000000000000000000..884483e4237482c6a4f73f9d62aae113a860a752
--- /dev/null
+++ b/beat/editor/test/test_resources.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+
+###############################################################################
+#                                                                             #
+# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/           #
+# Contact: beat.support@idiap.ch                                              #
+#                                                                             #
+# This file is part of the beat.cmdline module of the BEAT platform.          #
+#                                                                             #
+# Commercial License Usage                                                    #
+# Licensees holding valid commercial BEAT licenses may use this file in       #
+# accordance with the terms contained in a written agreement between you      #
+# and Idiap. For further information contact tto@idiap.ch                     #
+#                                                                             #
+# Alternatively, this file may be used under the terms of the GNU Affero      #
+# Public License version 3 as published by the Free Software and appearing    #
+# in the file LICENSE.AGPL included in the packaging of this file.            #
+# The BEAT platform is distributed in the hope that it will be useful, but    #
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY  #
+# or FITNESS FOR A PARTICULAR PURPOSE.                                        #
+#                                                                             #
+# You should have received a copy of the GNU Affero Public License along      #
+# with the BEAT platform. If not, see http://www.gnu.org/licenses/.           #
+#                                                                             #
+###############################################################################
+
+# test the resources.py file
+# (mostly endpoints and working with the filesystem)
+
+import nose.tools
+import os
+
+from .. import resources
+
+# the func names the endpoint the given name
+def test_check_valid_generated_endpoint_name():
+    name = 'TestEndpoint'
+    endpoint = resources.gen_endpoint(name)
+    nose.tools.eq_(endpoint.__name__, name)
+
+# the func doesnt accept non-entity names
+@nose.tools.raises(AssertionError)
+def test_assert_valid_entity_invalid():
+    resources.assert_valid_entity('notanentity')
+
+# the func parses this file
+def test_path_to_dict_file():
+    currfile = os.path.realpath(__file__)
+    res = resources.path_to_dict(currfile)
+    # in python 3 the first case works but in python 2 the second case works
+    assert res == {'name': 'test_resources.py', 'type': 'file'} or res == {'name': 'test_resources.pyc', 'type': 'file'}
+
+# the func parses this folder
+def test_path_to_dict_folder():
+    currfolder = os.path.dirname(os.path.realpath(__file__))
+    res = resources.path_to_dict(currfolder)
+    nose.tools.eq_(res['name'], 'test')
+    nose.tools.eq_(res['type'], 'directory')
+    nose.tools.ok_({'name': '__init__.py', 'type': 'file'} in res['children'])
+    nose.tools.ok_({'name': 'test_resources.py', 'type': 'file'} in res['children'])
+
diff --git a/beat/editor/test/test_utils.py b/beat/editor/test/test_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..100e17f33aae62d6220504b7cbe5c86f51ccdd0e
--- /dev/null
+++ b/beat/editor/test/test_utils.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+
+###############################################################################
+#                                                                             #
+# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/           #
+# Contact: beat.support@idiap.ch                                              #
+#                                                                             #
+# This file is part of the beat.cmdline module of the BEAT platform.          #
+#                                                                             #
+# Commercial License Usage                                                    #
+# Licensees holding valid commercial BEAT licenses may use this file in       #
+# accordance with the terms contained in a written agreement between you      #
+# and Idiap. For further information contact tto@idiap.ch                     #
+#                                                                             #
+# Alternatively, this file may be used under the terms of the GNU Affero      #
+# Public License version 3 as published by the Free Software and appearing    #
+# in the file LICENSE.AGPL included in the packaging of this file.            #
+# The BEAT platform is distributed in the hope that it will be useful, but    #
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY  #
+# or FITNESS FOR A PARTICULAR PURPOSE.                                        #
+#                                                                             #
+# You should have received a copy of the GNU Affero Public License along      #
+# with the BEAT platform. If not, see http://www.gnu.org/licenses/.           #
+#                                                                             #
+###############################################################################
+
+# test the utils.py file
+# (mostly python file generation via jinja2 templates)
+
+import nose.tools
+import os
+
+from .. import utils
+
+def test_generate_empty_database():
+    empty_db = """# You may import any python packages that will be available in the environment you will run this database in
+# Environments can change based on the experiment's settings
+from beat.backend.python.database import View
+
+class View(View):
+    # build the data for your view
+    # split the raw data into (homogenous) bits and return a keyed iterable
+    # (something with `.keys()` available to it, like a dict)
+    # the key names must be the same as the output names for sets that use this view
+    #    root_folder: the path to the root folder of the database's files (not always applicable)
+    #    parameters: parameters passed to the view, defined in the metadata
+    def index(self, root_folder, parameters):
+        pass
+
+    # returns a value at a specific index in the iterable for this view
+    #   output: the specific output value requested
+    #   index: the current index of the iterable
+    def get(self, output, index):
+        # to get the current object referenced by the given index:
+        #       obj = self.objs[index]
+        # note that this object is a named tuple, with fields equivalent to your keys from
+        # the objects returned from the index function
+        pass
+"""
+    str = utils.generate_database()
+    nose.tools.eq_(str, empty_db)
+
+def test_generate_empty_algorithm():
+    empty_alg = """# You may import any python packages that will be available in the environment you will run this algorithm in
+# Environments can change based on the experiment's settings
+
+class Algorithm:
+    # initialise fields to store cross-input data (e.g. machines, aggregations, etc.)
+    def __init__(self):
+        pass
+
+    # this will be called each time the sync'd input has more data available to be processed
+    def process(self, inputs, outputs):
+        # Groups available:
+
+        # to check if there is more data waiting in the inputs
+        # (if it is False, you have processed all the inputs and this "process" function won't be called again):
+        #       if inputs.hasMoreData():
+
+        # to check if a specific input is done:
+        #       if inputs["input1"].isDataUnitDone():
+
+        # to manually fetch the next input of a specific input
+        # (e.g. the block is not sync'd to the input but you want the input immediately)
+        #       inputs['input1'].next()
+        # you can then access that input value as normal:
+        #       self.val1 = inputs['input1'].data
+
+        # to get the data for an input (note that the value will be of the type specified in the metadata!):
+        #       data_value = inputs['input1'].data
+
+        # to write to an output:
+        #       outputs['output1'].write({
+        #           'output_field_1': 1,
+        #           'output_field_2': 'output'
+        #       })
+
+        # always return True, it signals BEAT to continue processing
+        return True"""
+
+    alg = { 'name': 'user/alg/1', 'contents': { 'splittable': True, 'groups': [], 'uses': {} }}
+    str = utils.generate_algorithm(alg['contents'])
+    nose.tools.eq_(str, empty_alg)
+
+def test_generate_empty_library():
+    empty_lib = """# You may import any python packages that will be available in the environment you will run this library in
+# Environments can change based on the experiment's settings
+"""
+
+    str = utils.generate_library()
+    nose.tools.eq_(str, empty_lib)
diff --git a/conda/js/package-lock.json b/conda/js/package-lock.json
index 660710ce44b23482ce3b39cbc032af4a221b00ed..ee6e7a5eca8b445b24670376fd3f1652a51b6c84 100644
--- a/conda/js/package-lock.json
+++ b/conda/js/package-lock.json
@@ -2524,14 +2524,12 @@
                                                 "balanced-match": {
                                                         "version": "1.0.0",
                                                         "bundled": true,
-                                                        "dev": true,
-                                                        "optional": true
+                                                        "dev": true
                                                 },
                                                 "brace-expansion": {
                                                         "version": "1.1.11",
                                                         "bundled": true,
                                                         "dev": true,
-                                                        "optional": true,
                                                         "requires": {
                                                                 "balanced-match": "^1.0.0",
                                                                 "concat-map": "0.0.1"
@@ -2546,20 +2544,17 @@
                                                 "code-point-at": {
                                                         "version": "1.1.0",
                                                         "bundled": true,
-                                                        "dev": true,
-                                                        "optional": true
+                                                        "dev": true
                                                 },
                                                 "concat-map": {
                                                         "version": "0.0.1",
                                                         "bundled": true,
-                                                        "dev": true,
-                                                        "optional": true
+                                                        "dev": true
                                                 },
                                                 "console-control-strings": {
                                                         "version": "1.1.0",
                                                         "bundled": true,
-                                                        "dev": true,
-                                                        "optional": true
+                                                        "dev": true
                                                 },
                                                 "core-util-is": {
                                                         "version": "1.0.2",
@@ -2676,8 +2671,7 @@
                                                 "inherits": {
                                                         "version": "2.0.3",
                                                         "bundled": true,
-                                                        "dev": true,
-                                                        "optional": true
+                                                        "dev": true
                                                 },
                                                 "ini": {
                                                         "version": "1.3.5",
@@ -2689,7 +2683,6 @@
                                                         "version": "1.0.0",
                                                         "bundled": true,
                                                         "dev": true,
-                                                        "optional": true,
                                                         "requires": {
                                                                 "number-is-nan": "^1.0.0"
                                                         }
@@ -2704,7 +2697,6 @@
                                                         "version": "3.0.4",
                                                         "bundled": true,
                                                         "dev": true,
-                                                        "optional": true,
                                                         "requires": {
                                                                 "brace-expansion": "^1.1.7"
                                                         }
@@ -2816,8 +2808,7 @@
                                                 "number-is-nan": {
                                                         "version": "1.0.1",
                                                         "bundled": true,
-                                                        "dev": true,
-                                                        "optional": true
+                                                        "dev": true
                                                 },
                                                 "object-assign": {
                                                         "version": "4.1.1",
@@ -2950,7 +2941,6 @@
                                                         "version": "1.0.2",
                                                         "bundled": true,
                                                         "dev": true,
-                                                        "optional": true,
                                                         "requires": {
                                                                 "code-point-at": "^1.0.0",
                                                                 "is-fullwidth-code-point": "^1.0.0",
@@ -4123,6 +4113,24 @@
                         "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
                         "dev": true
                 },
+                "deep-equal-in-any-order": {
+                        "version": "1.0.10",
+                        "resolved": "https://registry.npmjs.org/deep-equal-in-any-order/-/deep-equal-in-any-order-1.0.10.tgz",
+                        "integrity": "sha512-hzh3IwlIKwT885r5b/b4bXCXxzR7S9N+Dreuoommdykcnvlg0A+pS81wMU4ZsFz98CH4KBI8eWbAfDHWl7akTg==",
+                        "dev": true,
+                        "requires": {
+                                "lodash": "^4.17.10",
+                                "sort-any": "^1.1.12"
+                        },
+                        "dependencies": {
+                                "lodash": {
+                                        "version": "4.17.10",
+                                        "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
+                                        "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==",
+                                        "dev": true
+                                }
+                        }
+                },
                 "deep-is": {
                         "version": "0.1.3",
                         "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
@@ -6873,6 +6881,12 @@
                         "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==",
                         "dev": true
                 },
+                "immediate": {
+                        "version": "3.0.6",
+                        "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+                        "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
+                        "dev": true
+                },
                 "import-cwd": {
                         "version": "2.1.0",
                         "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@@ -7791,6 +7805,59 @@
                                 "array-includes": "^3.0.3"
                         }
                 },
+                "jszip": {
+                        "version": "3.1.5",
+                        "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.5.tgz",
+                        "integrity": "sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ==",
+                        "dev": true,
+                        "requires": {
+                                "core-js": "~2.3.0",
+                                "es6-promise": "~3.0.2",
+                                "lie": "~3.1.0",
+                                "pako": "~1.0.2",
+                                "readable-stream": "~2.0.6"
+                        },
+                        "dependencies": {
+                                "core-js": {
+                                        "version": "2.3.0",
+                                        "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz",
+                                        "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU=",
+                                        "dev": true
+                                },
+                                "es6-promise": {
+                                        "version": "3.0.2",
+                                        "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz",
+                                        "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=",
+                                        "dev": true
+                                },
+                                "isarray": {
+                                        "version": "1.0.0",
+                                        "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+                                        "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+                                        "dev": true
+                                },
+                                "readable-stream": {
+                                        "version": "2.0.6",
+                                        "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
+                                        "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
+                                        "dev": true,
+                                        "requires": {
+                                                "core-util-is": "~1.0.0",
+                                                "inherits": "~2.0.1",
+                                                "isarray": "~1.0.0",
+                                                "process-nextick-args": "~1.0.6",
+                                                "string_decoder": "~0.10.x",
+                                                "util-deprecate": "~1.0.1"
+                                        }
+                                },
+                                "string_decoder": {
+                                        "version": "0.10.31",
+                                        "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+                                        "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+                                        "dev": true
+                                }
+                        }
+                },
                 "just-extend": {
                         "version": "1.1.27",
                         "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.27.tgz",
@@ -8088,6 +8155,15 @@
                         "integrity": "sha1-9ebgatdLeU+1tbZpiL9yjvHe2+g=",
                         "dev": true
                 },
+                "lie": {
+                        "version": "3.1.1",
+                        "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
+                        "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
+                        "dev": true,
+                        "requires": {
+                                "immediate": "~3.0.5"
+                        }
+                },
                 "load-json-file": {
                         "version": "1.1.0",
                         "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
@@ -12383,6 +12459,12 @@
                         "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==",
                         "dev": true
                 },
+                "sax": {
+                        "version": "1.2.4",
+                        "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+                        "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+                        "dev": true
+                },
                 "schema-utils": {
                         "version": "0.4.5",
                         "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz",
@@ -12407,6 +12489,29 @@
                         "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=",
                         "dev": true
                 },
+                "selenium-webdriver": {
+                        "version": "4.0.0-alpha.1",
+                        "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.1.tgz",
+                        "integrity": "sha512-z88rdjHAv3jmTZ7KSGUkTvo4rGzcDGMq0oXWHNIDK96Gs31JKVdu9+FMtT4KBrVoibg8dUicJDok6GnqqttO5Q==",
+                        "dev": true,
+                        "requires": {
+                                "jszip": "^3.1.3",
+                                "rimraf": "^2.5.4",
+                                "tmp": "0.0.30",
+                                "xml2js": "^0.4.17"
+                        },
+                        "dependencies": {
+                                "tmp": {
+                                        "version": "0.0.30",
+                                        "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz",
+                                        "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=",
+                                        "dev": true,
+                                        "requires": {
+                                                "os-tmpdir": "~1.0.1"
+                                        }
+                                }
+                        }
+                },
                 "selfsigned": {
                         "version": "1.10.3",
                         "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.3.tgz",
@@ -12943,6 +13048,15 @@
                                 }
                         }
                 },
+                "sort-any": {
+                        "version": "1.1.12",
+                        "resolved": "https://registry.npmjs.org/sort-any/-/sort-any-1.1.12.tgz",
+                        "integrity": "sha512-RaVPeOjzn5tjhAfvQstA34gPiG/HT+1MefSsG0KHIMhXjLlZREYSIkxlYaCRvdwLKNgpsgM6nvpLEY1Y0Ja9FQ==",
+                        "dev": true,
+                        "requires": {
+                                "lodash": "^4.17.4"
+                        }
+                },
                 "source-list-map": {
                         "version": "2.0.0",
                         "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz",
@@ -16109,6 +16223,22 @@
                         "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=",
                         "dev": true
                 },
+                "xml2js": {
+                        "version": "0.4.19",
+                        "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
+                        "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
+                        "dev": true,
+                        "requires": {
+                                "sax": ">=0.6.0",
+                                "xmlbuilder": "~9.0.1"
+                        }
+                },
+                "xmlbuilder": {
+                        "version": "9.0.7",
+                        "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
+                        "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=",
+                        "dev": true
+                },
                 "xmlhttprequest-ssl": {
                         "version": "1.5.5",
                         "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
diff --git a/conda/js/package.json b/conda/js/package.json
index 01ee1093209379241d06247f948cc992021f8ee6..9e0d446c48da7856a9505aa4c2c9812175a471d5 100644
--- a/conda/js/package.json
+++ b/conda/js/package.json
@@ -37,6 +37,7 @@
                 "chai-enzyme": "^1.0.0-beta.1",
                 "cross-env": "^5.2.0",
                 "css-loader": "^1.0.0",
+                "deep-equal-in-any-order": "^1.0.10",
                 "enzyme": "^3.3.0",
                 "enzyme-adapter-react-16": "^1.1.1",
                 "eslint": "^5.2.0",
@@ -66,6 +67,7 @@
                 "react-test-renderer": "^16.4.1",
                 "redux-devtools": "^3.4.1",
                 "rimraf": "^2.6.2",
+                "selenium-webdriver": "^4.0.0-alpha.1",
                 "sinon": "^6.1.4",
                 "style-loader": "^0.21.0",
                 "stylelint": "^9.3.0",
diff --git a/conda/js/src/components/toolchain/GraphicalEditor.jsx b/conda/js/src/components/toolchain/GraphicalEditor.jsx
index 71063bf161a1ad066eb9bd2f427882f615dafb79..860aa27ccaa3e4aeaaf1de13c8531a0a5754a94b 100644
--- a/conda/js/src/components/toolchain/GraphicalEditor.jsx
+++ b/conda/js/src/components/toolchain/GraphicalEditor.jsx
@@ -583,7 +583,7 @@ export default class GraphicalEditor extends React.PureComponent<Props, State> {
 	}
 
 	// show a little black dot signifying the closest grid point to the current mouse location.
-	// left-clicking the svg background will use this location for its context menu's actions (e.g. adding/pasting blocks).
+	// right-clicking the svg background will use this location for its context menu's actions (e.g. adding/pasting blocks).
 	initD3MouseMovement = () => {
 		// mouse movement
 		setTimeout(() => {
diff --git a/conda/js/src/components/toolchain/InsertObjectModal.jsx b/conda/js/src/components/toolchain/InsertObjectModal.jsx
index f0ae1cd46fb94306ffe7abe79f810982c59b2b1c..cf3fc49a165cc7e25aaa4548e01639244a0f842e 100644
--- a/conda/js/src/components/toolchain/InsertObjectModal.jsx
+++ b/conda/js/src/components/toolchain/InsertObjectModal.jsx
@@ -17,9 +17,6 @@ import {
 	ListGroupItem,
 } from 'reactstrap';
 
-import { connect } from 'react-redux';
-
-import * as Selectors from '@store/selectors.js';
 import type { FlattenedDatabaseEntry } from '@store/selectors';
 
 import type { ConnectionType } from './types.js';
@@ -96,6 +93,7 @@ class InsertObjectModal extends React.PureComponent<Props, State> {
 	updateFuseInstance = (data: any[] = this.props[this.state.searchFilter]) => {
 		if(!this.searchWorker)
 			throw new Error(`searchWorker is not instantiated!`);
+
 		this.searchWorker.postMessage(
 			{
 				type: 'dataChanged',
@@ -391,15 +389,4 @@ class InsertObjectModal extends React.PureComponent<Props, State> {
 	}
 }
 
-const mapStateToProps = (state, ownProps) => {
-	const obj = {
-		toolchains: Selectors.toolchainGet(state),
-		protocols: Selectors.databaseProtocols(state),
-		sets: Selectors.flattenedDatabases(state),
-		normalAlgorithms: Selectors.normalBlocks(state),
-		analyzerAlgorithms: Selectors.analyzerBlocks(state),
-	};
-	return obj;
-};
-
-export default connect(mapStateToProps)(InsertObjectModal);
+export default InsertObjectModal;
diff --git a/conda/js/src/components/toolchain/ToolchainEditor.jsx b/conda/js/src/components/toolchain/ToolchainEditor.jsx
index e6c8b87f67436be5c8b83a177a2ef5db2f8f59e8..d04a59c6bc1a384df2d85014dbffec7e2afc7042 100644
--- a/conda/js/src/components/toolchain/ToolchainEditor.jsx
+++ b/conda/js/src/components/toolchain/ToolchainEditor.jsx
@@ -62,6 +62,8 @@ type Props = {
 	// all databases
 	databases: BeatObject[],
 	// sets of protocols of databases
+	// get protocols for InsertObjectModal
+	protocols: any,
 	sets: FlattenedDatabaseEntry[],
 	normalAlgorithms: BeatObject[],
 	analyzerAlgorithms: BeatObject[],
@@ -908,13 +910,15 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 		}
 	}
 
+	lastClickBlockName = ''
 	lastClickMs = 0
 	// handles left clicking on a block
 	handleBlockClick = (blockName: string, set: BlockSet) => {
 		const currTime = Date.now();
 		// 1sec throttling
 		const delay = 1000;
-		if(currTime - this.lastClickMs > delay){
+		if(currTime - this.lastClickMs > delay || blockName !== this.lastClickBlockName){
+			this.lastClickBlockName = blockName;
 			const newMBI = {
 				set,
 				name: blockName,
@@ -924,6 +928,7 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 				modalBlockInfo: newMBI,
 			});
 		}
+		this.lastClickBlockName = blockName;
 		this.lastClickMs = currTime;
 	}
 
@@ -1195,6 +1200,11 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 	renderInsertModal = () => {
 		return (
 			<InsertObjectModal
+				toolchains={this.props.toolchains}
+				protocols={this.props.protocols}
+				sets={this.props.sets}
+				normalAlgorithms={this.props.normalAlgorithms}
+				analyzerAlgorithms={this.props.analyzerAlgorithms}
 				active={this.state.insertModalActive}
 				toggle={this.toggleInsertModal}
 				addNewBlocks={(type, newBlocksData, newConnections) => {
@@ -1270,6 +1280,7 @@ const mapStateToProps = (state, ownProps) => {
 		toolchains: tcs,
 		databases: Selectors.databaseGet(state),
 		sets: Selectors.flattenedDatabases(state),
+		protocols: Selectors.databaseProtocols(state),
 		normalAlgorithms: Selectors.normalBlocks(state),
 		analyzerAlgorithms: Selectors.analyzerBlocks(state),
 		data: tcs[ownProps.index] || getValidObj()
diff --git a/conda/js/src/components/toolchain/ToolchainEditor.spec.jsx b/conda/js/src/components/toolchain/ToolchainEditor.spec.jsx
index 601ee0404ef7e270ee15a16ed6e03a11d1c1c308..0756590ae7813a381018296fa69ba5f971fe4567 100644
--- a/conda/js/src/components/toolchain/ToolchainEditor.spec.jsx
+++ b/conda/js/src/components/toolchain/ToolchainEditor.spec.jsx
@@ -1,16 +1,27 @@
 // @flow
 import React from 'react';
-import { expect } from 'chai';
+import chai, { expect } from 'chai';
 import { mount } from 'enzyme';
 import sinon from 'sinon';
 import { spies } from '@test';
+// sometimes we dont care about order of items in arrays when comparing objects deeply
+import deepEqualInAnyOrder from 'deep-equal-in-any-order';
 
+import { getValidToolchainObj as getValidObj, getValidDatabaseObj, getValidAlgorithmObj } from '@helpers/beat';
 import { ToolchainEditor as C } from '.';
+import * as Selectors from '@store/selectors';
+import reducer from '@store/reducers';
 
 import testTcs from '@test/test_tcs.json';
+import testDbs from '@test/test_dbs.json';
+import testAlgs from '@test/test_algs.json';
+
+chai.use(deepEqualInAnyOrder);
+
+describe('<ToolchainEditor />', function() {
+	// these tests might take a long time, comparatively
+	this.timeout(10000);
 
-// TODO: fix web workers breaking tests
-describe.skip('<ToolchainEditor />', () => {
 	let wrapper;
 
 	afterEach(() => {
@@ -19,30 +30,870 @@ describe.skip('<ToolchainEditor />', () => {
 	});
 
 	describe('accepts', () => {
-		const tcs = [
-		].concat(testTcs);
+		const tcs = testTcs.map(tc => getValidObj(tc));
+		const dbs = testDbs.map(db => getValidDatabaseObj(db));
+		const algs = testAlgs.map(alg => getValidAlgorithmObj(alg));
+
+		const state = {
+			...reducer({}, { type: '', payload: {}}),
+			toolchain: tcs,
+			database: dbs,
+			algorithm: algs,
+		};
+
+		const sets = Selectors.flattenedDatabases(state);
+		const protocols = Selectors.databaseProtocols(state);
+		const normalAlgorithms = Selectors.normalBlocks(state);
+		const analyzerAlgorithms = Selectors.analyzerBlocks(state);
 
 		tcs.forEach(function(tc){
 			const saveFunc = () => {};
+			const updateFunc = () => {};
 			it(`${ tc.name }`, () => {
 				wrapper = mount(
 					<C
 						data={tc}
-						toolchains={tcs}
+						sets={sets}
+						protocols={protocols}
+						toolchains={state.toolchain}
+						databases={state.database}
+						normalAlgorithms={normalAlgorithms}
+						analyzerAlgorithms={analyzerAlgorithms}
 						saveFunc={saveFunc}
+						updateFunc={updateFunc}
 					/>
 				);
 
 				expect(wrapper).to.have.props(
-					['data', 'toolchains', 'saveFunc']
+					['data', 'sets', 'protocols', 'toolchains', 'databases', 'normalAlgorithms', 'analyzerAlgorithms', 'saveFunc', 'updateFunc']
 				).deep.equal(
-					[tc, tcs, saveFunc]
+					[tc, sets, protocols, tcs, state.database, normalAlgorithms, analyzerAlgorithms, saveFunc, updateFunc]
 				);
 			});
 		});
 	});
 
 	describe('creates', () => {
+		const tcs = [];
+		const dbs = testDbs.map(db => getValidDatabaseObj(db));
+		const algs = testAlgs.map(alg => getValidAlgorithmObj(alg));
+
+		const state = {
+			...reducer({}, { type: '', payload: {}}),
+			toolchain: tcs,
+			database: dbs,
+			algorithm: algs,
+		};
+
+		const sets = Selectors.flattenedDatabases(state);
+		const normalAlgorithms = Selectors.normalBlocks(state);
+		const protocols = Selectors.databaseProtocols(state);
+		const analyzerAlgorithms = Selectors.analyzerBlocks(state);
+
+		it(`test/iris/1`, () => {
+			const saveFunc = sinon.spy();
+			const _updateFunc = (obj) => {
+				wrapper.setProps && wrapper.setProps({ data: obj });
+			};
+			const updateFunc = sinon.spy(_updateFunc);
+			const tcName = 'test/iris/1';
+			const tc = getValidObj({name: tcName, contents: {}});
+			wrapper = mount(
+				<C
+					data={tc}
+					sets={sets}
+					protocols={protocols}
+					toolchains={state.toolchain}
+					databases={state.database}
+					normalAlgorithms={normalAlgorithms}
+					analyzerAlgorithms={analyzerAlgorithms}
+					saveFunc={saveFunc}
+					updateFunc={updateFunc}
+				/>
+			);
+
+			expect(wrapper).to.have.props(
+				['data', 'sets', 'protocols', 'toolchains', 'databases', 'normalAlgorithms', 'analyzerAlgorithms', 'saveFunc', 'updateFunc']
+			);
+
+			expect(wrapper.props().data).to.have.property('name', tcName);
+
+			// add lots of blocks
+			const _selectBlocks = (bNames: string[]) => { return; };
+			const selectBlocks = sinon.spy(_selectBlocks);
+
+
+			/* add blocks via contextmenu handler */
+
+			// pretend to right click at a spot by calling the event handler
+			// dataset 1 training_data @ 6,0
+			wrapper.instance().handleSvgContextMenu({}, { clicked: 'addDataset', x: 6, y: 0, selectBlocks });
+			wrapper.update();
+			expect(updateFunc.callCount).to.equal(1);
+			expect(wrapper.props().data.contents.datasets.length).to.equal(1);
+
+			// dataset 2 @ 6,5
+			wrapper.instance().handleSvgContextMenu({}, { clicked: 'addDataset', x: 6, y: 5, selectBlocks });
+			wrapper.update();
+			expect(updateFunc.callCount).to.equal(2);
+			expect(wrapper.props().data.contents.datasets.length).to.equal(2);
+
+			// block 1 (training) @ 19,0
+			wrapper.instance().handleSvgContextMenu({}, { clicked: 'addBlock', x: 19, y: 0, selectBlocks });
+			wrapper.update();
+			expect(updateFunc.callCount).to.equal(3);
+			expect(wrapper.props().data.contents.blocks.length).to.equal(1);
+
+			// block 2 (testing) @ 32,3
+			wrapper.instance().handleSvgContextMenu({}, { clicked: 'addBlock', x: 32, y: 3, selectBlocks });
+			wrapper.update();
+			expect(updateFunc.callCount).to.equal(4);
+			expect(wrapper.props().data.contents.blocks.length).to.equal(2);
+
+			// block 3 (analyzer) @ 46,4
+			wrapper.instance().handleSvgContextMenu({}, { clicked: 'addAnalyzer', x: 46, y: 4, selectBlocks });
+			wrapper.update();
+			expect(updateFunc.callCount).to.equal(5);
+			expect(wrapper.props().data.contents.analyzers.length).to.equal(1);
+
+			/* open edit modal for each & edit blocks via input onChange stuff */
+
+			// training_data
+			wrapper.find('rect#block_dataset').simulate('click');
+			wrapper.update();
+			expect(wrapper.find('ToolchainModal').props().active).to.equal(true);
+			expect(wrapper.find('.modal').find('CacheInput').props().value).to.equal('dataset');
+			wrapper.find('.modal').find('CacheInput').prop('onChange')( { target: { value: 'training_data' }});
+			wrapper.update();
+			wrapper.find('.modal button.btn-secondary').simulate('click');
+			wrapper.update();
+			wrapper.find('.modal button.btn-secondary').simulate('click');
+			wrapper.update();
+			wrapper.find('.modal CacheInput[value="output"]').prop('onChange')( { target: { value: 'measurements' }});
+			wrapper.update();
+			wrapper.find('.modal CacheInput[value="output0"]').prop('onChange')( { target: { value: 'species' }});
+			wrapper.update();
+			wrapper.find('button.close').simulate('click');
+			wrapper.update();
+			expect(wrapper.find('ToolchainModal').props().active).to.equal(false);
+
+			expect(wrapper.props().data.contents.datasets[0]).to.deep.equal({
+				'name': 'training_data',
+				'outputs': [
+					'measurements',
+					'species'
+				]
+			});
+
+			// testing_data
+			wrapper.find('rect#block_dataset0').simulate('click');
+			wrapper.update();
+			expect(wrapper.find('ToolchainModal').props().active).to.equal(true);
+			expect(wrapper.find('.modal').find('CacheInput').props().value).to.equal('dataset0');
+			wrapper.find('.modal').find('CacheInput').prop('onChange')( { target: { value: 'testing_data' }});
+			wrapper.update();
+			wrapper.find('.modal button.btn-secondary').simulate('click');
+			wrapper.update();
+			wrapper.find('.modal button.btn-secondary').simulate('click');
+			wrapper.update();
+			wrapper.find('.modal CacheInput[value="output"]').prop('onChange')( { target: { value: 'measurements' }});
+			wrapper.update();
+			wrapper.find('.modal CacheInput[value="output0"]').prop('onChange')( { target: { value: 'species' }});
+			wrapper.update();
+			wrapper.find('button.close').simulate('click');
+			wrapper.update();
+
+			expect(wrapper.props().data.contents.datasets[1]).to.deep.equal({
+				'name': 'testing_data',
+				'outputs': [
+					'measurements',
+					'species'
+				]
+			});
+
+			// training_alg
+			wrapper.find('rect#block_block').simulate('click');
+			wrapper.update();
+			expect(wrapper.find('ToolchainModal').props().active).to.equal(true);
+			expect(wrapper.find('.modal').find('CacheInput').props().value).to.equal('block');
+			wrapper.find('.modal').find('CacheInput').prop('onChange')( { target: { value: 'training_alg' }});
+			wrapper.update();
+			wrapper.find('.modal button.btn-secondary').at(0).simulate('click');
+			wrapper.update();
+			wrapper.find('.modal button.btn-secondary').at(0).simulate('click');
+			wrapper.update();
+			wrapper.find('.modal button.btn-secondary').at(1).simulate('click');
+			wrapper.update();
+			wrapper.find('.modal CacheInput[value="input"]').prop('onChange')( { target: { value: 'measurements' }});
+			wrapper.update();
+			wrapper.find('.modal CacheInput[value="input0"]').prop('onChange')( { target: { value: 'species' }});
+			wrapper.update();
+			wrapper.find('.modal CacheInput[value="output"]').prop('onChange')( { target: { value: 'lda_machine' }});
+			wrapper.update();
+			wrapper.find('button.close').simulate('click');
+			wrapper.update();
+
+			expect(wrapper.props().data.contents.blocks[0]).to.deep.equal({
+				'name': 'training_alg',
+				'inputs': [
+					'measurements',
+					'species'
+				],
+				'outputs': [
+					'lda_machine'
+				],
+				'synchronized_channel': 'training_data',
+			});
+
+			// testing_alg
+			wrapper.find('rect#block_block0').simulate('click');
+			wrapper.update();
+			expect(wrapper.find('ToolchainModal').props().active).to.equal(true);
+			expect(wrapper.find('.modal').find('CacheInput').props().value).to.equal('block0');
+			wrapper.find('.modal').find('CacheInput').prop('onChange')( { target: { value: 'testing_alg' }});
+			wrapper.update();
+			wrapper.find('.modal button.btn-secondary').at(0).simulate('click');
+			wrapper.update();
+			wrapper.find('.modal button.btn-secondary').at(0).simulate('click');
+			wrapper.update();
+			wrapper.find('.modal button.btn-secondary').at(1).simulate('click');
+			wrapper.update();
+			wrapper.find('.modal CacheInput[value="input"]').prop('onChange')( { target: { value: 'measurements' }});
+			wrapper.update();
+			wrapper.find('.modal CacheInput[value="input0"]').prop('onChange')( { target: { value: 'lda_machine' }});
+			wrapper.update();
+			wrapper.find('.modal CacheInput[value="output"]').prop('onChange')( { target: { value: 'scores' }});
+			wrapper.update();
+			wrapper.find('button.close').simulate('click');
+			wrapper.update();
+
+			expect(wrapper.props().data.contents.blocks[1]).to.deep.equal({
+				'name': 'testing_alg',
+				'inputs': [
+					'measurements',
+					'lda_machine'
+				],
+				'outputs': [
+					'scores'
+				],
+				'synchronized_channel': 'training_data',
+			});
+
+			// analyzer
+			wrapper.find('rect#block_analyzer').simulate('click');
+			wrapper.update();
+			expect(wrapper.find('ToolchainModal').props().active).to.equal(true);
+			expect(wrapper.find('.modal').find('CacheInput').props().value).to.equal('analyzer');
+			wrapper.find('.modal button.btn-secondary').simulate('click');
+			wrapper.update();
+			wrapper.find('.modal button.btn-secondary').simulate('click');
+			wrapper.update();
+			wrapper.find('.modal CacheInput[value="input"]').prop('onChange')( { target: { value: 'scores' }});
+			wrapper.update();
+			wrapper.find('.modal CacheInput[value="input0"]').prop('onChange')( { target: { value: 'species' }});
+			wrapper.update();
+			wrapper.find('button.close').simulate('click');
+			wrapper.update();
+
+			expect(wrapper.props().data.contents.analyzers[0]).to.deep.equal({
+				'name': 'analyzer',
+				'inputs': [
+					'scores',
+					'species'
+				],
+				'synchronized_channel': 'training_data',
+			});
 
+			/* connect stuff via createConnections() */
+			// channel: training_data
+			wrapper.instance().createConnections([{ from: 'training_data.measurements', to: 'training_alg.measurements', channel: 'training_data' }]);
+			// channel: training_data
+			wrapper.instance().createConnections([{ from: 'training_data.species', to: 'training_alg.species', channel: 'training_data' }]);
+			// channel: training_data
+			wrapper.instance().createConnections([{ from: 'training_alg.lda_machine', to: 'testing_alg.lda_machine', channel: 'training_data' }]);
+			// channel: testing_data
+			wrapper.instance().createConnections([{ from: 'testing_data.measurements', to: 'testing_alg.measurements', channel: 'testing_data' }]);
+			// channel: training_data
+			wrapper.instance().createConnections([{ from: 'testing_alg.scores', to: 'analyzer.scores', channel: 'training_data' }]);
+			// channel: testing_data
+			wrapper.instance().createConnections([{ from: 'testing_data.species', to: 'analyzer.species', channel: 'testing_data' }]);
+
+			/* fix channels */
+			// testing_alg
+			wrapper.find('rect#block_testing_alg').simulate('click');
+			wrapper.update();
+			expect(wrapper.find('ToolchainModal').props().active).to.equal(true);
+			wrapper.find('.modal select').prop('onChange')( { target: { value: 'testing_data' }});
+			wrapper.update();
+			wrapper.find('button.close').simulate('click');
+			wrapper.update();
+
+			expect(wrapper.props().data.contents.blocks[1]).to.deep.equal({
+				'name': 'testing_alg',
+				'inputs': [
+					'measurements',
+					'lda_machine'
+				],
+				'outputs': [
+					'scores'
+				],
+				'synchronized_channel': 'testing_data',
+			});
+
+			// analyzer
+			wrapper.find('rect#block_analyzer').simulate('click');
+			wrapper.update();
+			expect(wrapper.find('ToolchainModal').props().active).to.equal(true);
+			wrapper.find('.modal select').prop('onChange')( { target: { value: 'testing_data' }});
+			wrapper.update();
+			wrapper.find('button.close').simulate('click');
+			wrapper.update();
+
+			expect(wrapper.props().data.contents.analyzers[0]).to.deep.equal({
+				'name': 'analyzer',
+				'inputs': [
+					'scores',
+					'species'
+				],
+				'synchronized_channel': 'testing_data',
+			});
+
+			/* theres alot of expect statements here for a reason:
+			 * each statement checks a sub-part of the toolchain
+			 * its redundant when the test passes but helpful when somethings off,
+			 * you can narrow the part of the tc that isn't right alot quicker
+			 */
+			const data = wrapper.props().data;
+			const ch1 = data.contents.representation.channel_colors['training_data'];
+			const ch2 = data.contents.representation.channel_colors['testing_data'];
+			expect(data.contents.datasets).to.deep.equalInAnyOrder( [
+				{
+					'name': 'training_data',
+					'outputs': [
+						'measurements',
+						'species'
+					]
+				},
+				{
+					'name': 'testing_data',
+					'outputs': [
+						'measurements',
+						'species'
+					]
+				}
+			]);
+
+			expect(data.contents.blocks).to.deep.equalInAnyOrder([
+				{
+					'inputs': [
+						'measurements',
+						'species'
+					],
+					'name': 'training_alg',
+					'outputs': [
+						'lda_machine'
+					],
+					'synchronized_channel': 'training_data'
+				},
+				{
+					'inputs': [
+						'lda_machine',
+						'measurements'
+					],
+					'name': 'testing_alg',
+					'outputs': [
+						'scores'
+					],
+					'synchronized_channel': 'testing_data'
+				}
+			]);
+
+			expect(data.contents.analyzers).to.deep.equalInAnyOrder([
+				{
+					'inputs': [
+						'scores',
+						'species'
+					],
+					'name': 'analyzer',
+					'synchronized_channel': 'testing_data'
+				}
+			]);
+
+			expect(data.contents.connections).to.deep.equalInAnyOrder([
+				{
+					'channel': 'testing_data',
+					'from': 'testing_alg.scores',
+					'to': 'analyzer.scores'
+				},
+				{
+					'channel': 'training_data',
+					'from': 'training_alg.lda_machine',
+					'to': 'testing_alg.lda_machine'
+				},
+				{
+					'channel': 'testing_data',
+					'from': 'testing_data.measurements',
+					'to': 'testing_alg.measurements'
+				},
+				{
+					'channel': 'training_data',
+					'from': 'training_data.measurements',
+					'to': 'training_alg.measurements'
+				},
+				{
+					'channel': 'training_data',
+					'from': 'training_data.species',
+					'to': 'training_alg.species'
+				},
+				{
+					'channel': 'testing_data',
+					'from': 'testing_data.species',
+					'to': 'analyzer.species'
+				}
+			]);
+
+			expect(data.contents.representation).to.deep.equalInAnyOrder({
+				'blocks': {
+					'analyzer': {
+						'col': 46,
+						'height': 3,
+						'row': 4,
+						'width': 10
+					},
+					'testing_alg': {
+						'col': 32,
+						'height': 3,
+						'row': 3,
+						'width': 10
+					},
+					'testing_data': {
+						'col': 6,
+						'height': 3,
+						'row': 5,
+						'width': 10
+					},
+					'training_alg': {
+						'col': 19,
+						'height': 3,
+						'row': 0,
+						'width': 10
+					},
+					'training_data': {
+						'col': 6,
+						'height': 3,
+						'row': 0,
+						'width': 10
+					}
+				},
+				'channel_colors': {
+					'testing_data': ch2,
+					'training_data': ch1
+				},
+				'connections': {
+					'testing_alg.scores/analyzer.scores': [],
+					'testing_data.measurements/testing_alg.measurements': [],
+					'testing_data.species/analyzer.species': [],
+					'training_alg.lda_machine/testing_alg.lda_machine': [],
+					'training_data.measurements/training_alg.measurements': [],
+					'training_data.species/training_alg.species': []
+				}
+			});
+
+			expect(data).to.deep.equalInAnyOrder({
+				'name': 'test/iris/1',
+				'contents': {
+					'description': '',
+					'datasets': [
+						{
+							'name': 'training_data',
+							'outputs': [
+								'measurements',
+								'species'
+							]
+						},
+						{
+							'name': 'testing_data',
+							'outputs': [
+								'measurements',
+								'species'
+							]
+						}
+					],
+					'blocks': [
+						{
+							'inputs': [
+								'measurements',
+								'species'
+							],
+							'name': 'training_alg',
+							'outputs': [
+								'lda_machine'
+							],
+							'synchronized_channel': 'training_data'
+						},
+						{
+							'inputs': [
+								'lda_machine',
+								'measurements'
+							],
+							'name': 'testing_alg',
+							'outputs': [
+								'scores'
+							],
+							'synchronized_channel': 'testing_data'
+						}
+					],
+					'analyzers': [
+						{
+							'inputs': [
+								'scores',
+								'species'
+							],
+							'name': 'analyzer',
+							'synchronized_channel': 'testing_data'
+						}
+					],
+					'connections': [
+						{
+							'channel': 'testing_data',
+							'from': 'testing_alg.scores',
+							'to': 'analyzer.scores'
+						},
+						{
+							'channel': 'training_data',
+							'from': 'training_alg.lda_machine',
+							'to': 'testing_alg.lda_machine'
+						},
+						{
+							'channel': 'testing_data',
+							'from': 'testing_data.measurements',
+							'to': 'testing_alg.measurements'
+						},
+						{
+							'channel': 'training_data',
+							'from': 'training_data.measurements',
+							'to': 'training_alg.measurements'
+						},
+						{
+							'channel': 'training_data',
+							'from': 'training_data.species',
+							'to': 'training_alg.species'
+						},
+						{
+							'channel': 'testing_data',
+							'from': 'testing_data.species',
+							'to': 'analyzer.species'
+						}
+					],
+					'representation': {
+						'blocks': {
+							'analyzer': {
+								'col': 46,
+								'height': 3,
+								'row': 4,
+								'width': 10
+							},
+							'testing_alg': {
+								'col': 32,
+								'height': 3,
+								'row': 3,
+								'width': 10
+							},
+							'testing_data': {
+								'col': 6,
+								'height': 3,
+								'row': 5,
+								'width': 10
+							},
+							'training_alg': {
+								'col': 19,
+								'height': 3,
+								'row': 0,
+								'width': 10
+							},
+							'training_data': {
+								'col': 6,
+								'height': 3,
+								'row': 0,
+								'width': 10
+							}
+						},
+						'channel_colors': {
+							'testing_data': ch2,
+							'training_data': ch1
+						},
+						'connections': {
+							'testing_alg.scores/analyzer.scores': [],
+							'testing_data.measurements/testing_alg.measurements': [],
+							'testing_data.species/analyzer.species': [],
+							'training_alg.lda_machine/testing_alg.lda_machine': [],
+							'training_data.measurements/training_alg.measurements': [],
+							'training_data.species/training_alg.species': []
+						}
+					}
+				},
+				'extraContents': {
+					'groups': []
+				}
+			});
+		});
+	});
+
+	describe.only('Insert Object Modal', () => {
+		const timeout = (ms) => {
+			return new Promise(resolve => setTimeout(resolve, ms));
+		}
+
+		const tcs = testTcs.map(tc => getValidObj(tc));
+		const dbs = testDbs.map(db => getValidDatabaseObj(db));
+		const algs = testAlgs.map(alg => getValidAlgorithmObj(alg));
+
+		const state = {
+			...reducer({}, { type: '', payload: {}}),
+			toolchain: tcs,
+			database: dbs,
+			algorithm: algs,
+		};
+
+		const sets = Selectors.flattenedDatabases(state);
+		const normalAlgorithms = Selectors.normalBlocks(state);
+		const protocols = Selectors.databaseProtocols(state);
+		const analyzerAlgorithms = Selectors.analyzerBlocks(state);
+
+		it(`inserts test/iris_advanced/1`, async () => {
+			const saveFunc = sinon.spy();
+			const _updateFunc = (obj) => {
+				wrapper.setProps && wrapper.setProps({ data: obj });
+			};
+			const updateFunc = sinon.spy(_updateFunc);
+			const tcName = 'test/iom/1';
+			const tc = getValidObj({name: tcName, contents: {}});
+			wrapper = mount(
+				<C
+					data={tc}
+					sets={sets}
+					protocols={protocols}
+					toolchains={state.toolchain}
+					databases={state.database}
+					normalAlgorithms={normalAlgorithms}
+					analyzerAlgorithms={analyzerAlgorithms}
+					saveFunc={saveFunc}
+					updateFunc={updateFunc}
+				/>
+			);
+
+			expect(wrapper).to.have.props(
+				['data', 'sets', 'protocols', 'toolchains', 'databases', 'normalAlgorithms', 'analyzerAlgorithms', 'saveFunc', 'updateFunc']
+			);
+
+			expect(wrapper.props().data).to.have.property('name', tcName);
+
+			const _selectBlocks = (bNames: string[]) => { return; };
+			const selectBlocks = sinon.spy(_selectBlocks);
+
+			// context-menu insert obj modal
+			// insert at x:6 because thats where the original toolchain has its first block on the x-axis
+			wrapper.instance().handleSvgContextMenu({}, { clicked: 'addObject', x: 6, y: 0, selectBlocks });
+			wrapper.update();
+			expect(wrapper.find('InsertObjectModal').props().active).to.equal(true);
+			expect(updateFunc.callCount).to.equal(0);
+
+			// search for "test/iris" in toolchain list
+			wrapper.find('InsertObjectModal Input[placeholder="Search..."]').prop('onChange')( { target: { value: 'test/iris' }});
+			wrapper.update();
+
+			// wait for fuse process to search & return result list
+			await timeout(1000);
+			wrapper.update();
+
+			// click on the iris_advanced one, which should be first
+			const res = wrapper.find('ListGroup.searchResults ListGroupItem');
+			expect(res).to.have.lengthOf(4);
+			expect(res.at(0).html()).to.include('test/iris_advanced/1');
+			res.at(0).simulate('click');
+			wrapper.update();
+
+			const data = wrapper.props().data;
+			const ch1 = data.contents.representation.channel_colors['training_data'];
+			const ch2 = data.contents.representation.channel_colors['testing_data'];
+
+			// make sure the toolchain was inserted
+			// basically make sure the toolchain is the same as test/iris_advanced/1 but w different name
+			expect(data.contents).to.deep.equalInAnyOrder({
+				'analyzers': [
+					{
+						'inputs': [
+							'scores',
+							'species'
+						],
+						'name': 'analyzer',
+						'synchronized_channel': 'testing_data'
+					}
+				],
+				'blocks': [
+					{
+						'inputs': [
+							'measurements',
+							'species'
+						],
+						'name': 'training_alg',
+						'outputs': [
+							'lda_machine'
+						],
+						'synchronized_channel': 'training_data'
+					},
+					{
+						'inputs': [
+							'lda_machine',
+							'measurements'
+						],
+						'name': 'testing_alg',
+						'outputs': [
+							'scores'
+						],
+						'synchronized_channel': 'testing_data'
+					},
+					{
+						'inputs': [
+							'measurements'
+						],
+						'name': 'pre_training',
+						'outputs': [
+							'measurements'
+						],
+						'synchronized_channel': 'training_data'
+					},
+					{
+						'inputs': [
+							'measurements'
+						],
+						'name': 'pre_testing',
+						'outputs': [
+							'measurements'
+						],
+						'synchronized_channel': 'testing_data'
+					}
+				],
+				'connections': [
+					{
+						'channel': 'testing_data',
+						'from': 'testing_alg.scores',
+						'to': 'analyzer.scores'
+					},
+					{
+						'channel': 'training_data',
+						'from': 'training_alg.lda_machine',
+						'to': 'testing_alg.lda_machine'
+					},
+					{
+						'channel': 'testing_data',
+						'from': 'testing_data.species',
+						'to': 'analyzer.species'
+					},
+					{
+						'channel': 'training_data',
+						'from': 'training_data.species',
+						'to': 'training_alg.species'
+					},
+					{
+						'channel': 'training_data',
+						'from': 'training_data.measurements',
+						'to': 'pre_training.measurements'
+					},
+					{
+						'channel': 'training_data',
+						'from': 'pre_training.measurements',
+						'to': 'training_alg.measurements'
+					},
+					{
+						'channel': 'testing_data',
+						'from': 'testing_data.measurements',
+						'to': 'pre_testing.measurements'
+					},
+					{
+						'channel': 'testing_data',
+						'from': 'pre_testing.measurements',
+						'to': 'testing_alg.measurements'
+					}
+				],
+				'datasets': [
+					{
+						'name': 'training_data',
+						'outputs': [
+							'measurements',
+							'species'
+						]
+					},
+					{
+						'name': 'testing_data',
+						'outputs': [
+							'measurements',
+							'species'
+						]
+					}
+				],
+				'description': '',
+				'representation': {
+					'blocks': {
+						'analyzer': {
+							'col': 59,
+							'height': 3,
+							'row': 6,
+							'width': 10
+						},
+						'pre_testing': {
+							'col': 19,
+							'height': 3,
+							'row': 5,
+							'width': 10
+						},
+						'pre_training': {
+							'col': 19,
+							'height': 3,
+							'row': 0,
+							'width': 10
+						},
+						'testing_alg': {
+							'col': 46,
+							'height': 3,
+							'row': 5,
+							'width': 10
+						},
+						'testing_data': {
+							'col': 6,
+							'height': 3,
+							'row': 6,
+							'width': 10
+						},
+						'training_alg': {
+							'col': 32,
+							'height': 3,
+							'row': 1,
+							'width': 10
+						},
+						'training_data': {
+							'col': 6,
+							'height': 3,
+							'row': 1,
+							'width': 10
+						}
+					},
+					'channel_colors': {
+						'testing_data': ch2,
+						'training_data': ch1
+					},
+					'connections': {
+						'pre_testing.measurements/testing_alg.measurements': [],
+						'pre_training.measurements/training_alg.measurements': [],
+						'testing_alg.scores/analyzer.scores': [],
+						'testing_data.measurements/pre_testing.measurements': [],
+						'testing_data.species/analyzer.species': [],
+						'training_alg.lda_machine/testing_alg.lda_machine': [],
+						'training_data.measurements/pre_training.measurements': [],
+						'training_data.species/training_alg.species': []
+					}
+				}
+			});
+		});
 	});
 });
diff --git a/conda/js/src/store/index.js b/conda/js/src/store/index.js
index 9ddcb5fdae43b736ef36660ee8eeb82667cfd071..78296f7110aac7a5800ee13c57322fb8293b412e 100644
--- a/conda/js/src/store/index.js
+++ b/conda/js/src/store/index.js
@@ -1,21 +1,8 @@
 // @flow
-// builds the store and fetches the objects
-import { createStore, applyMiddleware, compose } from 'redux';
-import reducer from './reducers';
-import thunk from 'redux-thunk';
+import createStore from './store';
 import { fetchAllObjects } from './actions.js';
 
-const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
-
-const preloadedState = {};
-const store = createStore(
-	reducer,
-	composeEnhancers(
-		applyMiddleware(
-			thunk,
-		),
-	),
-);
+const store = createStore();
 
 export default store;
 
diff --git a/conda/js/src/store/store.js b/conda/js/src/store/store.js
new file mode 100644
index 0000000000000000000000000000000000000000..3c4fc9bde082f91b37b2cb7c378c9e2114032dbb
--- /dev/null
+++ b/conda/js/src/store/store.js
@@ -0,0 +1,32 @@
+// @flow
+// builds the store and fetches the objects
+import { createStore, applyMiddleware, compose } from 'redux';
+import reducer from './reducers';
+import type { State } from './reducers';
+import thunk from 'redux-thunk';
+
+const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
+
+const createStoreFunc = (preloadedState?: State) => {
+	return preloadedState ?
+		createStore(
+			reducer,
+			preloadedState,
+			composeEnhancers(
+				applyMiddleware(
+					thunk,
+				),
+			),
+		)
+		:
+		createStore(
+			reducer,
+			composeEnhancers(
+				applyMiddleware(
+					thunk,
+				),
+			),
+		);
+};
+
+export default createStoreFunc;
diff --git a/conda/js/test/selenium_test.js b/conda/js/test/selenium_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..31a49d7b3e404f6d7812bc72e843ca4e6dd60a9b
--- /dev/null
+++ b/conda/js/test/selenium_test.js
@@ -0,0 +1,17 @@
+const {Builder, By, Key, until} = require('selenium-webdriver');
+const firefox = require('selenium-webdriver/firefox');
+
+const options = new firefox.Options();
+options.addArguments('-headless');
+
+(async function example() {
+	let driver = await new Builder().forBrowser('firefox').setFirefoxOptions(options).build();
+	try {
+		await driver.get('http://www.google.com/ncr');
+		await driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN);
+		await driver.wait(until.titleIs('webdriver - Google Search'), 10000);
+		console.log('it worked!');
+	} finally {
+		await driver.quit();
+	}
+})();
diff --git a/conda/js/test/selenium_tutorial_test.js b/conda/js/test/selenium_tutorial_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..df6170deda63248d967412cb5c10c9a0a659bc0a
--- /dev/null
+++ b/conda/js/test/selenium_tutorial_test.js
@@ -0,0 +1,380 @@
+// must have the rest server running with at least the contents of the tutorial prefix
+const {Builder, By, Key, until} = require('selenium-webdriver');
+const firefox = require('selenium-webdriver/firefox');
+
+// window size we're testing with
+const winX = 1920, winY = 1080;
+const options = new firefox.Options();
+// sometimes, selenium will hang forever in the middle of the test if the test is ran headlessly
+// if you encounter this issue, comment out this headless flag
+// (it doesn't seem to ever hang when running non-headlessly)
+// the test shouldnt take more than a minute-ish either way
+options.addArguments('-headless');
+options.addArguments(`--window-size=${ winX },${ winY }`);
+
+(async function example() {
+	let driver = await new Builder().forBrowser('firefox').setFirefoxOptions(options).build();
+	await driver.manage().window().setRect({x: 0, y: 0, width: winX, height: winY});
+	try {
+		await driver.get('http://localhost:9101/');
+
+		/**************************************************************
+		 * MEANS EXP
+		 */
+
+		// navigate to exp list
+		await driver.findElement(By.linkText('experiments')).click();
+		//.sendKeys('webdriver', Key.RETURN);
+		await driver.wait(until.elementLocated(By.css('input[placeholder="Search experiments..."]'), 2000));
+
+		// click the clone button next to test/test/iris/1/iris
+		let eLink = await driver.findElement(By.linkText('test/test/iris/1/iris'));
+		await eLink.findElement(By.xpath('../..')).findElement(By.css('button.btn-outline-success')).click();
+		await driver.wait(until.elementLocated(By.css('.modal')), 2000);
+
+		// make new exp
+		let eUser = await driver.findElement(By.css('input[placeholder="Experiment user..."]'));
+		await eUser.clear();
+		await eUser.sendKeys('selenium');
+		let eName = driver.findElement(By.css('input[placeholder="Experiment name..."]'));
+		await eName.clear();
+		await eName.sendKeys('means');
+		await driver.sleep(1000);
+		await driver.findElement(By.css('.modal button.btn-primary')).click();
+		await driver.wait(until.elementLocated(By.linkText('selenium/test/iris/1/means')), 2000);
+		await driver.sleep(1000);
+
+		// go to new exp
+		await driver.findElement(By.linkText('selenium/test/iris/1/means')).click();
+		await driver.wait(until.elementLocated(By.css('div.experimentEditor')), 2000);
+
+		// click on training_alg block
+		await driver.findElement(By.css('rect#block_training_alg')).click();
+		await driver.wait(until.elementLocated(By.css('div.block_training_alg')), 2000);
+
+		// set the algorithm to the new one
+		await driver.findElement(By.css('select.custom-select')).click();
+		await driver.findElement(By.css('select.custom-select option[value="test/means_training/1"]')).click();
+
+		// do same for testing_alg block
+		await driver.findElement(By.css('rect#block_testing_alg')).click();
+		await driver.wait(until.elementLocated(By.css('div.block_testing_alg')), 2000);
+		await driver.findElement(By.css('select.custom-select')).click();
+		await driver.findElement(By.css('select.custom-select option[value="test/means_testing/1"]')).click();
+
+		// make sure the exp is still valid
+		await driver.wait(until.elementLocated(By.css('.badge-success')), 2000);
+
+		// save exp
+		await driver.findElement(By.css('div.experimentEditor > div.d-flex > button.btn-outline-secondary')).click();
+		await driver.sleep(1000);
+
+		/**************************************************************
+		 * CLEANUP
+		 */
+
+		// go back and delete exp
+		await driver.findElement(By.linkText('experiments')).click();
+		await driver.wait(until.elementLocated(By.css('input[placeholder="Search experiments..."]'), 2000));
+		eLink = await driver.findElement(By.linkText('selenium/test/iris/1/means'));
+		await eLink.findElement(By.xpath('../..')).findElement(By.css('button.btn-outline-danger')).click();
+		await driver.sleep(1000);
+		await driver.findElement(By.css('.modal button.btn-danger')).click();
+		await driver.sleep(1000);
+
+		/**************************************************************
+		 * ADVANCED IRIS TOOLCHAIN
+		 */
+
+		// navigate to tc list
+		await driver.findElement(By.linkText('toolchains')).click();
+		await driver.wait(until.elementLocated(By.css('input[placeholder="Search toolchains..."]'), 2000));
+
+		// click the clone button next to test/iris/1
+		let tLink = await driver.findElement(By.linkText('test/iris/1'));
+		await tLink.findElement(By.xpath('../..')).findElement(By.css('button.btn-outline-success + button.btn-outline-success')).click();
+		await driver.wait(until.elementLocated(By.css('.modal')), 2000);
+
+		// make new tc
+		const tUser = await driver.findElement(By.css('input[placeholder="Toolchain user..."]'));
+		await tUser.clear();
+		await tUser.sendKeys('selenium');
+		const tName = driver.findElement(By.css('input[placeholder="New toolchain name..."]'));
+		await tName.clear();
+		await tName.sendKeys('iris_advanced');
+		await driver.sleep(1000);
+		await driver.findElement(By.css('.modal button.btn-primary')).click();
+		await driver.wait(until.elementLocated(By.linkText('selenium/iris_advanced/1')), 2000);
+		await driver.sleep(1000);
+
+		// go to new tc
+		await driver.findElement(By.linkText('selenium/iris_advanced/1')).click();
+		await driver.wait(until.elementLocated(By.css('div.toolchainEditor')), 2000);
+
+		// delete conn between training_data.measurements & training_alg.measurements
+		let rcEl = await driver.findElement(By.css('#\\#training_data-measurements-training_alg-measurements'));
+		await driver.actions().contextClick(rcEl).perform();
+		await driver.sleep(1000);
+		await driver.findElement(By.css('nav.react-contextmenu--visible > div')).click();
+
+		// delete conn between testing_data.measurements & testing_alg.measurements
+		rcEl = await driver.findElement(By.css('#\\#testing_data-measurements-testing_alg-measurements'));
+		await driver.actions().contextClick(rcEl).perform();
+		await driver.sleep(1000);
+		await driver.findElement(By.css('nav.react-contextmenu--visible > div')).click();
+
+		// add training preprocessor block way below the rest of the toolchain
+		let originEl = await driver.findElement(By.css('#block_testing_data'));
+		await driver.actions().move({x: 100, y: 100, origin: originEl}).pause(500).contextClick().perform();
+		await driver.sleep(1000);
+		await driver.findElement(By.css('nav.react-contextmenu--visible > div:first-child')).click();
+
+		// edit training preprocessor
+		await driver.findElement(By.css('#block_block')).click();
+		await driver.sleep(1000);
+		// name
+		let tbName = await driver.findElement(By.css('#tcModalInitFocus'));
+		tbName.clear();
+		tbName.sendKeys('pre_training');
+		// input
+		await driver.findElement(By.css('.modal-body .btn-secondary')).click();
+		let tbiName = await driver.findElement(By.css('.modal-body > form > div.row input'));
+		await tbiName.clear();
+		await tbiName.sendKeys('measurements');
+		// output
+		await driver.findElement(By.css('.modal-body .col-sm-6 + .col-sm-6 .btn-secondary')).click();
+		let tboName = await driver.findElement(By.css('.modal-body > form > div.row .col-sm-6 + .col-sm-6 input'));
+		await tboName.clear();
+		await tboName.sendKeys('measurements');
+		await driver.sleep(1000);
+		//close
+		await driver.findElement(By.css('.modal button.close')).click();
+		await driver.sleep(1000);
+
+		// copy the block
+		rcEl = await driver.findElement(By.css('#block_pre_training'));
+		await driver.actions().contextClick(rcEl).perform();
+		await driver.sleep(1000);
+		await driver.findElement(By.css('nav.react-contextmenu--visible > div + div')).click();
+
+		// paste testing preprocessor block way below the rest of the toolchain
+		originEl = await driver.findElement(By.css('#block_pre_training'));
+		await driver.actions().move({x: 150, origin: originEl}).pause(500).contextClick().perform();
+		await driver.sleep(1000);
+		await driver.findElement(By.css('nav.react-contextmenu--visible > div:first-child')).click();
+
+		// unselect pasted block
+		await driver.actions().move({y: 200, origin: originEl}).pause(500).click().pause(500).perform();
+
+		// edit the block name
+		await driver.findElement(By.css('#block_pre_training0')).click();
+		await driver.sleep(1000);
+		tbName = await driver.findElement(By.css('#tcModalInitFocus'));
+		await tbName.clear();
+		await tbName.sendKeys('pre_testing');
+		await driver.sleep(1000);
+		//close
+		await driver.findElement(By.css('.modal button.close')).click();
+		await driver.sleep(1000);
+
+		// connect pre_training to rest of tc
+		// data to pre
+		let fromBox = await driver.findElement(By.css('rect#training_data-output-measurements'));
+		let toBox = await driver.findElement(By.css('rect#pre_training-input-measurements'));
+		await driver.actions().dragAndDrop(fromBox, toBox).pause(200).perform();
+		// pre to alg
+		fromBox = await driver.findElement(By.css('rect#pre_training-output-measurements'));
+		toBox = await driver.findElement(By.css('rect#training_alg-input-measurements'));
+		await driver.actions().dragAndDrop(fromBox, toBox).pause(200).perform();
+
+		// connect pre_testing to rest of tc
+		// data to pre
+		fromBox = await driver.findElement(By.css('rect#testing_data-output-measurements'));
+		toBox = await driver.findElement(By.css('rect#pre_testing-input-measurements'));
+		await driver.actions().dragAndDrop(fromBox, toBox).pause(500).perform();
+		// pre to alg
+		fromBox = await driver.findElement(By.css('rect#pre_testing-output-measurements'));
+		toBox = await driver.findElement(By.css('rect#testing_alg-input-measurements'));
+		await driver.actions().dragAndDrop(fromBox, toBox).pause(500).perform();
+
+		// fix the channel on pre_training
+		await driver.findElement(By.css('#block_pre_training')).click();
+		await driver.sleep(1000);
+		await driver.findElement(By.css('.modal-body select.form-control')).click();
+		await driver.findElement(By.css('.modal-body option[value="training_data"]')).click();
+		await driver.sleep(500);
+		await driver.findElement(By.css('.modal button.close')).click();
+		await driver.sleep(1000);
+
+		// layout the tc
+		await driver.findElement(By.xpath('//button[contains(.,"Layout")]')).click();
+		// needs to communicate w graphviz, give it some time
+		await driver.sleep(5000);
+
+		// make sure the tc is still valid
+		await driver.wait(until.elementLocated(By.css('.badge-success')), 2000);
+
+		// save
+		await driver.findElement(By.css('div.toolchainEditor > div.d-flex > button.btn-outline-secondary')).click();
+		await driver.sleep(1000);
+
+		/**************************************************************
+		 * CLEANUP
+		 */
+
+		/**************************************************************
+		 * ADVANCED IRIS PREPROCESSOR ALGORITHM
+		 */
+
+		// navigate to alg list
+		await driver.findElement(By.linkText('algorithms')).click();
+		await driver.wait(until.elementLocated(By.css('input[placeholder="Search algorithms..."]'), 2000));
+
+		// click the new button
+		await driver.findElement(By.css('.dropdown + div > .btn-outline-success')).click();
+		await driver.wait(until.elementLocated(By.css('.modal')), 2000);
+
+		// make new alg
+		const aUser = await driver.findElement(By.css('input[placeholder="Algorithm user..."]'));
+		await aUser.clear();
+		await aUser.sendKeys('selenium');
+		const aName = driver.findElement(By.css('input[placeholder="New algorithm name..."]'));
+		await aName.clear();
+		await aName.sendKeys('iris_preprocessor');
+		await driver.sleep(1000);
+		await driver.findElement(By.css('.modal button.btn-primary')).click();
+		await driver.wait(until.elementLocated(By.linkText('selenium/iris_preprocessor/1')), 2000);
+		await driver.sleep(1000);
+
+		// go to new alg
+		await driver.findElement(By.linkText('selenium/iris_preprocessor/1')).click();
+		await driver.wait(until.elementLocated(By.css('ul.nav-fill.nav.nav-tabs')), 2000);
+
+		// add an input & output
+		await driver.findElement(By.css('#newInputBtn')).click();
+		await driver.findElement(By.css('#newOutputBtn')).click();
+
+		// change input & output name to "measurements"
+		let aiDiv = await driver.findElement(By.css('#newInputBtn')).findElement(By.xpath('..')).findElement(By.css('div:first-child'));
+		let aoDiv = await driver.findElement(By.css('#newOutputBtn')).findElement(By.xpath('..')).findElement(By.css('div:first-child'));
+		let aiName = await aiDiv.findElement(By.css('input'));
+		await aiName.clear();
+		await aiName.sendKeys('measurements');
+		let aoName = await aoDiv.findElement(By.css('input'));
+		await aoName.clear();
+		await aoName.sendKeys('measurements');
+
+		// select type system/array_1d_floats/1
+		await aiDiv.findElement(By.css('select.custom-select')).click();
+		await aiDiv.findElement(By.css('select.custom-select option[value="system/array_1d_floats/1"]')).click();
+		await aoDiv.findElement(By.css('select.custom-select')).click();
+		await aoDiv.findElement(By.css('select.custom-select option[value="system/array_1d_floats/1"]')).click();
+		await driver.sleep(500);
+
+		// save
+		await driver.findElement(By.css('.tab-pane.active > div > div.d-flex > button.btn-outline-secondary')).click();
+		await driver.sleep(1000);
+
+		/**************************************************************
+		 * ADVANCED IRIS EXPERIMENT
+		 */
+
+		// navigate to exp list
+		await driver.findElement(By.linkText('experiments')).click();
+		//.sendKeys('webdriver', Key.RETURN);
+		await driver.wait(until.elementLocated(By.css('input[placeholder="Search experiments..."]'), 2000));
+
+		// click the new button
+		await driver.findElement(By.css('.dropdown + div > .btn-outline-success')).click();
+		await driver.wait(until.elementLocated(By.css('.modal')), 2000);
+
+		// make new exp
+		eUser = await driver.findElement(By.css('input[placeholder="Experiment user..."]'));
+		await eUser.clear();
+		await eUser.sendKeys('selenium');
+		let eTc = await driver.findElement(By.css('.modal select'));
+		await eTc.click();
+		await eTc.findElement(By.css('select option[value="selenium/iris_advanced/1"]')).click();
+		eName = driver.findElement(By.css('input[placeholder="Experiment name..."]'));
+		await eName.clear();
+		await eName.sendKeys('iris');
+		await driver.sleep(1000);
+		await driver.findElement(By.css('.modal button.btn-primary')).click();
+		await driver.wait(until.elementLocated(By.linkText('selenium/selenium/iris_advanced/1/iris')), 2000);
+		await driver.sleep(1000);
+
+		// go to new exp
+		await driver.findElement(By.linkText('selenium/selenium/iris_advanced/1/iris')).click();
+		await driver.wait(until.elementLocated(By.css('div.experimentEditor')), 2000);
+
+		// assign dataset protocol
+		await driver.findElement(By.css('.datasets select')).click();
+		await driver.findElement(By.css(`.datasets select option[value='{"testing_data":{"database":"iris/1","protocol":"Main","set":"training"},"training_data":{"database":"iris/1","protocol":"Main","set":"testing"}}']`)).click();
+
+		// pre_training block
+		await driver.findElement(By.css('rect#block_pre_training')).click();
+		await driver.wait(until.elementLocated(By.css('div.block_pre_training')), 2000);
+		await driver.findElement(By.css('select.custom-select')).click();
+		await driver.findElement(By.css('select.custom-select option[value="selenium/iris_preprocessor/1"]')).click();
+
+		// pre_testing block
+		await driver.findElement(By.css('rect#block_pre_testing')).click();
+		await driver.wait(until.elementLocated(By.css('div.block_pre_testing')), 2000);
+		await driver.findElement(By.css('select.custom-select')).click();
+		await driver.findElement(By.css('select.custom-select option[value="selenium/iris_preprocessor/1"]')).click();
+
+		// training_alg block
+		await driver.findElement(By.css('rect#block_training_alg')).click();
+		await driver.wait(until.elementLocated(By.css('div.block_training_alg')), 2000);
+		await driver.findElement(By.css('select.custom-select')).click();
+		await driver.findElement(By.css('select.custom-select option[value="test/iris_training/1"]')).click();
+
+		// testing_alg block
+		await driver.findElement(By.css('rect#block_testing_alg')).click();
+		await driver.wait(until.elementLocated(By.css('div.block_testing_alg')), 2000);
+		await driver.findElement(By.css('select.custom-select')).click();
+		await driver.findElement(By.css('select.custom-select option[value="test/iris_testing/1"]')).click();
+
+		// analyzer block
+		await driver.findElement(By.css('rect#block_analyzer')).click();
+		await driver.wait(until.elementLocated(By.css('div.block_analyzer')), 2000);
+		await driver.findElement(By.css('select.custom-select')).click();
+		await driver.findElement(By.css('select.custom-select option[value="test/iris_analyzer/1"]')).click();
+
+		// make sure the exp is still valid
+		await driver.wait(until.elementLocated(By.css('.badge-success')), 2000);
+
+		// save exp
+		await driver.findElement(By.css('div.experimentEditor > div.d-flex > button.btn-outline-secondary')).click();
+		await driver.sleep(1000);
+
+		/**************************************************************
+		 * CLEANUP
+		 */
+
+		// go back and delete toolchain (which also deletes the associated experiment)
+		await driver.findElement(By.linkText('toolchains')).click();
+		await driver.wait(until.elementLocated(By.css('input[placeholder="Search toolchains..."]'), 2000));
+		tLink = await driver.findElement(By.linkText('selenium/iris_advanced/1'));
+		await tLink.findElement(By.xpath('../..')).findElement(By.css('button.btn-outline-danger')).click();
+		await driver.sleep(1000);
+		await driver.findElement(By.css('.modal button.btn-danger')).click();
+		await driver.sleep(1000);
+
+		// go back and delete algorithm
+		await driver.findElement(By.linkText('algorithms')).click();
+		await driver.wait(until.elementLocated(By.css('input[placeholder="Search algorithms..."]'), 2000));
+		tLink = await driver.findElement(By.linkText('selenium/iris_preprocessor/1'));
+		await tLink.findElement(By.xpath('../..')).findElement(By.css('button.btn-outline-danger')).click();
+		await driver.sleep(1000);
+		await driver.findElement(By.css('.modal button.btn-danger')).click();
+		await driver.sleep(1000);
+
+		console.log('selenium tests finished successfully!');
+	} catch(e) {
+		console.log('selenium tests failed!');
+		console.error(e);
+	} finally {
+		await driver.quit();
+	}
+})();
diff --git a/doc/development.rst b/doc/development.rst
index 4b0c4f79c46e595d702c013a99a6df9fae48fbd5..e9c77b481d69ed0039b0c1dfb96fbe85afad610e 100644
--- a/doc/development.rst
+++ b/doc/development.rst
@@ -119,3 +119,24 @@ How does the webapp operate on the local BEAT prefix? Through the small Python R
 * ``layout/``: Only accepts POST requests - given the toolchain as the request body, generates a layout for the toolchain using Graphviz's ``dot`` layout algorithm and returns it.
 
 .. automodule:: beat.editor
+
+E2E Testing
+-----------
+
+There are selenium tests found in ``conda/js/test/``. These tests are set up to be ran in Firefox in headless mode with the REST server running locally. To run these tests:
+
+* A relatively recent version of Firefox with headless support
+* You need the contents of the tutorial's prefix in your local BEAT prefix (find it at ``https://gitlab.idiap.ch/beat/beat.tutorial.prefix``)
+* A recent version of `the Geckodriver executable <https://github.com/mozilla/geckodriver/releases/>`_ available in your path for Selenium to use
+* The ``beat.editor`` REST server running locally
+
+Just do ``node conda/js/test/<selenium test>`` to run the test. Please see inside the tests for additional notes.
+
+The tests should always be cleaning up test artifacts in your prefix after the test finishes. If tests do not finish successfully, some of these artifacts may still be present in your prefix and will cause future runs of that test to fail. So, if a test doesn't finish successfully, you will have to delete the test artifacts manually. To make it easier, all BEAT objects created by these tests have the username "selenium" so you know what to delete.
+
+Developing E2E Tests
+********************
+
+The webdriver & all its functionality is `well documented <http://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/>`_.
+
+It's recommended to debug tests by not using headless mode and inserting plenty of long pauses via ``driver.sleep()``. You'll need to know the modern ``async``/``await`` pattern as well as be comfortable with CSS selector syntax. See the ``selenium_tutorial_test.js`` test file for working examples of these concepts and how to use selenium's API.