package xapi.gwt.junit.gui;
import elemental.client.Browser;
import elemental.dom.Element;
import xapi.elemental.X_Elemental;
import xapi.elemental.api.PotentialNode;
import xapi.gwt.junit.impl.JUnit4Executor;
import xapi.util.X_Debug;
import com.google.gwt.core.client.Callback;
import com.google.gwt.reflect.client.ConstPool;
import com.google.gwt.reflect.shared.JsMemberPool;
import com.google.gwt.reflect.shared.ReflectUtil;
import javax.inject.Provider;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Created by james on 16/10/15.
*/
public abstract class JUnitGui {
private static final String TEST_RESULTS = "test.result";
private final Map<Class<?>, Object> tests = new LinkedHashMap<>();
private final Map<Class<?>, Method[]> testClasses = new LinkedHashMap<>();
Map<Class<?>, Map<Method, Throwable>> testResults = new LinkedHashMap<>();
/**
* Performs a very primitive test that reflection works.
* We need it to pull methods off classes and execute them. :-)
*/
protected void sanityCheck() {
try {
String.class.getMethod("equals", Object.class).invoke("!", "!");
} catch (final Exception e) {
print(
"Basic string reflection not working; expect failures...", e
);
}
}
protected void runAllTests() {
sanityCheck();
if (!loadWholeWorld()) {
loadTests(true);
} else {
loadTests(false);
ConstPool.loadConstPool(
new Callback<ConstPool, Throwable>() {
@Override
public void onSuccess(final ConstPool result) {
for (final JsMemberPool<?> m : result.getAllReflectionData()) {
try {
final Class<?> c = m.getType();
if (!testClasses.containsKey(c)) {
addTests(c);
}
} catch (final Throwable e) {
print("Error adding tests", e);
}
}
loadTests(true);
}
@Override
public void onFailure(final Throwable caught) {
print("Error loading ConstPool", caught);
}
}
);
}
}
protected boolean loadWholeWorld() {
// override this method to skip loading the ConstPool full of reflection data.
return "wholeWorld".equals(System.getProperty("gwt.test.wholeWorld", "wholeWorld"));
}
private void loadTests(final boolean forReal) {
if (forReal) {
loadAllTests();
displayTests();
runTests();
}
}
/**
* @return <pre>
* new Class[]{
* GwtReflect.magicClass(MyTestClass.class),
* GwtReflect.magicClass(MyOtherClass.class),
* };
* </pre>
*/
protected abstract Class[] testClasses();
protected void loadAllTests() {
// A hook for subclasses to add arbitrary test cases
for (Class<?> c : testClasses()) {
try {
addTests(c);
} catch (Throwable e) {
print("Failure loading test class " + c, e);
}
}
}
protected void addTests(Class<?> c) throws Throwable {
final Method[] allTests = findTestMethods(c);
addTests(c, allTests);
}
protected Method[] findTestMethods(Class<?> c) throws Throwable {
return JUnit4Executor.findTests(c);
}
public void addTests(final Class<?> cls, Method[] allTests) throws Throwable {
if (allTests.length > 0) {
testClasses.putIfAbsent(cls, allTests);
// TODO verify that it is correct to only instantiate one instance and share it across methods.
final Object inst = instantiate(cls);
tests.put(cls, inst);
}
}
protected Object instantiate(Class<?> cls) {
try {
return cls.newInstance();
} catch (Exception e) {
throw X_Debug.rethrow(e);
}
}
protected void displayTests() {
final Element body = getInsertionPoint();
for (final Class<?> c : testClasses.keySet()) {
renderTest(body, c);
}
}
private Element getInsertionPoint() {
return Browser.getDocument().getBody();
}
private void renderTest(Element body, Class<?> c) {
PotentialNode<Element> out = newClassBlock(c);
out.setClass("junit root");
final String id = toId(c);
buildHeader(out, c, id);
try {
final String path = c.getProtectionDomain().getCodeSource().getLocation().getPath();
out.append("<sup><a href='file://" + path + "'>")
.append(path)
.append("</a></s.setInnerHTML(b.toString());up>");
} catch (final Exception ignored) {}
for (final Method m : testClasses.get(c)) {
renderMethod(out, m);
}
appendGui(out);
}
protected void appendGui(PotentialNode<Element> out) {
getInsertionPoint().appendChild(out.getElement());
}
private void renderMethod(PotentialNode<Element> out, Method m) {
Class<?> c = m.getDeclaringClass();
final String methodId = m.getName() + c.hashCode();
final PotentialNode<Element> block = out.createChild("div");
final PotentialNode<Element> link = block
.createChild("a");
link
.setHref("javascript:")
.append(m.getName());
block.append("(")
.append(ReflectUtil.joinClasses(", ", m.getParameterTypes()))
.append(")");
block.createChild("div").setId(methodId).append(" ");
link.onCreated(
el ->
el.addEventListener(
"click",
e -> runTest(m), false
)
);
}
protected PotentialNode<Element> newClassBlock(Class<?> c) {
return new PotentialNode<>("div", true);
}
protected void buildHeader(PotentialNode<Element> b, Class<?> c, String id) {
b.createChild("h3")
.createChild("a")
.setClass("junit")
.setAttribute("id", id)
.setAttribute("href", "#run:" + id)
.onCreated(
a -> a.addEventListener(
"click", e -> {
runTests(c);
}, false
)
)
.append(c.getName())
;
b.createChild("div")
.setClass("junit results")
.setAttribute("id", TEST_RESULTS + id)
;
}
private native void log(Object o)
/*-{
$wnd.console && $wnd.console.log(o);
}-*/;
private String toId(final Class<?> c) {
return c.getName().replace('.', '_');
}
public void runTests() {
testResults.clear();
for (final Class<?> cls : tests.keySet()) {
Map<Method, Throwable> results = testResults.get(cls);
if (results == null) {
results = new LinkedHashMap<>();
testResults.put(cls, results);
}
results.clear();
runTests(cls);
}
testResults.keySet().forEach(this::updateTestClass);
}
public void runTests(Class<?> c) {
final Map<Method, Throwable> res = testResults.get(c);
for (final Method m : res.keySet().toArray(new Method[res.size()])) {
res.put(m, null);
}
updateTestClass(c);
final Object inst = instantiate(c);
tests.put(c, inst);
JUnitGuiController controller;
if (inst instanceof JUnitGuiController) {
controller = (JUnitGuiController) inst;
} else {
controller = new JUnitGuiController(()->updateTestClass(c));
}
Element[] view = new Element[0];
final Provider<Element> stageProvider = () -> {
view[0] = X_Elemental.newDiv();
final Element result = initialize(view[0], controller, inst);
return result;
};
if (controller.onTestClassStart(stageProvider, inst)) {
controller.runAll(
c, inst, fin -> {
res.putAll(fin);
updateTestClass(c);
controller.onTestClassFinish(inst, fin);
}
);
}
}
protected Element elementForClass(Class<?> cls) {
final String id = toId(cls);
return Browser.getDocument().getElementById(TEST_RESULTS + id);
}
private void updateTestClass(final Class<?> cls) {
final Element el = elementForClass(cls);
final Map<Method, Throwable> results = testResults.get(cls);
int success = 0, fail = 0;
final int total = results.size();
for (final Map.Entry<Method, Throwable> e : results.entrySet()) {
if (e.getValue() == JUnit4Executor.SUCCESS) {
success++;
} else {
fail++;
}
}
final StringBuilder b = new StringBuilder("<span class='junit success'>Passed: ")
.append(success).append("/").append(total).append("</span>; ")
.append("<span");
if (fail > 0) {
b.append(" class='junit fail'");
}
b.append(">Failed: ").append(fail).append("/").append(total);
el.setInnerHTML(b.toString());
}
protected void runTest(final Method m) {
final String id = m.getName() + m.getDeclaringClass().hashCode();
final Element stage = Browser.getDocument().getElementById(id);
stage.setInnerHTML("");
final Map<Method, Throwable> results = testResults.get(m.getDeclaringClass());
try {
final Object inst = tests.get(m.getDeclaringClass());
JUnitGuiController controller;
if (inst instanceof JUnitGuiController) {
controller = (JUnitGuiController) inst;
} else {
controller = new JUnitGuiController(()->updateTestClass(m.getDeclaringClass()));
}
Element[] view = new Element[0];
final Provider<Element> stageProvider = () -> {
view[0] = X_Elemental.newDiv();
final Element result = initialize(view[0], controller, inst);
if (result.getParentElement() == null) {
stage.appendChild(result);
}
return result;
};
if (controller.onTestClassStart(stageProvider, inst)) {
controller.run(
inst, m, e -> {
e = e == null ? JUnit4Executor.SUCCESS : e;
results.put(m, e);
updateTestClass(m.getDeclaringClass());
}
);
}
} catch (Throwable e) {
results.put(m, e);
stage.setInnerHTML(debug("<div class='junit' style='color:red'>" + m.getName() + " fails!</div>", e));
}
}
/**
* This method is here to let subclasses pick what element, if any,
* that we want to render for our execution.
*/
protected Element initialize(
Element element,
JUnitGuiController controller,
Object inst
) {
return element == null ? X_Elemental.newDiv() : element;
}
protected void print(final Object string, final Throwable e) {
final Element el = Browser.getDocument().createDivElement();
el.setInnerHTML(debug(string, e));
insertionPoint().appendChild(el);
}
protected Element insertionPoint() {
return Browser.getDocument().getBody();
}
protected String debug(final Object message, Throwable e) {
String debug = JUnit4Executor.debug(message, e);
log(message);
log(e);
return debug;
}
}