diff --git a/src/main/java/be/klak/junit/jasmine/JasmineTestRunner.java b/src/main/java/be/klak/junit/jasmine/JasmineTestRunner.java index a4ab95c..6d846c4 100755 --- a/src/main/java/be/klak/junit/jasmine/JasmineTestRunner.java +++ b/src/main/java/be/klak/junit/jasmine/JasmineTestRunner.java @@ -1,203 +1,214 @@ -package be.klak.junit.jasmine; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; - -import org.apache.commons.lang.StringUtils; -import org.junit.After; -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; - -import be.klak.rhino.RhinoContext; - -public class JasmineTestRunner extends Runner { - - private static final int SLEEP_TIME_MILISECONDS = 50; - private static final String JASMINE_LIB_DIR = "/lib/jasmine-1.0.2/"; - - private JasmineDescriptions jasmineSuite; - - private final RhinoContext rhinoContext; - private final JasmineSuite suiteAnnotation; - private final Class testClass; - - @JasmineSuite - private class DefaultSuite { - } - - public JasmineTestRunner(Class testClass) { - this.testClass = testClass; - this.suiteAnnotation = getJasmineSuiteAnnotationFromTestClass(); - - Main debugger = null; - if (this.suiteAnnotation.debug()) { - debugger = createDebugger(); - } - - this.rhinoContext = setUpRhinoScope(); - - if (this.suiteAnnotation.debug()) { - debugger.doBreak(); - } - } - - private RhinoContext setUpRhinoScope() { - RhinoContext context = new RhinoContext(); - context.loadEnv(suiteAnnotation.jsRootDir()); - setUpJasmine(context); - - context.load(suiteAnnotation.sourcesRootDir() + "/", suiteAnnotation.sources()); - context.load(suiteAnnotation.jsRootDir() + "/specs/", getJasmineSpecs(suiteAnnotation)); - return context; - } - - private void setUpJasmine(RhinoContext context) { - context.load(getJsLibDir() + "jasmine.js"); - context.load(getJsLibDir() + "jasmine.delegator_reporter.js"); - - 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) { - suiteAnnotation = DefaultSuite.class.getAnnotation(JasmineSuite.class); - } - return suiteAnnotation; - } - - private String[] getJasmineSpecs(JasmineSuite suiteAnnotation) { - if (suiteAnnotation.specs().length == 0) { - return new String[] { StringUtils.uncapitalize(testClass.getSimpleName()).replace("Test", "Spec") + ".js" }; - } - return suiteAnnotation.specs(); - } - - private void resetEnvjsWindowSpace() { - this.rhinoContext.evalJS("window.location = '" + suiteAnnotation.jsRootDir() + "/lib/blank.html';"); - } - - private String getJsLibDir() { - return suiteAnnotation.jsRootDir() + JASMINE_LIB_DIR; - } - - private JasmineDescriptions getJasmineDescriptions() { - if (this.jasmineSuite == null) { - NativeArray baseSuites = (NativeArray) rhinoContext.evalJS("jasmine.getEnv().currentRunner().suites()"); - this.jasmineSuite = new JasmineJSSuiteConverter(rhinoContext).convertToJunitDescriptions(testClass, baseSuites); - } - return this.jasmineSuite; - } - - @Override - public Description getDescription() { - return getJasmineDescriptions().getRootDescription(); - } - - @Override - public void run(RunNotifier notifier) { - generateSpecRunnerIfNeeded(); - - for (JasmineSpec spec : getJasmineDescriptions().getSpecs()) { - Object testClassInstance = createTestClassInstance(); - fireMethodsWithSpecifiedAnnotationIfAny(testClassInstance, Before.class); - - try { - notifier.fireTestStarted(spec.getDescription()); - spec.execute(rhinoContext); - while (!spec.isDone()) { - waitALittle(); - } - - reportSpecResultToNotifier(notifier, spec); - resetEnvjsWindowSpace(); - } finally { - fireMethodsWithSpecifiedAnnotationIfAny(testClassInstance, After.class); - } - } - - this.rhinoContext.exit(); - } - - private Object createTestClassInstance() { - try { - return testClass.newInstance(); - } catch (Exception ex) { - throw new RuntimeException("Unable to create a new instance of testClass " + testClass.getSimpleName() - + " using a no-arg constructor", ex); - } - } - - private void fireMethodsWithSpecifiedAnnotationIfAny(Object testClassInstance, Class annotation) { - for (Method method : testClass.getMethods()) { - - try { - if (method.getAnnotation(annotation) != null) { - method.setAccessible(true); - 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 }); - } else { - throw new IllegalStateException("Annotated method does not have zero or rhinoContext as parameterTypes"); - } - } - } catch (Exception ex) { - throw new RuntimeException( - "Exception while firing " + annotation.getSimpleName() + " method: " + method.getName(), ex); - } - } - } - - private void generateSpecRunnerIfNeeded() { - if (suiteAnnotation.generateSpecRunner()) { - String[] jasmineSpecs = getJasmineSpecs(suiteAnnotation); - new JasmineSpecRunnerGenerator(jasmineSpecs, suiteAnnotation, suiteAnnotation.jsRootDir() + "/runners", - testClass.getSimpleName() - + "Runner.html") - .generate(); - } - } - - private void reportSpecResultToNotifier(RunNotifier notifier, JasmineSpec spec) { - if (spec.isPassed(rhinoContext)) { - notifier.fireTestFinished(spec.getDescription()); - } 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); - } - } - -} +package be.klak.junit.jasmine; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.apache.commons.lang.StringUtils; +import org.junit.After; +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; + +import be.klak.rhino.RhinoContext; + +public class JasmineTestRunner extends Runner { + + private static final int SLEEP_TIME_MILISECONDS = 50; + private static final String JASMINE_LIB_DIR = "/lib/jasmine-1.0.2/"; + + private JasmineDescriptions jasmineSuite; + + protected final RhinoContext rhinoContext; + protected final JasmineSuite suiteAnnotation; + private final Class testClass; + + @JasmineSuite + private class DefaultSuite { + } + + public JasmineTestRunner(Class testClass) { + this.testClass = testClass; + this.suiteAnnotation = getJasmineSuiteAnnotationFromTestClass(); + + Main debugger = null; + if (this.suiteAnnotation.debug()) { + debugger = createDebugger(); + } + + this.rhinoContext = setUpRhinoScope(); + + if (this.suiteAnnotation.debug()) { + debugger.doBreak(); + } + } + + private RhinoContext setUpRhinoScope() { + RhinoContext context = new RhinoContext(); + + pre(context); + + context.loadEnv(suiteAnnotation.jsRootDir()); + setUpJasmine(context); + + context.load(suiteAnnotation.sourcesRootDir() + "/", suiteAnnotation.sources()); + context.load(suiteAnnotation.jsRootDir() + "/specs/", getJasmineSpecs(suiteAnnotation)); + return context; + } + + protected void pre(RhinoContext context) { + } + + private void setUpJasmine(RhinoContext context) { + context.load(getJsLibDir() + "jasmine.js"); + context.load(getJsLibDir() + "jasmine.delegator_reporter.js"); + + 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) { + suiteAnnotation = DefaultSuite.class.getAnnotation(JasmineSuite.class); + } + return suiteAnnotation; + } + + private String[] getJasmineSpecs(JasmineSuite suiteAnnotation) { + if (suiteAnnotation.specs().length == 0) { + return new String[] { StringUtils.uncapitalize(testClass.getSimpleName()).replace("Test", "Spec") + ".js" }; + } + return suiteAnnotation.specs(); + } + + private void resetEnvjsWindowSpace() { + this.rhinoContext.evalJS("window.location = '" + suiteAnnotation.jsRootDir() + "/lib/blank.html';"); + } + + private String getJsLibDir() { + return suiteAnnotation.jsRootDir() + JASMINE_LIB_DIR; + } + + private JasmineDescriptions getJasmineDescriptions() { + if (this.jasmineSuite == null) { + NativeArray baseSuites = (NativeArray) rhinoContext.evalJS("jasmine.getEnv().currentRunner().suites()"); + this.jasmineSuite = new JasmineJSSuiteConverter(rhinoContext).convertToJunitDescriptions(testClass, baseSuites); + } + return this.jasmineSuite; + } + + @Override + public Description getDescription() { + return getJasmineDescriptions().getRootDescription(); + } + + @Override + public void run(RunNotifier notifier) { + generateSpecRunnerIfNeeded(); + + for (JasmineSpec spec : getJasmineDescriptions().getSpecs()) { + Object testClassInstance = createTestClassInstance(); + fireMethodsWithSpecifiedAnnotationIfAny(testClassInstance, Before.class); + + try { + notifier.fireTestStarted(spec.getDescription()); + spec.execute(rhinoContext); + while (!spec.isDone()) { + waitALittle(); + } + + reportSpecResultToNotifier(notifier, spec); + resetEnvjsWindowSpace(); + } finally { + fireMethodsWithSpecifiedAnnotationIfAny(testClassInstance, After.class); + } + } + + after(); + } + + protected void after() { + this.rhinoContext.exit(); + } + + + private Object createTestClassInstance() { + try { + return testClass.newInstance(); + } catch (Exception ex) { + throw new RuntimeException("Unable to create a new instance of testClass " + testClass.getSimpleName() + + " using a no-arg constructor", ex); + } + } + + private void fireMethodsWithSpecifiedAnnotationIfAny(Object testClassInstance, Class annotation) { + for (Method method : testClass.getMethods()) { + + try { + if (method.getAnnotation(annotation) != null) { + method.setAccessible(true); + 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 }); + } else { + throw new IllegalStateException("Annotated method does not have zero or rhinoContext as parameterTypes"); + } + } + } catch (Exception ex) { + throw new RuntimeException( + "Exception while firing " + annotation.getSimpleName() + " method: " + method.getName(), ex); + } + } + } + + private void generateSpecRunnerIfNeeded() { + if (suiteAnnotation.generateSpecRunner()) { + String[] jasmineSpecs = getJasmineSpecs(suiteAnnotation); + new JasmineSpecRunnerGenerator(jasmineSpecs, suiteAnnotation, suiteAnnotation.jsRootDir() + "/runners", + testClass.getSimpleName() + + "Runner.html") + .generate(); + } + } + + private void reportSpecResultToNotifier(RunNotifier notifier, JasmineSpec spec) { + if (spec.isPassed(rhinoContext)) { + notifier.fireTestFinished(spec.getDescription()); + } 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); + } + } + +} diff --git a/src/test/javascript/lib/env.utils.js b/src/test/javascript/lib/env.utils.js index f8735f4..4d3fef1 100755 --- a/src/test/javascript/lib/env.utils.js +++ b/src/test/javascript/lib/env.utils.js @@ -54,10 +54,11 @@ /** * 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 for parsing style attributes: get from raw node context. - * 4) Fix CSS2Properties support for setting values: all properties have the same objmaps, wtf? - */ + * 2) 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 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() { var oldEnvjsUriFn = Envjs.uri; @@ -68,13 +69,6 @@ return oldEnvjsUriFn(path, "file:///" + ("" + Envjs.getcwd()).replace(/\\/g, '/') + "/"); }; - window.setTimeout = function(closure, timeout) { - spawn(function() { - java.lang.Thread.sleep(timeout); - closure(); - }); - }; - (function(Element) { var style = "style"; @@ -97,6 +91,35 @@ }); })(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) { @@ -111,3 +134,58 @@ } })(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; +})(); diff --git a/src/test/javascript/specs/envUtilsSpec.js b/src/test/javascript/specs/envUtilsSpec.js index 13ddb6e..35d02bc 100755 --- a/src/test/javascript/specs/envUtilsSpec.js +++ b/src/test/javascript/specs/envUtilsSpec.js @@ -1,6 +1,72 @@ 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() { beforeEach(function() { 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() { - var done = false; - window.setTimeout(function() { - done = true; - }, 1000); - - waitsFor(function() { - return done === true; + describe("setTimeout", function() { + it("should wait one second before executing", function() { + var done = false; + window.setTimeout(function() { + done = true; + }, 50); + + 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() { - expect(done).toBeTruthy(); + describe("setInterval", function() { + 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); + }); }); }); diff --git a/src/test/javascript/specs/fixtures/formevents.html b/src/test/javascript/specs/fixtures/formevents.html new file mode 100755 index 0000000..79a9adf --- /dev/null +++ b/src/test/javascript/specs/fixtures/formevents.html @@ -0,0 +1,7 @@ + +
+ + + + +