Split SDM's JavaScript into a lib that can be tested.

Change-Id: I11b7821871fbd626a579d4dba9b5f9a312f537ca
diff --git a/dev/codeserver/build.xml b/dev/codeserver/build.xml
index fc7f96b..8b40e3a 100755
--- a/dev/codeserver/build.xml
+++ b/dev/codeserver/build.xml
@@ -21,6 +21,7 @@
       <src path="java" />
       <src path="javatests" />
       <classpath>
+        <pathelement location="${gwt.root}/build/out/dev/bin-test"/>
         <pathelement location="${javac.out}"/>
         <pathelement location="${gwt.dev.jar}" />
         <pathelement location="${gwt.tools.lib}/junit/junit-4.8.2.jar" />
@@ -57,11 +58,14 @@
         test.out="${junit.out}/codeserver"
         test.cases="tests">
       <extraclasspaths>
+        <pathelement location="${gwt.root}/build/out/dev/bin-test"/>
         <pathelement location="${project.lib}"/>
         <pathelement location="${gwt.dev.jar}" />
         <!-- Pull in gwt-user sources -->
         <pathelement location="${gwt.root}/user/src/"/>
         <pathelement location="${gwt.root}/user/super/"/>
+        <pathelement location="java/" />
+        <pathelement location="javatests/" />
       </extraclasspaths>
     </gwt.junit>
   </target>
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
index 7f07ca0..53f9c30 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
@@ -51,7 +51,6 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.net.URL;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -260,13 +259,20 @@
 
     String outputModuleName = module.getName();
     try {
-      URL resource = Resources.getResource(Recompiler.class, "recompile.nocache.js");
-      String stub = Resources.toString(resource, Charsets.UTF_8);
-      return "(function() {\n"
-      + " var moduleName = '" + outputModuleName  + "';\n"
-      + PropertiesUtil.generatePropertiesSnippet(module, compileLogger)
-      + stub
-      + "})();\n";
+      String templateJs = Resources.toString(
+          Resources.getResource(Recompiler.class, "recompile_template.js"), Charsets.UTF_8);
+      String propertyProviders = PropertiesUtil.generatePropertiesSnippet(module, compileLogger);
+      String libJs = Resources.toString(
+          Resources.getResource(Recompiler.class, "recompile_lib.js"), Charsets.UTF_8);
+      String recompileJs = Resources.toString(
+          Resources.getResource(Recompiler.class, "recompile_main.js"), Charsets.UTF_8);
+      templateJs = templateJs.replace("__MODULE_NAME__", "'" + outputModuleName + "'");
+      templateJs = templateJs.replace("__PROPERTY_PROVIDERS__", propertyProviders);
+      templateJs = templateJs.replace("__LIB_JS__", libJs);
+      templateJs = templateJs.replace("__MAIN__", recompileJs);
+
+      return templateJs;
+
     } catch (IOException e) {
       compileLogger.log(Type.ERROR, "Can not generate + " + outputModuleName
           + " + .recompile.nocache.js", e);
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/recompile.nocache.js b/dev/codeserver/java/com/google/gwt/dev/codeserver/recompile_lib.js
similarity index 75%
rename from dev/codeserver/java/com/google/gwt/dev/codeserver/recompile.nocache.js
rename to dev/codeserver/java/com/google/gwt/dev/codeserver/recompile_lib.js
index 0c412fa..c74d699 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/recompile.nocache.js
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/recompile_lib.js
@@ -14,11 +14,9 @@
  * under the License.
  */
 
-
-// These variables are used by property providers.
-// (We also use them below.)
-var $wnd = window;
-var $doc = document;
+// Export into our name space
+// We do not consider any of these classes a public API and they will be changed as needed.
+$namespace.lib = $namespace.lib || {};
 
 /**
  * Construct an instance of the PropertyHelper.
@@ -50,7 +48,8 @@
     // TODO(dankurka): trigger this error in the ui
     // IE8 only has console defined if its dev tools have been opened before
     if ($wnd.console && $wnd.console.log) {
-      $wnd.console.log("provider for " + propName + " returned unexpected value: '" + val + "'");
+      $wnd.console.log("provider for " + propName
+          + " returned unexpected value: '" + val + "'");
     }
     throw "can't compute binding property value for " + propName;
   }
@@ -70,6 +69,9 @@
   return result;
 };
 
+// Export PropertyHelper to namespace
+$namespace.lib.PropertyHelper = PropertyHelper;
+
 /**
  * Create a dialog.
  * @constructor
@@ -147,6 +149,9 @@
   $doc.body.removeChild(this.__dialog);
 };
 
+//Export Dialog to namespace
+$namespace.lib.Dialog = Dialog;
+
 /**
  * Construct a Recompiler object.
  * @constructor
@@ -155,10 +160,15 @@
  * @returns
  */
 function Recompiler(moduleName, permutationProperties) {
-  $wnd.__gwt_sdm__recompiler = $wnd.__gwt_sdm__recompiler || {};
-  $wnd.__gwt_sdm__recompiler.counter = $wnd.__gwt_sdm__recompiler.counter || 0;
-  $wnd.__gwt_sdm__recompiler.callbacks = $wnd.__gwt_sdm__recompiler.callback || {};
-  this.__globals = $wnd.__gwt_sdm__recompiler;
+  if ($wnd.__gwt_sdm_globals) {
+    this.__globals = $wnd.__gwt_sdm_globals;
+  } else {
+    this.__globals = {
+      callbackCounter: new Date().getTime(), // avoid cache hits
+      callbacks: {}
+    };
+    $wnd.__gwt_sdm_globals = this.__globals;
+  }
   this.__moduleName = moduleName;
   this.__permutationProperties = permutationProperties;
   this.__compiling = false;
@@ -175,7 +185,7 @@
     props.push($wnd.encodeURIComponent(key) + '=' +
         $wnd.encodeURIComponent(this.__permutationProperties[key]));
   }
-  return url + props.join('&') + '&';
+  return url + props.join('&');
 };
 
 /**
@@ -205,7 +215,7 @@
     callback(json);
   };
 
-  var url = url + '_callback=__gwt_sdm__recompiler.callbacks.' + callback_id;
+  var url = url + '&_callback=__gwt_sdm_globals.callbacks.' + callback_id;
   var script = $doc.createElement('script');
   script.src = url;
   var $head = $doc.head || $doc.getElementsByTagName('head')[0];
@@ -268,65 +278,5 @@
   return this.getCodeServerBaseUrl() + 'log/' + this.__moduleName;
 };
 
-/**
- * Construct the main class.
- *
- * @constructor
- * @param {string} moduleName
- * @param {Object} propertyProviders
- * @param {Object} propertyValues
- */
-function Main(moduleName, propertyProviders, propertyValues){
-  var propertyHelper = new PropertyHelper(moduleName, propertyProviders, propertyValues);
-  this.__moduleName = moduleName;
-  this.__dialog = new Dialog();
-  this.__recompiler = new Recompiler(moduleName, propertyHelper.computeBindingProperties());
-  // Publish a global variable to let others know that we have been loaded
-  $wnd.__gwt_sdm__recompiler = $wnd.__gwt_sdm__recompiler || {};
-  $wnd.__gwt_sdm__recompiler.loaded = true;
-}
-
-/**
- * Compile the current gwt module.
- */
-Main.prototype.compile = function() {
-  var that = this;
-  this.__dialog.clear();
-  this.__dialog.add(this.__dialog.createTextElement("div", "12pt", "Compiling " + this.__moduleName));
-  this.__dialog.show();
-  this.__recompiler.compile(function(result) {
-    that.__dialog.clear();
-    if (result.status != 'ok') {
-      that.__renderError(result);
-    } else {
-      that.__dialog.hide();
-      that.__recompiler.loadApp();
-    }
-  });
-};
-
-/**
- * Render an error if compile failed.
- * @param {object} result - the jsonp object from the compile server.
- */
-Main.prototype.__renderError = function(result) {
-  var that = this;
-  var link = this.__dialog.createTextElement('a', '16pt', result.status);
-  link.setAttribute('href', this.__recompiler.getLogUrl());
-  link.setAttribute('target', 'gwt_dev_mode_log');
-  link.style.color = 'red';
-  link.style.textDecoration = 'underline';
-  this.__dialog.add(link);
-
-  var button = this.__dialog.createTextElement('button', '12pt', 'Try Again');
-  button.onclick = function() {
-    that.compile();
-  };
-  button.style.marginLeft = '10px';
-  this.__dialog.add(button);
-};
-
-
-new Main(moduleName, providers, values).compile();
-
-
+//Export Recompiler to namespace
+$namespace.lib.Recompiler = Recompiler;
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/recompile_main.js b/dev/codeserver/java/com/google/gwt/dev/codeserver/recompile_main.js
new file mode 100644
index 0000000..16f05d8
--- /dev/null
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/recompile_main.js
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+var Dialog = $namespace.lib.Dialog;
+var PropertyHelper = $namespace.lib.PropertyHelper;
+var Recompiler = $namespace.lib.Recompiler;
+//Publish a global variable to let others know that we have been loaded
+$wnd.__gwt_sdm = $wnd.__gwt_sdm || {};
+$wnd.__gwt_sdm.loaded = true;
+
+/**
+ * Construct the main class.
+ *
+ * @constructor
+ * @param {string} moduleName
+ * @param {Object} propertyProviders
+ * @param {Object} propertyValues
+ */
+function Main(moduleName, propertyProviders, propertyValues){
+  var propertyHelper = new PropertyHelper(moduleName, propertyProviders, propertyValues);
+  this.__moduleName = moduleName;
+  this.__dialog = new Dialog();
+  this.__recompiler = new Recompiler(moduleName, propertyHelper.computeBindingProperties());
+}
+
+/**
+ * Compile the current gwt module.
+ */
+Main.prototype.compile = function() {
+  var that = this;
+  this.__dialog.clear();
+  this.__dialog.add(this.__dialog.createTextElement("div", "12pt", "Compiling " + this.__moduleName));
+  this.__dialog.show();
+  this.__recompiler.compile(function(result) {
+    that.__dialog.clear();
+    if (result.status != 'ok') {
+      that.__renderError(result);
+    } else {
+      that.__dialog.hide();
+      that.__recompiler.loadApp();
+    }
+  });
+};
+
+/**
+ * Render an error if compile failed.
+ * @param {object} result - the jsonp object from the compile server.
+ */
+Main.prototype.__renderError = function(result) {
+  var that = this;
+  var link = this.__dialog.createTextElement('a', '16pt', result.status);
+  link.setAttribute('href', this.__recompiler.getLogUrl());
+  link.setAttribute('target', 'gwt_dev_mode_log');
+  link.style.color = 'red';
+  link.style.textDecoration = 'underline';
+  this.__dialog.add(link);
+
+  var button = this.__dialog.createTextElement('button', '12pt', 'Try Again');
+  button.onclick = function() {
+    that.compile();
+  };
+  button.style.marginLeft = '10px';
+  this.__dialog.add(button);
+};
+
+new Main(moduleName, providers, values).compile();
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/recompile_template.js b/dev/codeserver/java/com/google/gwt/dev/codeserver/recompile_template.js
new file mode 100644
index 0000000..0afd4a5
--- /dev/null
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/recompile_template.js
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+(function(){
+  var $wnd = window;
+  var $doc = $wnd.document;
+  var $namespace = {};
+  var moduleName = __MODULE_NAME__;
+  __PROPERTY_PROVIDERS__
+  __LIB_JS__
+  __MAIN__
+})();
diff --git a/dev/codeserver/javatests/com/google/gwt/dev/codeserver/CodeServerSuite.java b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/CodeServerSuite.java
new file mode 100644
index 0000000..9f8ea74
--- /dev/null
+++ b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/CodeServerSuite.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.codeserver;
+
+import com.google.gwt.dev.codeserver.client.CodeServerGwtTest;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+public class CodeServerSuite {
+  public static Test suite() {
+    TestSuite suite = new TestSuite("Tests of the code server package");
+    suite.addTestSuite(CodeServerGwtTest.class);
+    return suite;
+  }
+}
diff --git a/dev/codeserver/javatests/com/google/gwt/dev/codeserver/CodeServerTest.gwt.xml b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/CodeServerTest.gwt.xml
new file mode 100644
index 0000000..38f5f90
--- /dev/null
+++ b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/CodeServerTest.gwt.xml
@@ -0,0 +1,18 @@
+<!--                                                                        -->
+<!-- Copyright 2014 Google Inc.                                             -->
+<!-- Licensed under the Apache License, Version 2.0 (the "License"); you    -->
+<!-- may not use this file except in compliance with the License. You may   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+<module>
+  <inherits name='com.google.gwt.resources.Resources'/>
+  <source path="" includes="recompile_lib.js"/>
+  <source path="client"/>
+</module>
diff --git a/dev/codeserver/javatests/com/google/gwt/dev/codeserver/client/CodeServerGwtTest.java b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/client/CodeServerGwtTest.java
new file mode 100644
index 0000000..6ebea95
--- /dev/null
+++ b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/client/CodeServerGwtTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.codeserver.client;
+
+import com.google.gwt.core.client.ScriptInjector;
+import com.google.gwt.core.shared.GWT;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.TextResource;
+
+/**
+ * A GwtTestCase for the JavaScript that is part of super dev mode.
+ *
+ * Since inside of the GWT SDK there is not a lot of JavaScript it does not make sense to add a
+ * extra testing framework for JavaScript. Rather this class bundles the JavaScript that should
+ * be tested as a TextResource and injects it into the current page. This way we can write test
+ * against it using JSNI.
+ */
+public class CodeServerGwtTest extends GWTTestCase {
+
+  interface Resource extends ClientBundle {
+    @Source("com/google/gwt/dev/codeserver/recompile_lib.js")
+    TextResource libJS();
+  }
+
+  private boolean injected;
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.dev.codeserver.CodeServerTest";
+  }
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    ensureJsInjected();
+  }
+
+  public native void testPropertyHelper_withProperInput() /*-{
+    // setup property providers and values for the test
+    var mocks = (function(){
+      var propProviders = {};
+      var propValues = {};
+      propProviders['prop1'] = function(){
+        return 'val1_1';
+      };
+
+      propValues['prop1'] = {'val1_1':0,'val1_2':1,'val1_3':2};
+
+      propProviders['prop2'] = function(){
+        return 'val2_2';
+      };
+
+      propValues['prop2'] = {'val2_1':0,'val2_2':1,'val2_3':2};
+      return {provider: propProviders, values : propValues};
+    })();
+
+    // Actual test
+    var PropertyHelper = $wnd.namespace.lib.PropertyHelper;
+    var propertyHelper = new PropertyHelper('testModule', mocks.provider, mocks.values);
+    var result = propertyHelper.computeBindingProperties();
+    var assertStringEquals = @CodeServerGwtTest::assertEquals(Ljava/lang/String;Ljava/lang/String;);
+    var assertTrue = @CodeServerGwtTest::assertTrue(Ljava/lang/String;Z);
+
+    var length = Object.keys(result).length;
+    assertTrue(length == 2, "PropertyHelper did not return two entries: " + length);
+    assertStringEquals('val1_1', result['prop1']);
+    assertStringEquals('val2_2', result['prop2']);
+  }-*/;
+
+  public native void testRecompiler() /*-{
+    var Recompiler = $wnd.namespace.lib.Recompiler;
+    var recompiler = new Recompiler('testModule', {prop1: 'val1', prop2 : 'val2'});
+
+    var jsonpUrl = '';
+    var callbackCalled = false;
+
+    var assertStringEquals = @CodeServerGwtTest::assertEquals(Ljava/lang/String;Ljava/lang/String;);
+    var assertTrue = @CodeServerGwtTest::assertTrue(Ljava/lang/String;Z);
+
+    // patch up functions of recompiler that need the actual SDM environment
+    recompiler.getCodeServerBaseUrl = function() {
+      return "http://mytesthost:7812/";
+    };
+
+    recompiler.__jsonp = function(url, callback) {
+      jsonpUrl = url;
+      callback({status : 'ok'});
+    };
+
+    // do the test
+    recompiler.compile(function(result) {
+      callbackCalled = true;
+      //compile is done
+      assertStringEquals('ok', result.status);
+      assertStringEquals('http://mytesthost:7812/recompile/testModule?prop1=val1&prop2=val2',
+          jsonpUrl);
+    });
+    assertTrue(callbackCalled, 'callback for successful recompile was not executed');
+  }-*/;
+
+  private void ensureJsInjected() {
+    if(injected) {
+      return;
+    }
+    Resource res = GWT.create(Resource.class);
+    String before = "(function(){ \n" +
+    		"$wnd.namespace = {};$namespace = $wnd.namespace; $global = $wnd";
+    String js = res.libJS().getText();
+    ScriptInjector.fromString(before + js + "})()").inject();
+  }
+}
diff --git a/eclipse/dev/codeserver/.classpath b/eclipse/dev/codeserver/.classpath
index 92a2b6c..5837833 100644
--- a/eclipse/dev/codeserver/.classpath
+++ b/eclipse/dev/codeserver/.classpath
@@ -10,5 +10,6 @@
 	<classpathentry kind="var" path="GWT_TOOLS/lib/jetty/jetty-8.1.12.v20130726/servlet-api-3.0-NoMetaInf.jar"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/junit/junit-4.8.2.jar" sourcepath="/GWT_TOOLS/lib/junit/junit-4.8.2-src.zip"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/gwt-dev"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/gwt-user"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>