Pulls in updates to more recent Rhino.
This commit is contained in:
Brian Lalor 2012-11-21 06:34:51 -05:00 committed by Brian Lalor
commit 3fef5237aa
4 changed files with 478 additions and 225 deletions

View File

@ -1,203 +1,214 @@
package be.klak.junit.jasmine; package be.klak.junit.jasmine;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.runner.Description; import org.junit.runner.Description;
import org.junit.runner.Runner; import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier; import org.junit.runner.notification.RunNotifier;
import org.mozilla.javascript.ContextFactory; import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.NativeArray; import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.tools.debugger.Main; import org.mozilla.javascript.tools.debugger.Main;
import be.klak.rhino.RhinoContext; import be.klak.rhino.RhinoContext;
public class JasmineTestRunner extends Runner { public class JasmineTestRunner extends Runner {
private static final int SLEEP_TIME_MILISECONDS = 50; private static final int SLEEP_TIME_MILISECONDS = 50;
private static final String JASMINE_LIB_DIR = "/lib/jasmine-1.0.2/"; private static final String JASMINE_LIB_DIR = "/lib/jasmine-1.0.2/";
private JasmineDescriptions jasmineSuite; private JasmineDescriptions jasmineSuite;
private final RhinoContext rhinoContext; protected final RhinoContext rhinoContext;
private final JasmineSuite suiteAnnotation; protected final JasmineSuite suiteAnnotation;
private final Class<?> testClass; private final Class<?> testClass;
@JasmineSuite @JasmineSuite
private class DefaultSuite { private class DefaultSuite {
} }
public JasmineTestRunner(Class<?> testClass) { public JasmineTestRunner(Class<?> testClass) {
this.testClass = testClass; this.testClass = testClass;
this.suiteAnnotation = getJasmineSuiteAnnotationFromTestClass(); this.suiteAnnotation = getJasmineSuiteAnnotationFromTestClass();
Main debugger = null; Main debugger = null;
if (this.suiteAnnotation.debug()) { if (this.suiteAnnotation.debug()) {
debugger = createDebugger(); debugger = createDebugger();
} }
this.rhinoContext = setUpRhinoScope(); this.rhinoContext = setUpRhinoScope();
if (this.suiteAnnotation.debug()) { if (this.suiteAnnotation.debug()) {
debugger.doBreak(); debugger.doBreak();
} }
} }
private RhinoContext setUpRhinoScope() { private RhinoContext setUpRhinoScope() {
RhinoContext context = new RhinoContext(); RhinoContext context = new RhinoContext();
context.loadEnv(suiteAnnotation.jsRootDir());
setUpJasmine(context); pre(context);
context.load(suiteAnnotation.sourcesRootDir() + "/", suiteAnnotation.sources()); context.loadEnv(suiteAnnotation.jsRootDir());
context.load(suiteAnnotation.jsRootDir() + "/specs/", getJasmineSpecs(suiteAnnotation)); setUpJasmine(context);
return context;
} context.load(suiteAnnotation.sourcesRootDir() + "/", suiteAnnotation.sources());
context.load(suiteAnnotation.jsRootDir() + "/specs/", getJasmineSpecs(suiteAnnotation));
private void setUpJasmine(RhinoContext context) { return context;
context.load(getJsLibDir() + "jasmine.js"); }
context.load(getJsLibDir() + "jasmine.delegator_reporter.js");
protected void pre(RhinoContext context) {
context.evalJS("jasmine.getEnv().addReporter(new jasmine.DelegatorJUnitReporter());"); }
}
private void setUpJasmine(RhinoContext context) {
private Main createDebugger() { context.load(getJsLibDir() + "jasmine.js");
Main debugger = new Main("JS Debugger"); context.load(getJsLibDir() + "jasmine.delegator_reporter.js");
debugger.setExitAction(new Runnable() { context.evalJS("jasmine.getEnv().addReporter(new jasmine.DelegatorJUnitReporter());");
public void run() { }
System.exit(0);
} private Main createDebugger() {
}); Main debugger = new Main("JS Debugger");
debugger.attachTo(ContextFactory.getGlobal()); debugger.setExitAction(new Runnable() {
debugger.pack(); public void run() {
debugger.setSize(600, 460); System.exit(0);
debugger.setVisible(true); }
});
return debugger;
} debugger.attachTo(ContextFactory.getGlobal());
debugger.pack();
private JasmineSuite getJasmineSuiteAnnotationFromTestClass() { debugger.setSize(600, 460);
JasmineSuite suiteAnnotation = testClass.getAnnotation(JasmineSuite.class); debugger.setVisible(true);
if (suiteAnnotation == null) {
suiteAnnotation = DefaultSuite.class.getAnnotation(JasmineSuite.class); return debugger;
} }
return suiteAnnotation;
} private JasmineSuite getJasmineSuiteAnnotationFromTestClass() {
JasmineSuite suiteAnnotation = testClass.getAnnotation(JasmineSuite.class);
private String[] getJasmineSpecs(JasmineSuite suiteAnnotation) { if (suiteAnnotation == null) {
if (suiteAnnotation.specs().length == 0) { suiteAnnotation = DefaultSuite.class.getAnnotation(JasmineSuite.class);
return new String[] { StringUtils.uncapitalize(testClass.getSimpleName()).replace("Test", "Spec") + ".js" }; }
} return suiteAnnotation;
return suiteAnnotation.specs(); }
}
private String[] getJasmineSpecs(JasmineSuite suiteAnnotation) {
private void resetEnvjsWindowSpace() { if (suiteAnnotation.specs().length == 0) {
this.rhinoContext.evalJS("window.location = '" + suiteAnnotation.jsRootDir() + "/lib/blank.html';"); return new String[] { StringUtils.uncapitalize(testClass.getSimpleName()).replace("Test", "Spec") + ".js" };
} }
return suiteAnnotation.specs();
private String getJsLibDir() { }
return suiteAnnotation.jsRootDir() + JASMINE_LIB_DIR;
} private void resetEnvjsWindowSpace() {
this.rhinoContext.evalJS("window.location = '" + suiteAnnotation.jsRootDir() + "/lib/blank.html';");
private JasmineDescriptions getJasmineDescriptions() { }
if (this.jasmineSuite == null) {
NativeArray baseSuites = (NativeArray) rhinoContext.evalJS("jasmine.getEnv().currentRunner().suites()"); private String getJsLibDir() {
this.jasmineSuite = new JasmineJSSuiteConverter(rhinoContext).convertToJunitDescriptions(testClass, baseSuites); return suiteAnnotation.jsRootDir() + JASMINE_LIB_DIR;
} }
return this.jasmineSuite;
} private JasmineDescriptions getJasmineDescriptions() {
if (this.jasmineSuite == null) {
@Override NativeArray baseSuites = (NativeArray) rhinoContext.evalJS("jasmine.getEnv().currentRunner().suites()");
public Description getDescription() { this.jasmineSuite = new JasmineJSSuiteConverter(rhinoContext).convertToJunitDescriptions(testClass, baseSuites);
return getJasmineDescriptions().getRootDescription(); }
} return this.jasmineSuite;
}
@Override
public void run(RunNotifier notifier) { @Override
generateSpecRunnerIfNeeded(); public Description getDescription() {
return getJasmineDescriptions().getRootDescription();
for (JasmineSpec spec : getJasmineDescriptions().getSpecs()) { }
Object testClassInstance = createTestClassInstance();
fireMethodsWithSpecifiedAnnotationIfAny(testClassInstance, Before.class); @Override
public void run(RunNotifier notifier) {
try { generateSpecRunnerIfNeeded();
notifier.fireTestStarted(spec.getDescription());
spec.execute(rhinoContext); for (JasmineSpec spec : getJasmineDescriptions().getSpecs()) {
while (!spec.isDone()) { Object testClassInstance = createTestClassInstance();
waitALittle(); fireMethodsWithSpecifiedAnnotationIfAny(testClassInstance, Before.class);
}
try {
reportSpecResultToNotifier(notifier, spec); notifier.fireTestStarted(spec.getDescription());
resetEnvjsWindowSpace(); spec.execute(rhinoContext);
} finally { while (!spec.isDone()) {
fireMethodsWithSpecifiedAnnotationIfAny(testClassInstance, After.class); waitALittle();
} }
}
reportSpecResultToNotifier(notifier, spec);
this.rhinoContext.exit(); resetEnvjsWindowSpace();
} } finally {
fireMethodsWithSpecifiedAnnotationIfAny(testClassInstance, After.class);
private Object createTestClassInstance() { }
try { }
return testClass.newInstance();
} catch (Exception ex) { after();
throw new RuntimeException("Unable to create a new instance of testClass " + testClass.getSimpleName() }
+ " using a no-arg constructor", ex);
} protected void after() {
} this.rhinoContext.exit();
}
private void fireMethodsWithSpecifiedAnnotationIfAny(Object testClassInstance, Class<? extends Annotation> annotation) {
for (Method method : testClass.getMethods()) {
private Object createTestClassInstance() {
try { try {
if (method.getAnnotation(annotation) != null) { return testClass.newInstance();
method.setAccessible(true); } catch (Exception ex) {
Class<?>[] parameterTypes = method.getParameterTypes(); throw new RuntimeException("Unable to create a new instance of testClass " + testClass.getSimpleName()
if (parameterTypes.length == 0) { + " using a no-arg constructor", ex);
method.invoke(testClassInstance, (Object[]) null); }
} else if (parameterTypes.length == 1 && RhinoContext.class.isAssignableFrom(parameterTypes[0])) { }
method.invoke(testClassInstance, new Object[] { this.rhinoContext });
} else { private void fireMethodsWithSpecifiedAnnotationIfAny(Object testClassInstance, Class<? extends Annotation> annotation) {
throw new IllegalStateException("Annotated method does not have zero or rhinoContext as parameterTypes"); for (Method method : testClass.getMethods()) {
}
} try {
} catch (Exception ex) { if (method.getAnnotation(annotation) != null) {
throw new RuntimeException( method.setAccessible(true);
"Exception while firing " + annotation.getSimpleName() + " method: " + method.getName(), ex); Class<?>[] parameterTypes = method.getParameterTypes();
} if (parameterTypes.length == 0) {
} method.invoke(testClassInstance, (Object[]) null);
} } else if (parameterTypes.length == 1 && RhinoContext.class.isAssignableFrom(parameterTypes[0])) {
method.invoke(testClassInstance, new Object[] { this.rhinoContext });
private void generateSpecRunnerIfNeeded() { } else {
if (suiteAnnotation.generateSpecRunner()) { throw new IllegalStateException("Annotated method does not have zero or rhinoContext as parameterTypes");
String[] jasmineSpecs = getJasmineSpecs(suiteAnnotation); }
new JasmineSpecRunnerGenerator(jasmineSpecs, suiteAnnotation, suiteAnnotation.jsRootDir() + "/runners", }
testClass.getSimpleName() } catch (Exception ex) {
+ "Runner.html") throw new RuntimeException(
.generate(); "Exception while firing " + annotation.getSimpleName() + " method: " + method.getName(), ex);
} }
} }
}
private void reportSpecResultToNotifier(RunNotifier notifier, JasmineSpec spec) {
if (spec.isPassed(rhinoContext)) { private void generateSpecRunnerIfNeeded() {
notifier.fireTestFinished(spec.getDescription()); if (suiteAnnotation.generateSpecRunner()) {
} else if (spec.isFailed(rhinoContext)) { String[] jasmineSpecs = getJasmineSpecs(suiteAnnotation);
notifier.fireTestFailure(spec.getJunitFailure(rhinoContext)); new JasmineSpecRunnerGenerator(jasmineSpecs, suiteAnnotation, suiteAnnotation.jsRootDir() + "/runners",
} else { testClass.getSimpleName()
throw new IllegalStateException("Unexpected spec status received: " + spec); + "Runner.html")
} .generate();
} }
}
private void waitALittle() {
try { private void reportSpecResultToNotifier(RunNotifier notifier, JasmineSpec spec) {
Thread.sleep(SLEEP_TIME_MILISECONDS); if (spec.isPassed(rhinoContext)) {
} catch (Exception e) { notifier.fireTestFinished(spec.getDescription());
throw new RuntimeException(e); } else if (spec.isFailed(rhinoContext)) {
} notifier.fireTestFailure(spec.getJunitFailure(rhinoContext));
} } else {
throw new IllegalStateException("Unexpected spec status received: " + spec);
} }
}
private void waitALittle() {
try {
Thread.sleep(SLEEP_TIME_MILISECONDS);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@ -54,10 +54,11 @@
/** /**
* Envjs specific hacks * Envjs specific hacks
* 1) Fix Envjs relative path system to work with Windows path systems * 1) Fix Envjs relative path system to work with Windows path systems
* 2) Fix window.setTimeout() using Rhino specific functions * 2) Fix CSS2Properties support for parsing style attributes: get from raw node context.
* 3) Fix CSS2Properties support for parsing style attributes: get from raw node context. * 3) Fix CSS2Properties support for setting values: all properties have the same objmaps, wtf?
* 4) Fix CSS2Properties support for setting values: all properties have the same objmaps, wtf? * 4) Fix focus() which sets document.activeElement correctly for jQuery:focus
*/ * 5) Fix Input click() behavior for checkboxes. Warning: jQ's click() <-> DOM's click (checked value too late set)!
**/
(function() { (function() {
var oldEnvjsUriFn = Envjs.uri; var oldEnvjsUriFn = Envjs.uri;
@ -68,13 +69,6 @@
return oldEnvjsUriFn(path, "file:///" + ("" + Envjs.getcwd()).replace(/\\/g, '/') + "/"); return oldEnvjsUriFn(path, "file:///" + ("" + Envjs.getcwd()).replace(/\\/g, '/') + "/");
}; };
window.setTimeout = function(closure, timeout) {
spawn(function() {
java.lang.Thread.sleep(timeout);
closure();
});
};
(function(Element) { (function(Element) {
var style = "style"; var style = "style";
@ -97,6 +91,35 @@
}); });
})(HTMLElement.prototype); })(HTMLElement.prototype);
(function(input) {
var oldClick = input.prototype.click;
input.prototype.click = function() {
if(this.type === "checkbox") {
this.checked = !this.checked;
}
oldClick.apply(this, arguments);
}
})(HTMLInputElement);
(function(Input, Textarea, document) {
var activeElement;
function fixFocusForPrototype(element) {
var originalFocus = element.prototype.focus;
element.prototype.focus = function(element) {
activeElement = this;
originalFocus.apply(this, arguments);
}
}
fixFocusForPrototype(Input);
fixFocusForPrototype(Textarea);
document.__defineGetter__("activeElement", function() {
return activeElement;
});
})(HTMLInputElement, HTMLTextAreaElement, document);
(function(css) { (function(css) {
@ -111,3 +134,58 @@
} }
})(CSS2Properties); })(CSS2Properties);
})(); })();
/**
* Envjs timeout fixes which use native Java code to re-implement setTimeout and setInterval
* also sets clearTimeout & clearInterval on same level.
*/
(function() {
var threadTimeoutPool = {};
window.setTimeout = function(closure, timeout) {
var thread = spawn(function() {
try {
java.lang.Thread.sleep(timeout);
closure();
} catch(e) {
// ignore InterruptedExceptions, is probably due to clearTimeout
if (!(e.javaException instanceof java.lang.InterruptedException)) {
throw(e);
}
}
});
threadTimeoutPool[thread.getId()] = thread;
return thread.getId();
};
window.setInterval = function(closure, timeout) {
var thread = spawn(function() {
try {
while(true) {
java.lang.Thread.sleep(timeout);
closure();
}
} catch(e) {
// ignore InterruptedExceptions, is probably due to clearTimeout
if (!(e.javaException instanceof java.lang.InterruptedException)) {
throw(e);
}
}
});
threadTimeoutPool[thread.getId()] = thread;
return thread.getId();
};
window.clearTimeout = function(threadId) {
if (threadId) {
if(threadTimeoutPool[threadId]) {
threadTimeoutPool[threadId].interrupt();
delete threadTimeoutPool[threadId];
}
}
};
window.clearInterval = window.clearTimeout;
})();

