diff --git a/.gitignore b/.gitignore
index c8524f2c536f64beb9bfa068978ba3b686ae4ac9..2c0b2c243db6e92d9d7ae5e4bc359c48e0ed2940 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1 @@
-lib/*
-locales/*
-swiftshader/*
-credits.html
-icudtl.dat
-natives_blob.bin
-nw
-nw_100_percent.pak
-nw_200_percent.pak
-resources.pak
-snapshot_blob.bin
-
+nwjs/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a41323f73b4a20ff5549050f2a2f1281bf070335
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,17 @@
+sudo: required
+dist: trusty
+language: generic
+
+before_install:
+  - git clone https://github.com/FlexBE/flexbe_ci.git ~/flexbe_ci
+  - source ~/flexbe_ci/setup.bash
+  - ~/flexbe_ci/ci_scripts/before_install.bash
+  
+install:
+  - ~/flexbe_ci/ci_scripts/install.bash
+  
+before_script:
+  - ~/flexbe_ci/ci_scripts/before_script.bash
+  
+script:
+  - ~/flexbe_ci/ci_scripts/script.bash
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7b44927dab32fa349c645d50c8a8a00b6022f644..575d37dd8759db726d156d30ba6de27d8a34a426 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -21,23 +21,50 @@ catkin_package(
 # use add_library() or add_executable() as required
 #add_library(${PROJECT_NAME} ${${PROJECT_NAME}_SRCS})
 
-#FILE(GLOB BIN_FILES "bin/*")
-
-# install executables
-#install(PROGRAMS ${BIN_FILES} DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION})
-#install(DIRECTORY launch DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION})
-
 #add_custom_target(nwjs_inst)
 #add_custom_command(TARGET nwjs_inst POST_BUILD COMMAND bin/nwjs_install)
 
-add_custom_command(OUTPUT nw
+add_custom_command(OUTPUT nwjs
   COMMAND bin/nwjs_install
   WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
   VERBATIM
 )
 
-add_custom_target(nw_install DEPENDS nw)
+add_custom_target(nw_install DEPENDS nwjs)
 
-if(NOT ${CMAKE_CURRENT_SOURCE_DIR}/nw)
+if(NOT ${CMAKE_CURRENT_SOURCE_DIR}/nwjs)
   safe_execute_process(COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/bin/nwjs_install)
 endif()
+
+#############
+## Install ##
+#############
+
+INSTALL(PROGRAMS
+    bin/run_app
+    bin/shortcut
+    DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
+)
+INSTALL(FILES
+    package.json
+    DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
+)
+INSTALL(DIRECTORY
+    src
+    DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
+)
+INSTALL(DIRECTORY
+    nwjs
+    DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
+    USE_SOURCE_PERMISSIONS
+)
+install(DIRECTORY
+    launch
+    DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
+)
+
+# run tests
+if(CATKIN_ENABLE_TESTING)
+	find_package(rostest REQUIRED)
+	add_rostest(launch/test_report.test)
+endif()
diff --git a/README.md b/README.md
index 66e2e7a13fa908809980ff99abb4cb670df75729..6bf020863abcdd56625b533107f5cf618356c90d 100644
--- a/README.md
+++ b/README.md
@@ -2,16 +2,6 @@
 
 User interface (editor + runtime control) for the FlexBE behavior engine.
 
-The FlexBE App in this repository replaces the previous *flexbe_chrome_app*. Please refer to the following announcement for an overview of the most important changes: [Future of the FlexBE Chrome App](https://github.com/pschillinger/flexbe_chrome_app/issues/11)
-
----
-
-FlexBE App branch: **feature/flexbe_app**
-
-*Changes to FlexBE required for running this version of the FlexBE App are not yet merged into master. Please checkout the above branch on all repos if available.*
-
----
-
 ## Installation
 
 Clone the following repos into your ROS workspace:
@@ -19,19 +9,12 @@ Clone the following repos into your ROS workspace:
     git clone https://github.com/team-vigir/flexbe_behavior_engine.git  # if not already present
     git clone https://github.com/FlexBE/flexbe_app.git
 
-As mentioned above, switch to the correct branch:
-
-    cd flexbe_behavior_engine
-    git fetch
-    git checkout feature/flexbe_app
-    cd ..
-
 Build you workspace:
 
-    cd ..
     catkin_make # or catkin build
 
 During the build process, the required nwjs binaries are automatically downloaded and extracted.
+To download the binaries manually instead, run the script `bin/nwjs_install`.
 
 ## Workspace
 
@@ -41,8 +24,6 @@ In order to create and prepare a new repository for behavior development, run th
 
 This will initialize a new local git repository with the correct workspace structure which you can then push to a desired remote location. Make sure that you build the workspace afterwards.
 
-If you have been using FlexBE already, you can convert the content of your repository according to the structure defined below. Besides adding the export statement to your state packages, you can automate this conversion by running the FlexBE App. If no behavior package is detected, it will suggest you to initialize one.
-
 ## Usage
 
 If desired, run the following command to create a shortcut in the application menu:
@@ -65,7 +46,24 @@ Use the following launch file to run both of the above for local behavior execut
 
     roslaunch flexbe_app flexbe_full.launch
 
-## Packages
+
+## Backwards Compatibility
+
+The FlexBE App in this repository replaces the previous *flexbe_chrome_app*. Please refer to the following announcement for an overview of the most important changes: [Future of the FlexBE Chrome App](https://github.com/pschillinger/flexbe_chrome_app/issues/11)
+
+If you have been using FlexBE already with the old Chrome app, you can convert the content of your repository according to the structure defined below. Besides adding the export statement to your state packages, you can automate this conversion by running the FlexBE App. If no behavior package is detected, it will suggest you to initialize one.
+
+---
+
+Deprecated Chrome App branch: **deprecated/chrome_app**
+
+*Please checkout the above branch on all repos if available for a best-effort support of the deprecated Chrome app. However, please consider to update as soon as possible according to the instructions below to ensure that the system will remain working in the future and to receive all updates.*
+
+---
+
+Please note that the way how state and behavior packages are detected has changed and breaks direct compatibility.
+Follow the instructions below to make the required changes.
+Behavior packages can also be converted automatically by the new FlexBE App.
 
 ### State packages
 
diff --git a/bin/nwjs_install b/bin/nwjs_install
index f72c17fa7c9180e5881d954509c43543c99d3f07..9dfc7f4599e941d2310004ae669ef920f9778665 100755
--- a/bin/nwjs_install
+++ b/bin/nwjs_install
@@ -1,15 +1,24 @@
 #!/bin/bash -e
 cd "$( dirname "${BASH_SOURCE[0]}" )"
 TEMPFILE="nwjs.tar.gz"
-cd ..
+mkdir -p ../nwjs
+cd ../nwjs
+
+VERSION="v0.34.1" # verified working, to be incremented explicitly
 
-# check if nw is already existing and abort if so
+# check if nw is already existing and up-to-date, abort if so
 if [ -f nw ]; then
-  exit 0
+	if [ -f version ] && [ "$(cat version)" == "$VERSION" ]; then
+		exit 0
+	else
+		cd ..
+		mv nwjs nwjs_old
+		mkdir nwjs
+		cd nwjs
+	fi
 fi
 
 # determine correct executable version
-VERSION="v0.27.2" # verified working, to be incremented explicitly
 OS="linux"
 if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then
 	OS="win"
@@ -32,5 +41,11 @@ wget -q -O $TEMPFILE https://dl.nwjs.io/$VERSION/nwjs-$VERSION-$OS-$ARCH.tar.gz
 echo "Unpacking..."
 tar -xzf $TEMPFILE --strip-components 1
 rm $TEMPFILE
+echo "$VERSION" > version
+
+cd ..
+if [ -f nwjs_old/nw ]; then
+	rm -rf nwjs_old
+fi
 
 echo -e "Successfully downloaded nwjs!"
diff --git a/bin/run_app b/bin/run_app
index 32a25a59537895d5d1b327b924b41ea96b2bf23c..3164ef1972000e41f154e8e0178687070d5513a4 100755
--- a/bin/run_app
+++ b/bin/run_app
@@ -1,11 +1,16 @@
 #!/bin/bash
-cd "$( dirname "${BASH_SOURCE[0]}" )"
 
-if [ ! -f ../nw ]; then
+ROOT_PATH="$(rospack find flexbe_app)"
+NW=$ROOT_PATH/nwjs/nw
+if [ ! -f $NW ]; then
+    ROOT_PATH=.
+    NW=$ROOT_PATH/nwjs/nw
+fi
+if [ ! -x $NW ]; then
   echo "Cannot run flexbe_app, need to download nwjs first."
   echo "Please build flexbe_app via catkin before using it or run the following command now:"
   echo "  rosrun flexbe_app nwjs_install"
   exit -1
 fi
 
-../nw --password-store=basic $*
+"$NW" --password-store=basic $ROOT_PATH "$@"
diff --git a/bin/shortcut b/bin/shortcut
index c034e0416b31b55a24b91776e0702e5fd4a262d5..e4cf77c6579ea70fc0c8371f5d1518e9ce461340 100755
--- a/bin/shortcut
+++ b/bin/shortcut
@@ -1,10 +1,10 @@
 #!/bin/bash
-if [ $1 == "create" ] ; then
-	cd "$( dirname "${BASH_SOURCE[0]}" )"/..
+if [ "$1" == "create" ] ; then
+	cd $(rospack find flexbe_app)
 	cp flexbe.desktop ~/.local/share/applications
 	cp src/img/icon-128.png ~/.local/share/icons/flexbe_app.png
 	echo "Launcher shortcut created"
-elif [ $1 == "remove" ] ; then
+elif [ "$1" == "remove" ] ; then
 	rm ~/.local/share/applications/flexbe.desktop
 	rm ~/.local/share/icons/flexbe_app.png
 	echo "Launch shortcut removed"
diff --git a/bin/test_report b/bin/test_report
new file mode 100755
index 0000000000000000000000000000000000000000..732468f14fe86103cabae4948f1aafdc17318264
--- /dev/null
+++ b/bin/test_report
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+import yaml
+import unittest
+
+class TestReport(unittest.TestCase):
+
+	def test_report(self):
+		try:
+			with open("/tmp/flexbe_app_report.log", 'r') as f:
+				report = yaml.load(f)
+		except IOError:
+			return # skip test since there is no report to evaluate
+		for test_type, tests in report.items():
+			for test_name, test_data in tests.items():
+				if test_type == "assertTrue":
+					self.assertTrue(test_data, test_name)
+
+if __name__ == '__main__':
+    import rosunit
+    rosunit.unitrun("flexbe_app", "test_report", TestReport)
diff --git a/launch/test_report.test b/launch/test_report.test
new file mode 100644
index 0000000000000000000000000000000000000000..9c7189d2e3574ef8848cf0aec6aaf8d97e232182
--- /dev/null
+++ b/launch/test_report.test
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+
+<launch>
+
+<test test-name="flexbe_app" name="flexbe_app" pkg="flexbe_app" type="test_report" />
+
+</launch>
diff --git a/package.json b/package.json
index 2d496a37e083c84475187e00fe1b379ec35dccdb..a01758918f4372eb4da705726a6e1644cf27b058 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "FlexBE App",
-  "version": "2.0.6",
+  "version": "2.0.10",
   "main": "src/main.js",
   "window": {
     "icon": "src/img/icon-128.png",
diff --git a/package.xml b/package.xml
index ce310e3f450e1b1c491cc8446d00f7a752a25db9..2eb2c993b3969acaed694753404def4d66ff3332 100644
--- a/package.xml
+++ b/package.xml
@@ -1,10 +1,10 @@
 <package>
   <name>flexbe_app</name>
-  <version>2.0.6</version>
+  <version>2.0.10</version>
   <description>
      flexbe_app provides a user interface (editor + runtime control) for the FlexBE behavior engine.
   </description>
-  <maintainer email="schillinger@sim.tu-darmstadt.de">Philipp Schillinger</maintainer>
+  <maintainer email="schillin@kth.se">Philipp Schillinger</maintainer>
   <license>BSD</license>
 
   <url>http://ros.org/wiki/flexbe_app</url>
@@ -15,5 +15,8 @@
   <run_depend>rospy</run_depend>
   <run_depend>genpy</run_depend>
   <run_depend>actionlib</run_depend>
+  <run_depend>rospack</run_depend>
+
+  <test_depend>rosunit</test_depend>
 
 </package>
diff --git a/src/_model/behavior.js b/src/_model/behavior.js
index bf1c4e6e01f783cf3a14580d0fdb94c9a1eed0c9..df71c7fc9ae6edef9e4d266b86109b881e59ad25 100644
--- a/src/_model/behavior.js
+++ b/src/_model/behavior.js
@@ -31,6 +31,8 @@ Behavior = new (function() {
 	var file_name = undefined;
 	var manifest_path = undefined;
 
+	var readonly = false;
+
 
 	this.getBehaviorName = function() {
 		return behavior_name;
@@ -274,6 +276,14 @@ Behavior = new (function() {
 		root_sm = _root_sm;
 	}
 
+	this.setReadonly = function(_readonly) {
+		readonly = _readonly;
+	}
+
+	this.isReadonly = function() {
+		return readonly;
+	}
+
 	this.resetBehavior = function() {
 		behavior_name = "";
 		behavior_package = "";
@@ -299,6 +309,7 @@ Behavior = new (function() {
 		comment_notes = [];
 
 		root_sm = new Statemachine("", new WS.StateMachineDefinition([], [], []));
+		readonly = false;
 	}
 
 	this.setFiles = function(_file_name, _manifest_path) {
diff --git a/src/_model/state.js b/src/_model/state.js
index 75a612e17bce7f8095b61aa870c47b341b9e13a8..560a521462bb51555dca70bde1413a8f393b5686 100644
--- a/src/_model/state.js
+++ b/src/_model/state.js
@@ -130,6 +130,9 @@ State = function(state_name, state_def) {
 		resolved_parameter_output_values_old = resolved_parameter_output_values;
 	}
 
+	var noMetaFilter = function(element) { return element[0] != '$'; };
+	var metaFilter = function(element) { return element[0] == '$'; };
+
 	var state_name = state_name;
 	var state_class = state_def.getStateClass();
 	var state_import = state_def.getStatePath();
@@ -139,22 +142,22 @@ State = function(state_name, state_def) {
 	var parameters = state_def.getParameters().clone();
 	var parameter_values = state_def.getDefaultParameterValues().clone();
 
-	var outcomes = state_def.getOutcomes().filter(function(element) { return element[0] != '$'; });
+	var outcomes = state_def.getOutcomes().filter(noMetaFilter);
 	var autonomy = state_def.getDefaultAutonomy().clone();
 
-	var meta_outcomes = state_def.getOutcomes().filter(function(element) { return element[0] == '$'; });
+	var meta_outcomes = state_def.getOutcomes().filter(metaFilter);
 	for (var i=0; i<meta_outcomes.length; ++i) meta_outcomes[i] = meta_outcomes[i].slice(1, meta_outcomes[i].length);
 
 	var outcomes_unc = outcomes.clone();
 	var outcomes_con = [];
 
-	var input_keys = state_def.getInputKeys().filter(function(element) { return element[0] != '$'; });
-	var output_keys = state_def.getOutputKeys().filter(function(element) { return element[0] != '$'; });
+	var input_keys = state_def.getInputKeys().filter(noMetaFilter);
+	var output_keys = state_def.getOutputKeys().filter(noMetaFilter);
 
-	var meta_input = state_def.getInputKeys().filter(function(element) { return element[0] == '$'; });
+	var meta_input = state_def.getInputKeys().filter(metaFilter);
 	for (var i=0; i<meta_input.length; ++i) meta_input[i] = meta_input[i].slice(1, meta_input[i].length);
 
-	var meta_output = state_def.getOutputKeys().filter(function(element) { return element[0] == '$'; });
+	var meta_output = state_def.getOutputKeys().filter(metaFilter);
 	for (var i=0; i<meta_output.length; ++i) meta_output[i] = meta_output[i].slice(1, meta_output[i].length);
 
 	var input_mapping = [];
@@ -357,4 +360,132 @@ State = function(state_name, state_def) {
 		return behavior;
 	}
 
+	this.updateStateDefinition = function(new_def) {
+		state_class = new_def.getStateClass();
+		state_import = new_def.getStatePath();
+		state_pkg = new_def.getStatePackage();
+
+		var updateKeys = function(old_keys, new_keys, old_values, new_values) {
+			var result = {keys: [], values: []};
+			for (var i=0; i<new_keys.length; i++) {
+				var j = old_keys.indexOf(new_keys[i]);
+				if (j != -1) {
+					result.keys.push(old_keys[j]);
+					result.values.push(old_values[j]);
+				} else {
+					result.keys.push(new_keys[i]);
+					result.values.push(new_values[i]);
+				}
+			}
+			return result;
+		}
+
+		// required for meta
+		var old_params = parameters;
+
+		if (!parameters.hasSameElements(new_def.getParameters())) {
+			var result = updateKeys(parameters, new_def.getParameters(), parameter_values, new_def.getDefaultParameterValues());
+			parameters = result.keys;
+			parameter_values = result.values;
+		}
+
+		if (!outcomes.hasSameElements(new_def.getOutcomes().filter(noMetaFilter))) {
+			var old_outcomes = outcomes;
+			var result = updateKeys(outcomes, new_def.getOutcomes().filter(noMetaFilter), autonomy, new_def.getDefaultAutonomy());
+			outcomes = result.keys;
+			autonomy = result.values;
+			for (var i=0; i<outcomes.length; i++) {
+				if (!old_outcomes.contains(outcomes[i])) {
+					outcomes_unc.push(outcomes[i]);
+				}
+			}
+			for (var i=0; i<old_outcomes.length; i++) {
+				if (!outcomes.contains(old_outcomes[i])) {
+					if (outcomes_con.contains(old_outcomes[i])) {
+						if (container != undefined) {
+							container.removeTransitionFrom(that, old_outcomes[i]);
+						}
+						outcomes_con.remove(old_outcomes[i]);
+					} else {
+						outcomes_unc.remove(old_outcomes[i]);
+					}
+				}
+			}
+		}
+
+		var new_meta_outcomes = new_def.getOutcomes().filter(metaFilter);
+		for (var i=0; i<new_meta_outcomes.length; ++i) new_meta_outcomes[i] = new_meta_outcomes[i].slice(1, new_meta_outcomes[i].length);
+		if (!meta_outcomes.hasSameElements(new_meta_outcomes)) {
+			for (var i=0; i<meta_outcomes.length; i++) {
+				if (!new_meta_outcomes.contains(meta_outcomes[i])) {
+					var pidx = old_params.indexOf(meta_outcomes[i]);
+					var remove_list = resolved_parameter_outcome_values_old[i];
+					remove_list.forEach(function(element) {
+						var idx = outcomes.indexOf(element);
+						outcomes.splice(idx, 1);
+						autonomy.splice(idx, 1);
+						if (outcomes_unc.contains(element)) {
+							outcomes_unc.remove(element);
+						} else if (outcomes_con.contains(element)) {
+							outcomes_con.remove(element);
+							if (container != undefined) {
+								container.removeTransitionFrom(that, element);
+							}
+						}
+					});
+				}
+			}
+			meta_outcomes = new_meta_outcomes;
+			updateGeneratedOutcomes(parameter_values);
+		}
+
+		if (!input_keys.hasSameElements(new_def.getInputKeys().filter(noMetaFilter))) {
+			var result = updateKeys(input_keys, new_def.getInputKeys().filter(noMetaFilter), input_mapping, new_def.getInputKeys().filter(noMetaFilter));
+			input_keys = result.keys;
+			input_mapping = result.values;
+		}
+
+		var new_meta_input = new_def.getInputKeys().filter(metaFilter);
+		for (var i=0; i<new_meta_input.length; ++i) new_meta_input[i] = new_meta_input[i].slice(1, new_meta_input[i].length);
+		if (!meta_input.hasSameElements(new_meta_input)) {
+			for (var i=0; i<meta_input.length; i++) {
+				if (!new_meta_input.contains(meta_input[i])) {
+					var pidx = old_params.indexOf(meta_input[i]);
+					var remove_list = resolved_parameter_input_values_old[i];
+					remove_list.forEach(function(element) {
+						var idx = input_keys.indexOf(element);
+						input_keys.splice(idx, 1);
+						input_mapping.splice(idx, 1);
+					});
+				}
+			}
+			meta_input = new_meta_input;
+			updateGeneratedInput(parameter_values);
+		}
+
+		if (!output_keys.hasSameElements(new_def.getOutputKeys().filter(noMetaFilter))) {
+			var result = updateKeys(output_keys, new_def.getOutputKeys().filter(noMetaFilter), output_mapping, new_def.getOutputKeys().filter(noMetaFilter));
+			output_keys = result.keys;
+			output_mapping = result.values;
+		}
+
+		var new_meta_output = new_def.getOutputKeys().filter(metaFilter);
+		for (var i=0; i<new_meta_output.length; ++i) new_meta_output[i] = new_meta_output[i].slice(1, new_meta_output[i].length);
+		if (!meta_output.hasSameElements(new_meta_output)) {
+			for (var i=0; i<meta_output.length; i++) {
+				if (!new_meta_output.contains(meta_output[i])) {
+					var pidx = old_params.indexOf(meta_output[i]);
+					var remove_list = resolved_parameter_output_values_old[i];
+					remove_list.forEach(function(element) {
+						var idx = output_keys.indexOf(element);
+						output_keys.splice(idx, 1);
+						output_mapping.splice(idx, 1);
+					});
+				}
+			}
+			meta_output = new_meta_output;
+			updateGeneratedOutput(parameter_values);
+		}
+	}
+
 };
\ No newline at end of file
diff --git a/src/_model/statemachine.js b/src/_model/statemachine.js
index ceaf522275d62fc8c4eb14626c5c44552b4ec5bb..c29591ce6450f799715f9d694a434e4709e3e292 100644
--- a/src/_model/statemachine.js
+++ b/src/_model/statemachine.js
@@ -74,6 +74,21 @@ Statemachine = function(sm_name, sm_definition) {
 		}
 		return child.getStateByPath(path.slice(that.getStateName().length + 1));
 	}
+	this.traverseStates = function(_filter) {
+		var result = [];
+		for(var i=0; i<states.length; ++i) {
+			if (states[i] instanceof Statemachine) {
+				result = result.concat(states[i].traverseStates(_filter));
+				continue;
+			} else if (states[i] instanceof BehaviorState) {
+				continue;
+			}
+			if (_filter(states[i])) {
+				result.push(states[i]);
+			}
+		}
+		return result;
+	}
 
 	this.addState = function(state) {
 		states.push(state);
diff --git a/src/_testing/scripts.js b/src/_test/scripts.js
similarity index 100%
rename from src/_testing/scripts.js
rename to src/_test/scripts.js
diff --git a/src/_test/statelib.test.js b/src/_test/statelib.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..c3d62ec3f73a89b9d0916be1c10da3ae4ce53d49
--- /dev/null
+++ b/src/_test/statelib.test.js
@@ -0,0 +1,11 @@
+StatelibTest = new (function() {
+    var that = this;
+
+    this.testWaitStateAvailable = function(report, done) {
+        // flexbe_states/WaitState is found and properly imported
+        var result = WS.Statelib.getClassList().contains("WaitState");
+        report.assertTrue.test_statelib = result;
+        done();
+    }
+
+})();
\ No newline at end of file
diff --git a/src/_test/testreport.js b/src/_test/testreport.js
new file mode 100644
index 0000000000000000000000000000000000000000..d24286a0f7603743597875b83721d0224c0ddfc5
--- /dev/null
+++ b/src/_test/testreport.js
@@ -0,0 +1,21 @@
+TestReport = new (function() {
+    var that = this;
+
+    var report = {'assertTrue': {}};
+
+    this.runAllTests = function(callback) {
+        var testCases = [
+            StatelibTest.testWaitStateAvailable
+        ];
+        var done = function(code) {
+            if (testCases.length == 0) {
+                IO.Filesystem.createFile('/tmp', "flexbe_app_report.log", JSON.stringify(report), callback);
+            } else {
+                var test = testCases.shift();
+                test(report, done);
+            }
+        }
+        done();
+    }
+
+})();
\ No newline at end of file
diff --git a/src/_testing/test.js b/src/_testing/test.js
deleted file mode 100644
index 7406703154bb62c4bb50bfedc8d8b545801afb47..0000000000000000000000000000000000000000
--- a/src/_testing/test.js
+++ /dev/null
@@ -1,248 +0,0 @@
-console.log("### Test Cases Loaded ###");
-
-test_simpleSM = function() {
-	UI.Menu.toStatemachineClicked();
-
-	state = new State("Log_Anything", Statelib.getFromLib("LogState"));
-	state.getPosition().x += 100;
-	state.getPosition().y += 20;
-	UI.Statemachine.getRoot().addState(state);
-
-	UI.Statemachine.refreshView();
-}
-
-test_simpleCP = function() {
-	UI.Menu.toStatemachineClicked();
-
-	var state1 = new State("Log_Anything", Statelib.getFromLib("LogState"));
-	state1.getPosition().x += 100;
-	state1.getPosition().y += 20;
-	Behavior.getStatemachine().addState(state1);
-
-	var state2 = new State("Bla", Statelib.getFromLib("CalculationState"));
-	state2.getPosition().x += 400;
-	state2.getPosition().y += 30;
-	Behavior.getStatemachine().addState(state2);
-
-	UI.Statemachine.beginTransition(state1, "done");
-	UI.Statemachine.connectTransition(state2);
-}
-
-test_runtimeControlDisplay = function() {
-	UI.Menu.toStatemachineClicked();
-
-	state = new State("Calc_Pose", Statelib.getFromLib("CalculationState"));
-	state.getPosition().x += 100;
-	state.getPosition().y += 40;
-	UI.Statemachine.getRoot().addState(state);
-
-	state = new State("Grasp_Object", Statelib.getFromLib("GraspTemplateState"));
-	state.getPosition().x += 250;
-	state.getPosition().y += 40;
-	UI.Statemachine.getRoot().addState(state);
-
-	state = new State("Log_Success", Statelib.getFromLib("LogState"));
-	state.getPosition().x += 400;
-	state.getPosition().y += 10;
-	UI.Statemachine.getRoot().addState(state);
-
-	state = new State("Log_Failure", Statelib.getFromLib("LogState"));
-	state.getPosition().x += 400;
-	state.getPosition().y += 80;
-	UI.Statemachine.getRoot().addState(state);
-
-	UI.Menu.toStatemachineClicked();
-}
-
-
-test_executeAll = function() {
-
-}
-
-test_normalRun = function() {
-	var manifest = Behaviorlib.getByName("FlexBE Test Behavior").getBehaviorManifest();
-	BehaviorLoader.loadBehavior(manifest);
-	UI.Settings.connectRosbridgeClicked();
-	UI.Menu.toControlClicked();
-	setTimeout(function () {
-		UI.RuntimeControl.startBehaviorClicked();
-	}, 2000);
-}
-
-test_synthesisResult = function() {
-	RC.PubSub.DEBUG_synthesis_action_result_callback({
-		error_code: {
-			value: 1
-		},
-		states: [
-			{
-				state_path: "/",
-				state_class: ":STATEMACHINE",
-				initial_state_name: "Entry_Msg",
-				behavior_class: "",
-				parameter_names: [],
-				parameter_values: [],
-				input_keys:[],
-				output_keys: [],
-				position: [0,0],
-				outcomes: ['finished', 'failed'],
-				transitions: [],
-				autonomy: [],
-				cond_outcome: [],
-				cond_transition: [],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Wait_A_Bit",
-				state_class: "WaitState",
-				initial_state_name: "",
-				behavior_class: "",
-				parameter_names: ['wait_time'],
-				parameter_values: ['2'],
-				position: [0,0],
-				outcomes: ['done'],
-				transitions: ['Con1'],
-				autonomy: [2],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Entry_Msg",
-				state_class: "LogState",
-				initial_state_name: "",
-				behavior_class: "",
-				parameter_names: ['text'],
-				parameter_values: ['"Now at inner behavior..."'],
-				position: [0,0],
-				outcomes: ['done'],
-				transitions: ['Testprint_Behavior'],
-				autonomy: [0],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Con1",
-				state_class: ":STATEMACHINE",
-				initial_state_name: "Con",
-				behavior_class: "",
-				parameter_names: [],
-				parameter_values: [],
-				input_keys:[],
-				output_keys: [],
-				position: [],
-				outcomes: ['finished'],
-				transitions: ['Con2'],
-				autonomy: [],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Con1/Con",
-				state_class: ":STATEMACHINE",
-				initial_state_name: "Test_1",
-				behavior_class: "",
-				parameter_names: [],
-				parameter_values: [],
-				input_keys:[],
-				output_keys: [],
-				position: [0,0],
-				outcomes: ['finished'],
-				transitions: ['finished'],
-				autonomy: [],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Con2",
-				state_class: ":STATEMACHINE",
-				initial_state_name: "Con",
-				behavior_class: "",
-				parameter_names: [],
-				parameter_values: [],
-				input_keys:[],
-				output_keys: [],
-				position: [0,0],
-				outcomes: ['finished'],
-				transitions: ['Con2'],
-				autonomy: [],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Con2/Con",
-				state_class: ":STATEMACHINE",
-				initial_state_name: "Test_2",
-				behavior_class: "",
-				parameter_names: [],
-				parameter_values: [],
-				input_keys:[],
-				output_keys: [],
-				position: [0,0],
-				outcomes: ['finished'],
-				transitions: ['finished'],
-				autonomy: [],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Con1/Con/Test_1",
-				state_class: "WaitState",
-				initial_state_name: "",
-				behavior_class: "",
-				parameter_names: ['wait_time'],
-				parameter_values: ['2'],
-				position: [0,0],
-				outcomes: ['done'],
-				transitions: ['finished'],
-				autonomy: [2],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Con2/Con/Test_2",
-				state_class: "WaitState",
-				initial_state_name: "",
-				behavior_class: "",
-				parameter_names: ['wait_time'],
-				parameter_values: ['2'],
-				position: [0,0],
-				outcomes: ['done'],
-				transitions: ['finished'],
-				autonomy: [2],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Print_State",
-				state_class: "CalculationState",
-				initial_state_name: "",
-				behavior_class: "",
-				parameter_names: ['calculation'],
-				parameter_values: ['lambda x: 0'],
-				position: [0,0],
-				outcomes: ['done'],
-				transitions: ['finished'],
-				autonomy: [0],
-				userdata_keys: ['input_value', 'output_value'],
-				userdata_remapping: ['my_input', 'output_value']
-			},
-			{
-				state_path: "/Testprint_Behavior",
-				state_class: ":BEHAVIOR",
-				initial_state_name: "",
-				behavior_class: "FlexBETestprintBehaviorSM",
-				parameter_names: [],
-				parameter_values: [],
-				position: [0,0],
-				outcomes: ['finished'],
-				transitions: ['Wait_A_Bit'],
-				autonomy: [-1],
-				userdata_keys: [],
-				userdata_remapping: []
-			}
-		]
-	},
-	"/Synthesis_Test");
-	UI.Menu.toStatemachineClicked();
-}
\ No newline at end of file
diff --git a/src/events.js b/src/events.js
index 6ecd91285728483a8d0a3e44dcc530eb2a45463c..30b4116d51c8887c5eae9ac272e3449684badf1d 100644
--- a/src/events.js
+++ b/src/events.js
@@ -172,6 +172,7 @@ document.addEventListener('DOMContentLoaded', function() {
     process.on('exit', (code) => {
         RC.PubSub.shutdown();
         ROS.shutdown();
+        IO.PackageParser.stopWatching();
     });
 
     nw.App.on('open', () => {
diff --git a/src/io/io_behaviorloader.js b/src/io/io_behaviorloader.js
index f5ca06ffc04a4d8e9ebef8407fdad0b60e018183..b1487744272d69b96175e676d1367ad10a35f84d 100644
--- a/src/io/io_behaviorloader.js
+++ b/src/io/io_behaviorloader.js
@@ -34,6 +34,15 @@ IO.BehaviorLoader = new (function() {
 		T.logInfo("Behavior state machine built.");
 		
 		ActivityTracer.resetActivities();
+
+		ROS.getPackagePath(manifest.rosnode_name, (package_path) => {
+			ROS.getPackagePythonPath(manifest.rosnode_name, (python_path) => {
+				if (!python_path.startsWith(package_path)) {
+					Behavior.setReadonly(true);
+				}
+				UI.Statemachine.refreshView();
+			});
+		});
 	}
 
 	var resetEditor = function() {
@@ -52,48 +61,33 @@ IO.BehaviorLoader = new (function() {
 
 		resetEditor();
 
-		var package_name = manifest.rosnode_name;
-		ROS.getPackagePath(package_name, (package_path) => {
-			if (package_path == undefined) {
-				T.logError("Failed to load behavior: ROS package "+package_name+" not found, please check behavior manifest.");
-				return;
-			}
-			var file_path = path.join(package_path, 'src', package_name, manifest.codefile_name);
-			IO.Filesystem.readFile(file_path, (content) => {
-				T.logInfo("Parsing sourcecode...");
-				parseCode(content, manifest);
-			});
+		var file_path = path.join(manifest.codefile_path, manifest.codefile_name);
+		IO.Filesystem.readFile(file_path, (content) => {
+			T.logInfo("Parsing sourcecode...");
+			parseCode(content, manifest);
 		});
 	}
 
 	this.loadBehaviorInterface = function(manifest, callback) {
-		var package_name = manifest.rosnode_name;
-		ROS.getPackagePath(package_name, (package_path) => {
-			if (package_path == undefined) {
-				T.logError("Failed to load behavior: ROS package "+package_name+" not found, please check behavior manifest.");
+		var file_path = path.join(manifest.codefile_path, manifest.codefile_name);
+		IO.Filesystem.readFile(file_path, (content) => {
+			try {
+				var parsingResult = IO.CodeParser.parseSMInterface(content);
+				callback(parsingResult);
+			} catch (err) {
+				T.logError("Failed to parse behavior interface of " + manifest.name + ": " + err);
 				return;
 			}
-			var file_path = path.join(package_path, 'src', package_name, manifest.codefile_name);
-			IO.Filesystem.readFile(file_path, (content) => {
-				try {
-					var parsingResult = IO.CodeParser.parseSMInterface(content);
-					callback(parsingResult);
-				} catch (err) {
-					T.logError("Failed to parse behavior interface of " + manifest.name + ": " + err);
-					return;
-				}
-			});
 		});
 	}
 
 	this.updateManualSections = function(callback) {
 		var names = Behavior.createNames();
 		var package_name = names.rosnode_name;
-		ROS.getPackagePath(package_name, (package_path) => {
-			if (package_path == undefined) {
+		ROS.getPackagePythonPath(package_name, (folder_path) => {
+			if (folder_path == undefined) {
 				return;
 			}
-			var folder_path = path.join(package_path, 'src', package_name);
 			var file_path = path.join(folder_path, names.file_name);
 			IO.Filesystem.checkFileExists(folder_path, names.file_name, (exists) => {
 				if (exists) {
@@ -115,28 +109,21 @@ IO.BehaviorLoader = new (function() {
 	}
 
 	this.parseBehaviorSM = function(manifest, callback) {
-		var package_name = manifest.rosnode_name;
-		ROS.getPackagePath(package_name, (package_path) => {
-			if (package_path == undefined) {
-				T.logError("Failed to load behavior: ROS package "+package_name+" not found, please check behavior manifest.");
+		var file_path = path.join(manifest.codefile_path, manifest.codefile_name);
+		IO.Filesystem.readFile(file_path, (content) => {
+			console.log("Preparing sourcecode of behavior " + manifest.name + "...");
+			try {
+				parsingResult = IO.CodeParser.parseCode(content);
+			} catch (err) {
+				console.log("Code parsing failed: " + err);
 				return;
 			}
-			var file_path = path.join(package_path, 'src', package_name, manifest.codefile_name);
-			IO.Filesystem.readFile(file_path, (content) => {
-				console.log("Preparing sourcecode of behavior " + manifest.name + "...");
-				try {
-					parsingResult = IO.CodeParser.parseCode(content);
-				} catch (err) {
-					console.log("Code parsing failed: " + err);
-					return;
-				}
-				callback({
-					container_name: "",
-					container_sm_var_name: parsingResult.root_sm_name,
-					sm_defs: parsingResult.sm_defs,
-					sm_states: parsingResult.sm_states,
-					default_userdata: parsingResult.default_userdata
-				});
+			callback({
+				container_name: "",
+				container_sm_var_name: parsingResult.root_sm_name,
+				sm_defs: parsingResult.sm_defs,
+				sm_states: parsingResult.sm_states,
+				default_userdata: parsingResult.default_userdata
 			});
 		});
 	}
diff --git a/src/io/io_behaviorpacker.js b/src/io/io_behaviorpacker.js
index a468b4f58ac9818b4de695b2822f948027ed4a52..2a5b58617156473564d726aece3c520a2c66290e 100644
--- a/src/io/io_behaviorpacker.js
+++ b/src/io/io_behaviorpacker.js
@@ -6,11 +6,10 @@ IO.BehaviorPacker = new (function() {
 	this.loadBehaviorCode = function(callback) {
 		var names = Behavior.createNames();
 		var package_name = names.rosnode_name;
-		ROS.getPackagePath(package_name, (package_path) => {
-			if (package_path == undefined) {
+		ROS.getPackagePythonPath(package_name, (folder_path) => {
+			if (folder_path == undefined) {
 				return;
 			}
-			var folder_path = path.join(package_path, 'src', package_name);
 			var file_path = path.join(folder_path, names.file_name);
 			IO.Filesystem.checkFileExists(folder_path, names.file_name, (exists) => {
 				if (exists) {
diff --git a/src/io/io_behaviorsaver.js b/src/io/io_behaviorsaver.js
index 9e1e9bbfbed625ba116188f883426bcd0261c67c..bca6ccf0967a0a7925830de165c26207ed07fbdc 100644
--- a/src/io/io_behaviorsaver.js
+++ b/src/io/io_behaviorsaver.js
@@ -11,8 +11,7 @@ IO.BehaviorSaver = new (function() {
 		};
 		var names = Behavior.createNames();
 		var package_name = names.rosnode_name;
-		ROS.getPackagePath(package_name, (package_path) => {
-			var folder_path = path.join(package_path, 'src', package_name);
+		ROS.getPackagePythonPath(package_name, (folder_path) => {
 			var file_path = path.join(folder_path, names.file_name);
 			var file_tmp_path = path.join(folder_path, names.file_name_tmp);
 			if (RC.Controller.isConnected()) {
@@ -21,7 +20,7 @@ IO.BehaviorSaver = new (function() {
 						IO.Filesystem.checkFileExists(folder_path, names.file_name_tmp, function(src_exists) {
 							if (src_exists) {
 								IO.Filesystem.getFileContent(folder_path, names.file_name, function(content_onboard) {
-									IO.Filesystem.createFile(folder_path, names.file_name_tmp, content_onboard, function() { 
+									IO.Filesystem.createFile(folder_path, names.file_name_tmp, content_onboard, function() {
 										create_callback(folder_path);
 									});
 								});
diff --git a/src/io/io_manifestparser.js b/src/io/io_manifestparser.js
index b40ec638c79a21a0f4e730276938b5d5df893cd3..efff78c6004294711f8b9ea591991c8f3b7b2e9d 100644
--- a/src/io/io_manifestparser.js
+++ b/src/io/io_manifestparser.js
@@ -1,7 +1,7 @@
 IO.ManifestParser = new (function() {
 	var that = this;
 
-	this.parseManifest = function(content, file_path) {
+	this.parseManifest = function(content, file_path, python_path) {
 		parser = new DOMParser();
 		xml = parser.parseFromString(content,"text/xml");
 
@@ -26,6 +26,7 @@ IO.ManifestParser = new (function() {
 		var path = xml.getElementsByTagName("executable")[0].getAttribute("package_path").split(".");
 		var rosnode_name = path[0];
 		var codefile_name = path[1] + ".py";
+		var codefile_path = python_path;
 		var class_name = xml.getElementsByTagName("executable")[0].getAttribute("class");
 
 		var params_element = xml.getElementsByTagName("params");
@@ -74,6 +75,7 @@ IO.ManifestParser = new (function() {
 			date: 			date,
 			rosnode_name: 	rosnode_name,
 			codefile_name: 	codefile_name,
+			codefile_path: 	codefile_path,
 			class_name: 	class_name,
 			params: 		param_list,
 			contains: 		contains_list,
diff --git a/src/io/io_packageparser.js b/src/io/io_packageparser.js
index 2f1b1252ed85ef911e13b0ab63ec2db9a811a608..b21016ce27c06e65d6f887dce2128d6d18dca037 100644
--- a/src/io/io_packageparser.js
+++ b/src/io/io_packageparser.js
@@ -4,7 +4,6 @@ IO.PackageParser = new (function() {
 	var fs = require('fs');
 	var path = require('path');
 
-
 	var dom_parser = new DOMParser();
 	var watched_states = {};
 
@@ -27,17 +26,37 @@ IO.PackageParser = new (function() {
 					callback(pkg_list, add_states, add_behaviors);
 				} else {
 					checkForRelevance(entry['path'], (has_states, has_behaviors) => {
-							if (has_states) add_states.push(entry);
-							if (has_behaviors) add_behaviors.push(entry);
-						});
-
-					processEntry(idx+1);
+						if (has_states || has_behaviors) {
+							var add_package = function(python_path) {
+								if (python_path != undefined) {
+									entry['python_path'] = python_path;
+									if (has_states) add_states.push(entry);
+									if (has_behaviors) add_behaviors.push(entry);
+								}
+								processEntry(idx+1);
+							}
+							python_path = entry['python_path'];
+							if (python_path == undefined) {
+								ROS.getPackagePythonPath(entry['name'], add_package);
+							} else {
+								add_package(python_path);
+							}
+						} else {
+							processEntry(idx+1);
+						}
+					});
 				}
 			};
 			processEntry(0);
 		});
 	}
 
+	this.stopWatching = function() {
+		for (var state in watched_states) {
+			watched_states[state].close();
+		}
+	}
+
 	var checkForRelevance = function(pkg_path, callback) {
 		var data = fs.readFileSync(path.join(pkg_path, 'package.xml'));
 		var xml = dom_parser.parseFromString(String(data), "text/xml");
@@ -51,38 +70,54 @@ IO.PackageParser = new (function() {
 	var watchStateFolder = function(folder_path, import_path) {
 		if (watched_states[folder_path] != undefined) return;
 
-		// watched_states[folder_path] = fs.watch(folder_path,
-		// 	{persistent: false},
-		// 	(eventType, filename) => {
-		// 		if (eventType == 'change') {
-		// 			var entry = path.join(folder_path, filename);
-		// 			IO.Filesystem.readFile(entry, (content) => {
-		// 				var imports = entry.replace(import_path+"/", "").replace(/.py$/i, "").replace(/[\/]/g, ".");
-		// 				var state_def = IO.StateParser.parseState(content, imports);
-		// 				if (state_def != undefined) {
-		// 					state_def.setFilePath(entry);
-		// 					WS.Statelib.updateDef(state_def);
-		// 					T.logInfo("Updating changed definition for state: " + state_def.getStateClass());
-		// 					// TODO update defs for existing states and re-draw
-		// 				}
-		// 			});
-		// 		}
-		// 	}
-		// );
+		watched_states[folder_path] = fs.watch(folder_path,
+			{persistent: false},
+			(eventType, filename) => {
+				if(RC.Controller.isReadonly()) {
+					T.logWarn("A state definition source file changed while in read-only mode, ignoring the change for now!");
+					return;
+				}
+				if (filename.endsWith(".py")) {
+					var entry = path.join(folder_path, filename);
+					IO.Filesystem.readFile(entry, (content) => {
+						var imports = entry.replace(import_path+"/", "").replace(/.py$/i, "").replace(/[\/]/g, ".");
+						var state_def = IO.StateParser.parseState(content, imports);
+						if (state_def != undefined) {
+							state_def.setFilePath(entry);
+							WS.Statelib.updateDef(state_def);
+							T.logInfo("Updating changed definition for state: " + state_def.getStateClass());
+							var update_states = Behavior.getStatemachine().traverseStates(function(state) {
+								return state.getStateClass() == state_def.getStateClass();
+							});
+							update_states.forEach(function (state) {
+								state.updateStateDefinition(state_def);
+								if (state.getContainer() == UI.Statemachine.getDisplayedSM() 
+									&& UI.Panels.StateProperties.isCurrentState(state)) {
+									UI.Panels.StateProperties.hide();
+									UI.Panels.StateProperties.displayStateProperties(state);
+								}
+							});
+							if (UI.Menu.isPageStatemachine()) {
+								UI.Statemachine.refreshView();
+							}
+							RC.Controller.signalChanged();
+						}
+					});
+				}
+			}
+		);
 	}
 
-	this.parseStateFolder = function(folder, import_path, has_init) {
+	this.parseStateFolder = function(folder, import_path) {
 		IO.Filesystem.checkFileExists(folder, "__init__.py", function(exists) {
-			has_init = has_init || exists;
+			if (exists) {
+				import_path = import_path || path.dirname(folder);
+			}
 			IO.Filesystem.getFolderContent(folder, function(files) {
 				files.sort().forEach(function(entry, i) {
 					if(IO.Filesystem.isFolder(entry)) {
-						if (!has_init) {
-							that.parseStateFolder(entry, path.dirname(entry), has_init);
-						} else {
-							that.parseStateFolder(entry, import_path, has_init);
-						}
-					} else if (has_init) {
+						that.parseStateFolder(entry, import_path);
+					} else if (import_path != undefined) {
 						if (path.extname(entry) != ".py") return;
 						IO.Filesystem.readFile(entry, (content) => {
 							var imports = entry.replace(import_path+"/", "").replace(/.py$/i, "").replace(/[\/]/g, ".");
@@ -99,15 +134,15 @@ IO.PackageParser = new (function() {
 		});
 	}
 
-	this.parseBehaviorFolder = function(folder, pkg_name) {
+	this.parseBehaviorFolder = function(folder, pkg_name, python_path) {
 		IO.Filesystem.getFolderContent(folder, function(files) {
 			files.sort().forEach(function(entry, i) {
 				if(IO.Filesystem.isFolder(entry)) {
-					that.parseBehaviorFolder(entry, pkg_name);
+					that.parseBehaviorFolder(entry, pkg_name, python_path);
 				} else {
 					if (path.extname(entry) != ".xml" || path.basename(entry)[0] == '#') return;
 					IO.Filesystem.readFile(entry, (content) => {
-						var manifest = IO.ManifestParser.parseManifest(content, entry);
+						var manifest = IO.ManifestParser.parseManifest(content, entry, python_path);
 						if (manifest != undefined) {
 							if (manifest.rosnode_name != pkg_name) {
 								T.logWarn("Ignoring behavior " + manifest.name + ": Manifest and code need to be in the same ROS package.");
diff --git a/src/prototype.js b/src/prototype.js
index 66ff5dd15e1bbd2b73a91e925d737572f7b9f502..b88a91f4414b92a2c90b4184ee7c8703a54fabec 100644
--- a/src/prototype.js
+++ b/src/prototype.js
@@ -33,6 +33,11 @@ Array.prototype.contains = function(element) {
 	return this.indexOf(element) != -1;
 }
 
+Array.prototype.hasSameElements = function(other) {
+	var diff = this.filter(el => !other.contains(el));
+	return 0 == diff.concat(other.filter(el => !this.contains(el))).length;
+}
+
 
 //=====================
 //	String
diff --git a/src/ros/ros.js b/src/ros/ros.js
index 34f093511e336bedd43c63bbcb150bac3e6ecd25..02a6de770123d6bc393bad4cb96c096e9c97d259 100644
--- a/src/ros/ros.js
+++ b/src/ros/ros.js
@@ -64,7 +64,8 @@ rospy.spin()
 					package_cache[i] = package_cache[i].split(" ");
 					package_cache[i] = {
 						'name': package_cache[i][0],
-						'path': package_cache[i][1]
+						'path': package_cache[i][1],
+						'python_path': undefined
 					}
 				}
 				callback(package_cache.clone());
@@ -89,6 +90,46 @@ rospy.spin()
 		});
 	}
 
+	that.getPackagePythonPath = function(package_name, callback) {
+		var python_path = undefined;
+		that.getPackageList((package_cache) => {
+			for (var i=0; i<package_cache.length; i++) {
+				if (package_cache[i]['name'] == package_name) {
+					python_path = package_cache[i]['python_path'];
+					break;
+				}
+			}
+		});
+		if (python_path !== undefined) {
+            process.nextTick(() => {
+                callback(python_path);
+            });
+    	} else {
+			var proc = spawn('python', ['-c', `import importlib; print(importlib.import_module('` + package_name + `').__path__[-1])`]);
+			var path_data = '';
+			proc.stdout.on('data', data => {
+				path_data += data;
+			});
+			proc.stderr.on('data', data => {
+				console.log(package_name+" failed to import: "+data);
+			});
+			proc.on('close', (code) => {
+				if (path_data != "") {
+					python_path = path_data.replace(/\n/g, '');
+					for (var i=0; i<package_cache.length; i++) {
+						if (package_cache[i]['name'] == package_name) {
+							package_cache[i]['python_path'] = python_path;
+							break;
+						}
+					}
+					callback(python_path);
+				} else {
+					callback(undefined);
+				}
+			});
+		}
+	}
+
 	// that.getParam = function(name, callback) {
 	// 	var proc = spawn('rosparam', ['get', name]);
 	// 	proc.stdout.on('data', data => {
diff --git a/src/ui/panels/ui_panels_stateproperties.js b/src/ui/panels/ui_panels_stateproperties.js
index a17dd54199cc945566e4ea109805d355f5c77d43..988b17890f7063b9cf3fbef0c24fbf56b0310dd1 100644
--- a/src/ui/panels/ui_panels_stateproperties.js
+++ b/src/ui/panels/ui_panels_stateproperties.js
@@ -295,6 +295,7 @@ UI.Panels.StateProperties = new (function() {
 			remove_button.addEventListener("click", function() {
 				if (RC.Controller.isReadonly()
 					|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+					|| Behavior.isReadonly()
 					|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 					|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 					) return;
@@ -328,6 +329,7 @@ UI.Panels.StateProperties = new (function() {
 			input_field.addEventListener("blur", function() {
 				if (RC.Controller.isReadonly()
 					|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+					|| Behavior.isReadonly()
 					|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 					|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 					) return;
@@ -348,6 +350,7 @@ UI.Panels.StateProperties = new (function() {
 			remove_button.addEventListener("click", function() {
 				if (RC.Controller.isReadonly()
 					|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+					|| Behavior.isReadonly()
 					|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 					|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 					) return;
@@ -383,6 +386,7 @@ UI.Panels.StateProperties = new (function() {
 			input_field.addEventListener("blur", function() {
 				if (RC.Controller.isReadonly()
 					|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+					|| Behavior.isReadonly()
 					|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 					|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 					) return;
@@ -403,6 +407,7 @@ UI.Panels.StateProperties = new (function() {
 			remove_button.addEventListener("click", function() {
 				if (RC.Controller.isReadonly()
 					|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+					|| Behavior.isReadonly()
 					|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 					|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 					) return;
@@ -473,6 +478,7 @@ UI.Panels.StateProperties = new (function() {
 				input_field.addEventListener("blur", function() {
 					if (RC.Controller.isReadonly()
 						|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+						|| Behavior.isReadonly()
 						|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 						|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 						) return;
@@ -493,6 +499,7 @@ UI.Panels.StateProperties = new (function() {
 				default_button.addEventListener("change", function() {
 					if (RC.Controller.isReadonly()
 						|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+						|| Behavior.isReadonly()
 						|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 						|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 						) return;
@@ -546,6 +553,7 @@ UI.Panels.StateProperties = new (function() {
 				input_field.addEventListener("blur", function() {
 					if (RC.Controller.isReadonly()
 						|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+						|| Behavior.isReadonly()
 						|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 						|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 						) return;
@@ -596,6 +604,7 @@ UI.Panels.StateProperties = new (function() {
 	this.deleteStateClicked = function() {
 		if (RC.Controller.isReadonly()
 			|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			|| Behavior.isReadonly()
 			|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 			|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) {
@@ -692,6 +701,7 @@ UI.Panels.StateProperties = new (function() {
 	this.applyPropertiesClicked = function() {
 		if (RC.Controller.isReadonly() 
 			|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			|| Behavior.isReadonly()
 			|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 			|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) {
@@ -836,6 +846,7 @@ UI.Panels.StateProperties = new (function() {
 
 		if (!RC.Controller.isReadonly()
 			&& !UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			&& !Behavior.isReadonly()
 			&& (!RC.Controller.isLocked() || !RC.Controller.isStateLocked(current_prop_state.getStatePath()))
 			&& !RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) {
@@ -875,6 +886,7 @@ UI.Panels.StateProperties = new (function() {
 		if (document.getElementById("input_prop_outcome_add").value == "") return;
 		if (RC.Controller.isReadonly()
 			|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			|| Behavior.isReadonly()
 			|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 			|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) return;
@@ -910,6 +922,7 @@ UI.Panels.StateProperties = new (function() {
 		if (document.getElementById("input_prop_input_key_add").value == "") return;
 		if (RC.Controller.isReadonly()
 			|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			|| Behavior.isReadonly()
 			|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 			|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) return;
@@ -952,6 +965,7 @@ UI.Panels.StateProperties = new (function() {
 		if (document.getElementById("input_prop_output_key_add").value == "") return;
 		if (RC.Controller.isReadonly()
 			|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			|| Behavior.isReadonly()
 			|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 			|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) return;
@@ -993,6 +1007,7 @@ UI.Panels.StateProperties = new (function() {
 	this.containerTypeChanged = function(evt) {
 		if(RC.Controller.isReadonly()
 			|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			|| Behavior.isReadonly()
 			|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 			|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) return;
@@ -1094,6 +1109,7 @@ UI.Panels.StateProperties = new (function() {
 	this.synthesizeClicked = function() {
 		if(RC.Controller.isReadonly()
 			|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			|| Behavior.isReadonly()
 			|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 			|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) return;
diff --git a/src/ui/ui_dashboard.js b/src/ui/ui_dashboard.js
index a3a112ae814e003ee2ee65043d829b2f28dd430e..edd3b4cdd2aa19af1b9cde93252312c99b625fbc 100644
--- a/src/ui/ui_dashboard.js
+++ b/src/ui/ui_dashboard.js
@@ -1225,6 +1225,7 @@ UI.Dashboard = new (function() {
 
 	this.resetAllFields = function() {
 		UI.Settings.createBehaviorPackageSelect(document.getElementById('select_behavior_package'));
+		Behavior.setBehaviorPackage(UI.Settings.getDefaultPackage());
 		document.getElementById('input_behavior_name').value = "";
 		document.getElementById('input_behavior_description').value = "";
 		document.getElementById('input_behavior_tags').value = "";
diff --git a/src/ui/ui_menu.js b/src/ui/ui_menu.js
index 4fae0c90e4d47eb1c1f3701b670b6f6c6cda641f..f7fbc088c404f4fcb6a25d5f727e34588bc04eb1 100644
--- a/src/ui/ui_menu.js
+++ b/src/ui/ui_menu.js
@@ -284,7 +284,12 @@ UI.Menu = new (function() {
 	}
 
 	this.saveBehaviorClicked = function() {
-		var check_error_string = Checking.checkBehavior();
+		var check_error_string = undefined;
+		if (Behavior.isReadonly()) {
+			check_error_string = "behavior has been loaded from a read-only file";
+		} else {
+			check_error_string = Checking.checkBehavior();
+		}
 		if (check_error_string != undefined) {
 			T.clearLog();
 			T.show();
diff --git a/src/ui/ui_settings.js b/src/ui/ui_settings.js
index 1b6f566973db7e9498df0200fa2d5e3b2880507b..70e54365c21b0d2ad3646d08ef4ae9b0eac14db8 100644
--- a/src/ui/ui_settings.js
+++ b/src/ui/ui_settings.js
@@ -144,14 +144,14 @@ UI.Settings = new (function() {
 	this.updateStatelib = function() {
 		WS.Statelib.resetLib();
 		state_pkg_cache.forEach(state_pkg => {
-			IO.PackageParser.parseStateFolder(state_pkg['path'], state_pkg['path'], false);
+			IO.PackageParser.parseStateFolder(state_pkg['python_path']);
 		});
 	}
 
 	this.updateBehaviorlib = function() {
 		WS.Behaviorlib.resetLib();
 		behavior_pkg_cache.forEach(behavior_pkg => {
-			IO.PackageParser.parseBehaviorFolder(behavior_pkg['path'], behavior_pkg['name']);
+			IO.PackageParser.parseBehaviorFolder(behavior_pkg['path'], behavior_pkg['name'], behavior_pkg['python_path']);
 		});
 	}
 
@@ -208,7 +208,9 @@ UI.Settings = new (function() {
 			var pkg_select = document.createElement("select");
 			pkg_select.setAttribute("style", "width:100%; margin: 5px 0;");
 			var pkg_select_update_title = function() {
-				pkg_select.setAttribute("title", pkg_select.options[pkg_select.selectedIndex].getAttribute("title"));
+				if (pkg_select.options.length > 0) {
+					pkg_select.setAttribute("title", pkg_select.options[pkg_select.selectedIndex].getAttribute("title"));
+				}
 			};
 			pkg_select.addEventListener('change', pkg_select_update_title);
 			var packages = ros_pkg_cache.filter((pkg) => { return !pkg['path'].startsWith("/opt/ros"); });
diff --git a/src/ui/ui_statemachine.js b/src/ui/ui_statemachine.js
index 44646f32e1806c059231d3329bcff10d03192e2b..3f6b786a27bb537261175d8c463e73476716cad0 100644
--- a/src/ui/ui_statemachine.js
+++ b/src/ui/ui_statemachine.js
@@ -290,7 +290,7 @@ UI.Statemachine = new (function() {
 	}
 
 	this.isReadonly = function() {
-		return RC.Controller.isReadonly() || displayed_sm.isInsideDifferentBehavior();
+		return RC.Controller.isReadonly() || displayed_sm.isInsideDifferentBehavior() || Behavior.isReadonly();
 	}
 
 	this.applyGraphLayout = function() {
@@ -416,7 +416,7 @@ UI.Statemachine = new (function() {
 		}
 
 		// draw transitions at last
-		var transitions_readonly = RC.Controller.isReadonly() || dataflow_displayed || displayed_sm.isInsideDifferentBehavior();
+		var transitions_readonly = RC.Controller.isReadonly() || dataflow_displayed || displayed_sm.isInsideDifferentBehavior() || Behavior.isReadonly();
 		var new_transitions = [];
 		for (var i=0; i<transitions.length; ++i) {
 			var t = transitions[i];
@@ -472,7 +472,7 @@ UI.Statemachine = new (function() {
 
 		if (RC.Controller.isReadonly()) {
 			background.attr({fill: '#f3f6ff', stroke: '#c5d2ee'});
-		} else if (displayed_sm.isInsideDifferentBehavior()) {
+		} else if (displayed_sm.isInsideDifferentBehavior() || Behavior.isReadonly()) {
 			background.attr({fill: '#fff3f6'});
 		} else {
 			background.attr({fill: '#FFF'});
diff --git a/src/window.html b/src/window.html
index 3e5d9d8ef04b2cc22acc49165a95d0c776184366..54f9a1c653538ba432b7fa0e1eb7bec1a9980354 100644
--- a/src/window.html
+++ b/src/window.html
@@ -75,8 +75,9 @@
 	<script src="ws/ws_statemachinedefinition.js"></script>
 	<script src="ws/ws_statelib.js"></script>
 
-	<script src="_testing/scripts.js"></script>
-	<script src="_testing/test.js"></script>
+	<script src="_test/scripts.js"></script>
+	<script src="_test/testreport.js"></script>
+	<script src="_test/statelib.test.js"></script>
 
 	<link rel="stylesheet" href="style/main.css" type="text/css" />
 	<link rel="stylesheet" href="style/dashboard.css" type="text/css" />
diff --git a/src/window.js b/src/window.js
index 98c5950af88201878a7f93f25e963bef8a44b543..38027bf7cd8abd09d9327068e2bceb76e1b88b77 100644
--- a/src/window.js
+++ b/src/window.js
@@ -25,4 +25,10 @@ window.onload = function() {
 	UI.Settings.restoreSettings();
 
 	UI.Feed.initialize();
+
+	if (gui.App.argv.contains('--run-tests')) {
+		setTimeout(() => {
+			TestReport.runAllTests(status =>  gui.App.quit());
+		}, 5 * 1000);
+	}
 }