Merge branch 'master', remote-tracking branch 'origin/master'

This commit is contained in:
Wouter Groeneveld 2011-06-28 22:02:38 +02:00
commit dde8c0d89b
17 changed files with 9270 additions and 269 deletions

109
README.md
View File

@ -2,6 +2,37 @@
## What's this?
Something like this:
```javascript
describe("pure awesomeness", function() {
it("should be amazing!", function() {
expect(stuff).toEqual("amazing");
});
it("should be awesome", function() {
expect(moreStuff).toBe("awesome");
});
});
describe("coolness", function() {
it("should be cooler than ice when freezed", function() {
var coolness = CoolingRepository.beCool();
coolness.freeze();
expect(coolness.coolnessRatio).toBe(-100);
});
it("should be cool enough by default", function() {
expect(CoolingRepository.beCool().coolnessRatio).toBe(-5);
});
});
```
Being translated into something like this:
![Junit Eclipse runner](http://i54.tinypic.com/rswjrl.jpg)
* * *
Quite simple, it's a custsom Java Junit Runner that allows you to embed Javascript Unit tests (using Jasmine) in your Java-based projects. It fully integrates with your most beloved IDE, your most hated version control system and of course your most needed CI env.
So let's rephrase:
@ -151,81 +182,3 @@ When the debug mode flag has been set to _true_, you can use the <a href="http:/
After pressing "GO", the tests will run and you can inspect stuff and step through the code.
Integrated debugging into for example Eclipse does not work for the moment.
* * *
# Advanced: Implementation details
## RhinoContext API
The _RhinoContext_ class is basically a wrapper/facade/whatever which allows you to easily manipulate the Javascript scope.
Read <a href="https://developer.mozilla.org/En/Rhino_documentation/Scopes_and_Contexts" target="_blank">Rhino docs: scopes and contexts</a> first please!
Creating a new RhinoContext initializes one "root" scope (toplevel), and assignes one context to the current Thread.
### Evaluating async javascript code
Creating another RhinoContext while passing the root scope, uses prototypal inheritance to create a new toplevel scope. This means the root scope is shared across different contexts (and thus different threads).
You can execute the _runAsync_ method, which does this:
* create a new thread and thus a new context
* create a new scope based on the root one -> shared
* execute stuff in the new scope (You can access root JS functions but not modify them, remember prototypal inheritance!)
* cleanup
For example, JasmineSpec uses the _execute_ Jasmine JS function on a spec and calls it in another thread:
```java
baseContext.runAsync(new RhinoRunnable() {
@Override
public void run(RhinoContext context) {
// get some random spec from Jasmine
NativeObject someSpec = (NativeObject) context.evalJS("jasmine.getEnv().currentRunner().suites()[0].specs()[0]");
context.executeFunction(someSpec, "execute");
}
});
```
### Creating a Rhino debugger
Basically creates a _org.mozilla.javascript.tools.debugger.Main_ object. Pitfall: create before loading all required JS files, but after creating the rhino context!
To acutally break once (so users can set breakpoints and press GO), use this:
> debugger.doBreak();
### Executing functions
_executeFunction_ is a convenience method to call a function on a passed NativeObject. The function pointer may reside in the object's prototype, you don't need to explicitly check this in Javascript but you do using Rhino!
## Envjs Utils/Hacks
### Error.stack fix
In firefox, you can get a stacktrace from a JS exception using:
> new Error("BOOM").stack
Of course this does not work in Envjs. But Rhino attaches an internal _rhinoException_ to each JS Error object, so using a bit of magic, now it's possible to call _getStackTrace()_
### Envjs.uri Windows relative paths fix
Use _file:///_ (three forward slashes) if no context has been provided. Works like this:
```javascript
Envjs.uri(path, "file:///" + ("" + Envjs.getcwd()).replace(/\\/g, '/') + "/")
```
### window.setTimeout fix
Used by Jasmine internally for async spec execution, but for some reason the Envjs Javascript implementation is broken.
A simple fix is possible, since using Rhino you can call Java objects in Javascript space! Wow awesome. So just create a new thread and use _sleep_:
```javascript
window.setTimeout = function(closure, timeout) {
spawn(function() {
java.lang.Thread.sleep(timeout);
closure();
});
};
```

9
bin/bootstrap.js vendored Executable file
View File

@ -0,0 +1,9 @@
load("./../src/test/javascript/lib/env.rhino.1.2.js");
load("./../src/test/javascript/lib/env.utils.js");
load("./../src/test/javascript/envJsOptions.js");
load("jquery-1.6.1.js");
function append(itm) {
$('body').append($(itm));
}

8936
bin/jquery-1.6.1.js vendored Executable file

File diff suppressed because it is too large Load Diff

BIN
bin/js.jar Executable file

Binary file not shown.

7
bin/rhino.bat Executable file
View File

@ -0,0 +1,7 @@
@echo off
REM -opt -1 is needed for Envjs (interpretation, not compilation mode)
REM Rhino JAR options: See https://developer.mozilla.org/en/Rhino_Shell
REM EnvJS usage: See http://www.envjs.com/doc/guides#running-rhino
java -cp js.jar org.mozilla.javascript.tools.shell.Main -opt -1

2
bin/rhino.debug.bat Executable file
View File

@ -0,0 +1,2 @@
@echo off
java -cp js.jar org.mozilla.javascript.tools.debugger.Main -opt -1

BIN
jasmine-junit-runner.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -10,7 +10,7 @@ import org.mozilla.javascript.NativeObject;
import be.klak.rhino.RhinoContext;
public class JasmineJSSuiteConverter {
class JasmineJSSuiteConverter {
private final RhinoContext context;
@ -36,7 +36,7 @@ public class JasmineJSSuiteConverter {
specs.addAll(convertToJunitDescription(suite, suiteDescription));
NativeArray subSuites = (NativeArray) context.executeFunction(suite, "suites");
convertSuiteArrayToDescriptions(subSuites, suiteDescription, processed);
specs.addAll(convertSuiteArrayToDescriptions(subSuites, suiteDescription, processed));
}
}

View File

@ -3,17 +3,17 @@ package be.klak.junit.jasmine;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.ScriptableObject;
public class JasmineSpecFailureException extends Exception {
class JasmineSpecFailureException extends Exception {
private final ScriptableObject trace;
private final ScriptableObject trace;
public JasmineSpecFailureException(NativeObject specResultItem) {
this.trace = (ScriptableObject) specResultItem.get("trace", specResultItem);
}
public JasmineSpecFailureException(NativeObject specResultItem) {
this.trace = (ScriptableObject) specResultItem.get("trace", specResultItem);
}
@Override
public String getMessage() {
return (String) trace.get("message", trace);
}
@Override
public String getMessage() {
return (String) trace.get("message", trace);
}
}

View File

@ -5,73 +5,73 @@ import java.io.IOException;
import org.apache.commons.io.FileUtils;
public class JasmineSpecRunnerGenerator {
class JasmineSpecRunnerGenerator {
private enum TemplatePlaceholders {
RELATIVE_PATH("<!--RelativePath-->"),
SOURCE_FILES_TO_INCLUDE("<!--SourceFileIncludes-->"),
SPEC_FILES_TO_INCLUDE("<!--SpecFileIncludes-->");
private enum TemplatePlaceholders {
RELATIVE_PATH("<!--RelativePath-->"),
SOURCE_FILES_TO_INCLUDE("<!--SourceFileIncludes-->"),
SPEC_FILES_TO_INCLUDE("<!--SpecFileIncludes-->");
private final String placeholder;
private final String placeholder;
private TemplatePlaceholders(String placeholder) {
this.placeholder = placeholder;
}
private TemplatePlaceholders(String placeholder) {
this.placeholder = placeholder;
}
public String getPlaceholder() {
return placeholder;
}
public String getPlaceholder() {
return placeholder;
}
}
}
private final JasmineSuite suite;
private final String[] jasmineSpecs;
private final String outputPath;
private final String outputFileName;
private final JasmineSuite suite;
private final String[] jasmineSpecs;
private final String outputPath;
private final String outputFileName;
public JasmineSpecRunnerGenerator(String[] jasmineSpecs, JasmineSuite suite, String outputPath, String outputFileName) {
this.jasmineSpecs = jasmineSpecs;
this.suite = suite;
this.outputPath = outputPath;
this.outputFileName = outputFileName;
}
public JasmineSpecRunnerGenerator(String[] jasmineSpecs, JasmineSuite suite, String outputPath, String outputFileName) {
this.jasmineSpecs = jasmineSpecs;
this.suite = suite;
this.outputPath = outputPath;
this.outputFileName = outputFileName;
}
public void generate() {
// TODO hardcoded relative path stuff wat configureerbaar maken
String template = loadTemplate();
template = replaceRelativePathsForLibs(template);
template = template.replaceAll(TemplatePlaceholders.SOURCE_FILES_TO_INCLUDE.getPlaceholder(),
getJavascriptFileIncludes("./../../../main/webapp/js", suite.sources()));
template = template.replaceAll(TemplatePlaceholders.SPEC_FILES_TO_INCLUDE.getPlaceholder(),
getJavascriptFileIncludes("./../specs", jasmineSpecs));
public void generate() {
// TODO hardcoded relative path stuff wat configureerbaar maken
String template = loadTemplate();
template = replaceRelativePathsForLibs(template);
template = template.replaceAll(TemplatePlaceholders.SOURCE_FILES_TO_INCLUDE.getPlaceholder(),
getJavascriptFileIncludes("./../../../main/webapp/js", suite.sources()));
template = template.replaceAll(TemplatePlaceholders.SPEC_FILES_TO_INCLUDE.getPlaceholder(),
getJavascriptFileIncludes("./../specs", jasmineSpecs));
try {
FileUtils.writeStringToFile(new File(outputPath + "/" + outputFileName), template);
} catch (IOException e) {
throw new RuntimeException("unable to write spec runner contents to destination", e);
}
}
try {
FileUtils.writeStringToFile(new File(outputPath + "/" + outputFileName), template);
} catch (IOException e) {
throw new RuntimeException("unable to write spec runner contents to destination", e);
}
}
private String replaceRelativePathsForLibs(String template) {
return template.replaceAll(TemplatePlaceholders.RELATIVE_PATH.getPlaceholder(), suite.jsRootDir());
}
private String replaceRelativePathsForLibs(String template) {
return template.replaceAll(TemplatePlaceholders.RELATIVE_PATH.getPlaceholder(), suite.jsRootDir());
}
private String getJavascriptFileIncludes(String path, String[] jsFiles) {
StringBuilder sourceFileIncludes = new StringBuilder();
for (String sourceFile : jsFiles) {
sourceFileIncludes.append("\t\t<script type='text/javascript' src='" + path + "/" + sourceFile
+ "'></script>\r\n");
}
return sourceFileIncludes.toString();
}
private String getJavascriptFileIncludes(String path, String[] jsFiles) {
StringBuilder sourceFileIncludes = new StringBuilder();
for (String sourceFile : jsFiles) {
sourceFileIncludes.append("\t\t<script type='text/javascript' src='" + path + "/" + sourceFile
+ "'></script>\r\n");
}
return sourceFileIncludes.toString();
}
private String loadTemplate() {
String template = null;
try {
template = FileUtils.readFileToString(new File(suite.jsRootDir() + "/lib/specRunner.tpl"));
} catch (IOException e) {
throw new RuntimeException("spec runner template file not found!", e);
}
return template;
}
private String loadTemplate() {
String template = null;
try {
template = FileUtils.readFileToString(new File(suite.jsRootDir() + "/lib/specRunner.tpl"));
} catch (IOException e) {
throw new RuntimeException("spec runner template file not found!", e);
}
return template;
}
}

View File

@ -9,6 +9,7 @@ import org.junit.Before;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.tools.debugger.Main;
@ -35,7 +36,7 @@ public class JasmineTestRunner extends Runner {
Main debugger = null;
if (this.suiteAnnotation.debug()) {
debugger = this.rhinoContext.createDebugger();
debugger = createDebugger();
}
this.rhinoContext = setUpRhinoScope();
@ -62,6 +63,23 @@ public class JasmineTestRunner extends Runner {
context.evalJS("jasmine.getEnv().addReporter(new jasmine.DelegatorJUnitReporter());");
}
private Main createDebugger() {
Main debugger = new Main("JS Debugger");
debugger.setExitAction(new Runnable() {
public void run() {
System.exit(0);
}
});
debugger.attachTo(ContextFactory.getGlobal());
debugger.pack();
debugger.setSize(600, 460);
debugger.setVisible(true);
return debugger;
}
private JasmineSuite getJasmineSuiteAnnotationFromTestClass() {
JasmineSuite suiteAnnotation = testClass.getAnnotation(JasmineSuite.class);
if (suiteAnnotation == null) {

View File

@ -5,150 +5,132 @@ import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.tools.debugger.Main;
import org.mozilla.javascript.tools.shell.Global;
public class RhinoContext {
private Context jsContext;
private Scriptable jsScope;
private Context jsContext;
private Scriptable jsScope;
public RhinoContext() {
this.jsContext = createJavascriptContext();
this.jsScope = createJavascriptScopeForContext(this.jsContext);
}
public RhinoContext() {
this.jsContext = createJavascriptContext();
this.jsScope = createJavascriptScopeForContext(this.jsContext);
}
public RhinoContext(Scriptable sharedScope) {
this.jsContext = createJavascriptContext();
Scriptable newScope = this.jsContext.newObject(sharedScope);
newScope.setPrototype(sharedScope);
newScope.setParentScope(null);
public RhinoContext(Scriptable sharedScope) {
this.jsContext = createJavascriptContext();
Scriptable newScope = this.jsContext.newObject(sharedScope);
newScope.setPrototype(sharedScope);
newScope.setParentScope(null);
this.jsScope = newScope;
}
this.jsScope = newScope;
}
private RhinoContext createNewRhinoContextBasedOnPrevious() {
return new RhinoContext(this.jsScope);
}
private RhinoContext createNewRhinoContextBasedOnPrevious() {
return new RhinoContext(this.jsScope);
}
public void runAsync(final RhinoRunnable runnable) {
new Thread(new Runnable() {
public void runAsync(final RhinoRunnable runnable) {
new Thread(new Runnable() {
@Override
public void run() {
RhinoContext newRhinoContextBasedOnPrevious = createNewRhinoContextBasedOnPrevious();
try {
runnable.run(newRhinoContextBasedOnPrevious);
} finally {
newRhinoContextBasedOnPrevious.exit();
}
}
}).start();
}
@Override
public void run() {
RhinoContext newRhinoContextBasedOnPrevious = createNewRhinoContextBasedOnPrevious();
try {
runnable.run(newRhinoContextBasedOnPrevious);
} finally {
newRhinoContextBasedOnPrevious.exit();
}
}
}).start();
}
public Object evalJS(String js) {
return this.jsContext.evaluateString(this.jsScope, js, "script", 1, null);
}
public Object evalJS(String js) {
return this.jsContext.evaluateString(this.jsScope, js, "script", 1, null);
}
@SuppressWarnings("unchecked")
public <T extends ScriptableObject> T createClassInJS(Class<T> classToExport) {
exportClass(classToExport);
T newObj = (T) jsContext.newObject(jsScope, classToExport.getSimpleName());
return newObj;
}
@SuppressWarnings("unchecked")
public <T extends ScriptableObject> T createClassInJS(Class<T> classToExport) {
exportClass(classToExport);
T newObj = (T) jsContext.newObject(jsScope, classToExport.getSimpleName());
return newObj;
}
public void setProperty(String objectToReceiveProperty, String property, Object value) {
Object obj = evalJS(objectToReceiveProperty);
if (obj == null || !(obj instanceof ScriptableObject)) {
throw new IllegalStateException("object to receive property is no ScriptableObject but a "
+ (obj == null ? "" : obj.getClass().getSimpleName()));
}
public void setProperty(String objectToReceiveProperty, String property, Object value) {
Object obj = evalJS(objectToReceiveProperty);
if (obj == null || !(obj instanceof ScriptableObject)) {
throw new IllegalStateException("object to receive property is no ScriptableObject but a "
+ (obj == null ? "" : obj.getClass().getSimpleName()));
}
ScriptableObject objectToReceive = (ScriptableObject) obj;
objectToReceive.put(property, objectToReceive, value);
}
ScriptableObject objectToReceive = (ScriptableObject) obj;
objectToReceive.put(property, objectToReceive, value);
}
private void exportClass(Class<? extends ScriptableObject> classToExport) {
try {
ScriptableObject.defineClass(this.jsScope, classToExport);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void exportClass(Class<? extends ScriptableObject> classToExport) {
try {
ScriptableObject.defineClass(this.jsScope, classToExport);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void load(String path, String... jsFiles) {
for (String jsFile : jsFiles) {
load(path + jsFile);
}
}
public void load(String path, String... jsFiles) {
for (String jsFile : jsFiles) {
load(path + jsFile);
}
}
public void load(String fileName) {
evalJS("load('" + fileName + "')");
// Main.processFile(this.jsContext, this.jsScope, fileName);
}
public void load(String fileName) {
evalJS("load('" + fileName + "')");
// Main.processFile(this.jsContext, this.jsScope, fileName);
}
public Object executeFunction(ScriptableObject object, String fnName, Object[] arguments) {
Object fnPointer = object.get(fnName, object);
if (fnPointer == null || !(fnPointer instanceof Function)) {
fnPointer = object.getPrototype().get(fnName, object);
}
public Object executeFunction(ScriptableObject object, String fnName, Object[] arguments) {
Object fnPointer = object.get(fnName, object);
if (fnPointer == null || !(fnPointer instanceof Function)) {
fnPointer = object.getPrototype().get(fnName, object);
}
return ((Function) fnPointer).call(jsContext, jsScope, object, arguments);
}
return ((Function) fnPointer).call(jsContext, jsScope, object, arguments);
}
public Object executeFunction(ScriptableObject object, String fnName) {
return executeFunction(object, fnName, new Object[] {});
}
public Object executeFunction(ScriptableObject object, String fnName) {
return executeFunction(object, fnName, new Object[] {});
}
public Context getJsContext() {
return jsContext;
}
public Context getJsContext() {
return jsContext;
}
public Scriptable getJsScope() {
return jsScope;
}
public Scriptable getJsScope() {
return jsScope;
}
public void loadEnv(String jsDir) {
// TODO ensure rhino 1.7R3 instead of R2 -> geen shim nodig + paths
// gedoe in orde zetten hier
load(jsDir + "/lib/es5-shim-0.0.4.min.js");
load(jsDir + "/lib/env.rhino.1.2.js");
load(jsDir + "/lib/env.utils.js");
load(jsDir + "/envJsOptions.js");
}
public void loadEnv(String jsDir) {
// TODO ensure rhino 1.7R3 instead of R2 -> geen shim nodig + paths
// gedoe in orde zetten hier
load(jsDir + "/lib/es5-shim-0.0.4.min.js");
load(jsDir + "/lib/env.rhino.1.2.js");
load(jsDir + "/lib/env.utils.js");
load(jsDir + "/envJsOptions.js");
}
private Global createJavascriptScopeForContext(Context jsContext) {
Global scope = new Global();
scope.init(jsContext);
return scope;
}
private Global createJavascriptScopeForContext(Context jsContext) {
Global scope = new Global();
scope.init(jsContext);
return scope;
}
private Context createJavascriptContext() {
Context jsContext = ContextFactory.getGlobal().enterContext();
jsContext.setOptimizationLevel(-1);
jsContext.setLanguageVersion(Context.VERSION_1_5); // TODO 1.8 plx
jsContext.setErrorReporter(new ChainedErrorReporter(jsContext.getErrorReporter()));
return jsContext;
}
private Context createJavascriptContext() {
Context jsContext = ContextFactory.getGlobal().enterContext();
jsContext.setOptimizationLevel(-1);
jsContext.setLanguageVersion(Context.VERSION_1_5); // TODO 1.8 plx
jsContext.setErrorReporter(new ChainedErrorReporter(jsContext.getErrorReporter()));
return jsContext;
}
public void exit() {
Context.exit();
}
public Main createDebugger() {
Main debugger = new Main("JS Rhino Debugger");
debugger.setExitAction(new Runnable() {
public void run() {
System.exit(0);
}
});
debugger.attachTo(ContextFactory.getGlobal());
debugger.pack();
debugger.setSize(600, 460);
debugger.setVisible(true);
return debugger;
}
public void exit() {
Context.exit();
}
}

12
src/test/java/be/klak/env/EnvUtilsTest.java vendored Executable file
View File

@ -0,0 +1,12 @@
package be.klak.env;
import org.junit.runner.RunWith;
import be.klak.junit.jasmine.JasmineSuite;
import be.klak.junit.jasmine.JasmineTestRunner;
@RunWith(JasmineTestRunner.class)
@JasmineSuite
public class EnvUtilsTest {
}

View File

@ -16,7 +16,7 @@ public class DescriptionsRecursiveTreeInRunnerTest {
Description baseTestDescription = new JasmineTestRunner(RecursiveTreeTest.class).getDescription();
assertThat(baseTestDescription.getDisplayName()).contains(RecursiveTreeTest.class.getSimpleName());
assertThat(baseTestDescription.getChildren()).hasSize(1);
assertThat(baseTestDescription.getChildren()).hasSize(2);
Description root = baseTestDescription.getChildren().get(0);
assertThat(root.getDisplayName()).isEqualTo("root");
assertThat(root.getChildren()).hasSize(3);
@ -24,6 +24,12 @@ public class DescriptionsRecursiveTreeInRunnerTest {
assertThat(root.getChildren().get(0).getDisplayName()).isEqualTo("rootTest");
assertChild1AndChildren(root);
assertChild2AndChildren(root);
Description root2 = baseTestDescription.getChildren().get(1);
assertThat(root2.getDisplayName()).isEqualTo("root2");
assertThat(root2.getChildren()).hasSize(1);
assertThat(root2.getChildren().get(0).getDisplayName()).isEqualTo("root2Test");
}
private void assertChild2AndChildren(Description root) {

View File

@ -55,6 +55,7 @@
* Envjs specific hacks
* 1) Fix Envjs relative path system to work with Windows path systems
* 2) Fix window.setTimeout() using Rhino specific functions
* 3) Fix CSS2Properties support: all properties have the same objmaps, wtf?
*/
(function() {
@ -73,4 +74,17 @@
});
};
(function(css) {
var setCssProperty = css.prototype.setProperty;
css.prototype.setProperty = function(name, value) {
// create a shallow clone of __supportedStyles__ (styleIndex' default value) if prototype not yet set
if(Object.keys(Object.getPrototypeOf(this.styleIndex)).length === 0) {
this.styleIndex = Object.create(this.styleIndex);
}
return setCssProperty.call(this, name, value);
}
})(CSS2Properties);
})();

View File

@ -0,0 +1,55 @@
describe("envjs fixes", function() {
describe("CSS2 style property support", function() {
var someColor = "#FFFFFF";
var someFont = "12px 'Bitstream Vera Sans Mono','Courier',monospace";
it("should be visible and displayed by default for all new elements", function() {
var elStyle = document.createElement("b").style;
expect(elStyle.display).toBeFalsy();
expect(elStyle.visibility).toBeFalsy();
});
it("should be able to set a style value through setters", function() {
var someB = document.createElement("b");
someB.style.color = someColor;
expect(someB.style.color).toBe(someColor);
});
it("should have unique style values per DOM element", function() {
var someEl1 = document.createElement("b");
var someEl2 = document.createElement("b");
someEl1.style.color = someColor;
someEl2.style.font = someFont;
expect(someEl1.style.font).toBeFalsy();
expect(someEl2.style.color).toBeFalsy();
});
});
describe("window setTimeout", function() {
it("should wait one second before executing", function() {
var done = false;
window.setTimeout(function() {
done = true;
}, 1000);
waitsFor(function() {
return done === true;
});
runs(function() {
expect(done).toBeTruthy();
});
});
});
});

View File

@ -29,4 +29,11 @@ describe("root", function() {
});
});
});
describe("root2", function() {
it("root2Test", function() {
});
});