// Copyright (C) 2007 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.caja.util;
import com.google.caja.SomethingWidgyHappenedError;
import com.google.caja.lexer.CharProducer;
import com.google.caja.lexer.FilePosition;
import com.google.caja.lexer.InputSource;
import com.google.caja.lexer.ParseException;
import com.google.caja.parser.html.Namespaces;
import com.google.caja.parser.html.Nodes;
import com.google.caja.parser.js.StringLiteral;
import com.google.caja.reporting.MessageContext;
import com.google.caja.util.Executor.AbnormalExitException;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import junit.framework.Assert;
import junit.framework.AssertionFailedError;
import org.w3c.dom.Attr;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
/**
* A testbed that allows running javascript via the Rhino interpreter.
* TODO(mikesamuel): maybe replace this with the JSR 223 stuff.
*
* @author mikesamuel@gmail.com
*/
// TODO(felix8a): remove SuppressWarnings after full conversion to junit4
@SuppressWarnings("deprecation")
public class RhinoTestBed {
private static final String HTML_NS = Namespaces.HTML_NAMESPACE_URI;
/**
* Runs the javascript from the given inputs in order, and returns the
* result.
*/
public static Object runJs(Executor.Input... inputs) {
return runJs(null, inputs);
}
public static Object runJs(Object eval, Executor.Input... inputs) {
try {
Map<String, Object> actuals = Maps.newHashMap();
actuals.put("stderr", System.err);
actuals.put("_junit_", new JunitSandBoxSafe());
actuals.put("caja___", eval);
RhinoExecutor exec = new RhinoExecutor(inputs);
return exec.run(actuals, Object.class);
} catch (AbnormalExitException ex) {
Throwable th = ex.getCause();
if (th instanceof Error) { throw (Error) th; }
if (th instanceof RuntimeException) { throw (RuntimeException) th; }
throw new SomethingWidgyHappenedError(ex);
}
}
/**
* Given an HTML file that references javascript sources, load all
* the scripts, set up the DOM using env.js, and start JSUnit.
*
* <p>This lets us write test html files that can be run both
* in a browser, and automatically via ANT.
*
* <p>NOTE: This method interprets the input HTML in an idiosyncratic way to
* facilitate conveniently bundling test code into one file. It runs each
* {@code <script>} block as plain JavaScript.
*
* @param html an HTML DOM tree to run in Rhino.
*/
public static void runJsUnittestFromHtml(DocumentFragment html)
throws IOException, ParseException {
TestUtil.enableContentUrls(); // Used to get HTML to env.js
List<Executor.Input> inputs = Lists.newArrayList();
// Stub out the Browser
inputs.add(new Executor.Input(
RhinoTestBed.class, "../plugin/console-stubs.js"));
inputs.add(new Executor.Input(
RhinoTestBed.class, "/js/envjs/env.js"));
int injectHtmlIndex = inputs.size();
List<Pair<String, InputSource>> scriptContent
= new ArrayList<Pair<String, InputSource>>();
MessageContext mc = new MessageContext();
List<Element> scripts = new ArrayList<Element>();
for (Node root : Nodes.childrenOf(html)) {
if (root.getNodeType() == 1) {
for (Element script : Nodes.nodeListIterable(
((Element) root).getElementsByTagNameNS(HTML_NS, "script"),
Element.class)) {
scripts.add(script);
}
}
}
for (Element script : scripts) {
Attr src = script.getAttributeNodeNS(HTML_NS, "src");
CharProducer scriptBody;
if (src != null) {
String resourcePath = src.getNodeValue();
InputSource resource;
if (resourcePath.startsWith("/")) {
try {
resource = new InputSource(
RhinoTestBed.class.getResource(resourcePath).toURI());
} catch (URISyntaxException ex) {
throw new SomethingWidgyHappenedError(
"java.net.URL is not a valid java.net.URI", ex);
}
} else {
InputSource baseUri = Nodes.getFilePositionFor(html).source();
resource = new InputSource(baseUri.getUri().resolve(resourcePath));
}
scriptBody = loadResource(resource);
} else {
scriptBody = textContentOf(script);
}
String scriptText;
InputSource isrc = scriptBody.getSourceBreaks(0).source();
// Add blank lines at the front so that Rhino stack traces have correct
// line numbers.
scriptText = prefixWithBlankLines(
scriptBody.toString(0, scriptBody.getLimit()),
Nodes.getFilePositionFor(script).startLineNo() - 1);
scriptContent.add(Pair.pair(scriptText, isrc));
mc.addInputSource(isrc);
script.getParentNode().removeChild(script);
}
for (Pair<String, InputSource> script : scriptContent) {
inputs.add(new Executor.Input(script.a, mc.abbreviate(script.b)));
}
// Set up the DOM. env.js requires that location be set to a URI before it
// creates a DOM. Since it fetches HTML via java.net.URL and passes it off
// to the org.w3c parser, we use a content: URL which is handled by handlers
// registered in TestUtil so that we can provide html without having a file
// handy.
String domJs = "window.location = "
+ StringLiteral.toQuotedValue(
TestUtil.makeContentUrl(Nodes.render(html)))
+ ";";
String htmlSource = Nodes.getFilePositionFor(html).source().toString();
inputs.add(injectHtmlIndex, new Executor.Input(domJs, htmlSource));
inputs.add(new Executor.Input(
"(function () {\n"
+ " var onload = document.body.getAttribute('onload');\n"
+ " onload && eval(onload);\n"
+ " return document.title;\n"
+ " })();", htmlSource));
// Execute tests
String title = (String)
runJs(inputs.toArray(new Executor.Input[inputs.size()]));
// Test for success
if (title == null || !title.contains("all tests passed")) {
throw new junit.framework.AssertionFailedError(
"Rhino tests did not pass; title = " + title);
}
}
private static CharProducer loadResource(InputSource resource)
throws IOException {
File f = new File(resource.getUri());
return CharProducer.Factory.fromFile(f, Charsets.UTF_8);
}
private static String prefixWithBlankLines(String s, int n) {
if (n <= 0) { return s; }
StringBuilder sb = new StringBuilder(s.length() + n);
while (--n >= 0) { sb.append('\n'); }
return sb.append(s).toString();
}
private static CharProducer textContentOf(Element script) {
List<CharProducer> parts = new ArrayList<CharProducer>();
for (Node child : Nodes.childrenOf(script)) {
FilePosition childPos = Nodes.getFilePositionFor(child);
switch (child.getNodeType()) {
case Node.TEXT_NODE:
String rawText = Nodes.getRawText((Text) child);
String decodedText = child.getNodeValue();
CharProducer cp = null;
if (rawText != null) {
cp = CharProducer.Factory.fromHtmlAttribute(
CharProducer.Factory.create(
new StringReader(rawText), childPos));
if (!String.valueOf(cp.getBuffer(), cp.getOffset(), cp.getLength())
.equals(decodedText)) { // XHTML
cp = null;
}
}
if (cp == null) {
cp = CharProducer.Factory.create(
new StringReader(child.getNodeValue()), childPos);
}
parts.add(cp);
break;
case Node.CDATA_SECTION_NODE:
parts.add(CharProducer.Factory.create(
new StringReader(" " + child.getNodeValue()), childPos));
break;
default: break;
}
}
return CharProducer.Factory.chain(parts.toArray(new CharProducer[0]));
}
@SuppressWarnings("static-method")
public static final class JunitSandBoxSafe {
public void fail(Object message) {
Assert.fail("" + message);
}
public void fail() {
Assert.fail();
}
public boolean isAssertionFailedError(Object o) {
return o instanceof AssertionFailedError;
}
public void assertJsonEquals(String message, String x, String y) {
Assert.assertEquals(message, renderPrettyJson(x), renderPrettyJson(y));
}
private String renderPrettyJson(String s) {
try {
CajaTestCase t = new CajaTestCase() { { this.setUp(); } };
return
CajaTestCase.render(
t.js(
t.fromString(
"(" + s + ");",
FilePosition.UNKNOWN)));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
private RhinoTestBed() { /* uninstantiable */ }
}