View File

@ -1,6 +1,72 @@
describe("envjs fixes", function() { describe("envjs fixes", function() {
describe("Envjs event handling fixes", function() {
beforeEach(function() {
loadFixtures("formevents.html");
});
describe("focussing events", function() {
it("should set activeElement when focussing an input element", function() {
$("#input").focus();
expect(document.activeElement.id).toBe("input");
expect($(":focus")).toBe("#input");
});
it("should set activeElement when focussing a textarea element", function() {
$("#area").focus();
expect(document.activeElement.id).toBe("area");
expect($(":focus")).toBe("#area");
});
});
describe("form submit events", function() {
it("should be able to catch a formsubmit event", function() {
var submitted = false;
$("#form").submit(function() {
submitted = true;
});
$("#form").submit();
waitsFor(function() {
return submitted === true;
});
runs(function() {
expect(submitted).toBeTruthy();
});
});
});
describe("Checkbox click events", function() {
it("should set the state of the checkbox to checked if not checked when clicked", function() {
$("#checkbox").click();
expect($("#checkbox")).toBeChecked();
});
it("should set the state of the checkbox to unchecked if checked when clicked", function() {
$("#checkbox").attr('checked', true);
$("#checkbox").click();
expect($("#checkbox")).not.toBeChecked();
});
it("should still fire the click event after clicking on a checkbox", function() {
var clicked = false;
$("#checkbox").click(function() {
clicked = true;
});
waitsFor(function() {
return clicked;
});
$("#checkbox").click();
runs(function() {
expect(clicked).toBeTruthy();
});
});
});
});
describe("CSS2 style property support for parsing style attributes", function() { describe("CSS2 style property support for parsing style attributes", function() {
beforeEach(function() { beforeEach(function() {
loadFixtures("styleAttributes.html"); loadFixtures("styleAttributes.html");
@ -52,20 +118,111 @@ describe("envjs fixes", function() {
}); });
describe("window setTimeout", function() { describe("timer based events", function() {
it("should wait one second before executing", function() { describe("setTimeout", function() {
var done = false; it("should wait one second before executing", function() {
window.setTimeout(function() { var done = false;
done = true; window.setTimeout(function() {
}, 1000); done = true;
}, 50);
waitsFor(function() {
return done === true; waitsFor(function() {
return done === true;
});
runs(function() {
expect(done).toBeTruthy();
});
}); });
it("should return a unique timerID when the timeout has been set which can be cancelled", function() {
var done = false;
var timerID = window.setTimeout(function() {
done = true;
}, 10);
var timerID2 = window.setTimeout(function() { }, 10);
window.clearTimeout(timerID);
waits(50);
runs(function() {
expect(typeof(timerID)).toEqual("number");
expect(timerID).not.toEqual(timerID2);
expect(done).toBeFalsy();
});
});
it("should be able to use clearInterval for timeouts", function() {
var done = false;
var timerID = window.setTimeout(function() {
done = true;
}, 10);
window.clearInterval(timerID);
waits(50);
runs(function() {
expect(done).toBeFalsy();
});
});
});
runs(function() { describe("setInterval", function() {
expect(done).toBeTruthy(); it("should call the callback method x times until the interval has been stopped", function() {
var count = 0, storedCount;
var intervalId = window.setInterval(function() {
count++;
}, 20);
waitsFor(function() {
return count > 3;
});
runs(function() {
storedCount = count;
window.clearInterval(intervalId);
});
waits(100);
runs(function() {
expect(storedCount).toEqual(count);
});
});
it("should be able to use setTimeout and setInterval which create unique return IDs", function() {
var id1 = window.setTimeout(function() {}, 10);
var id2 = window.setInterval(function() {}, 10);
waits(50);
this.after(function() {
window.clearInterval(id2);
});
runs(function() {
expect(id1 < id2).toBeTruthy();
});
});
it("should be able to use clearTimeout for intervals", function() {
var count = 0, storedCount;
var intervalId = window.setInterval(function() {
count++;
}, 10);
waitsFor(function() {
return count > 1;
});
runs(function() {
storedCount = count;
window.clearTimeout(intervalId);
});
waits(100);
runs(function() {
expect(storedCount).toEqual(count);
});
}); });
}); });

View File

@ -0,0 +1,7 @@
<form id="form">
<input type="text" id="input" value="text"></input>
<input type="checkbox" id="checkbox"></input>
<textarea id="area"></textarea>
<input type="submit" id="submit"></input>
</form>