/*
* Copyright (c) 2013, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html
*
* 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.dart.engine.internal.html.angular;
import com.google.dart.engine.EngineTestCase;
import com.google.dart.engine.ast.AstNode;
import com.google.dart.engine.ast.CompilationUnit;
import com.google.dart.engine.ast.Expression;
import com.google.dart.engine.ast.SimpleIdentifier;
import com.google.dart.engine.context.AnalysisContext;
import com.google.dart.engine.context.AnalysisContextHelper;
import com.google.dart.engine.context.AnalysisErrorInfo;
import com.google.dart.engine.context.AnalysisException;
import com.google.dart.engine.element.CompilationUnitElement;
import com.google.dart.engine.element.Element;
import com.google.dart.engine.element.ElementKind;
import com.google.dart.engine.element.HtmlElement;
import com.google.dart.engine.element.visitor.GeneralizingElementVisitor;
import com.google.dart.engine.error.AnalysisError;
import com.google.dart.engine.error.ErrorCode;
import com.google.dart.engine.error.GatheringErrorListener;
import com.google.dart.engine.html.ast.HtmlUnit;
import com.google.dart.engine.html.ast.HtmlUnitUtils;
import com.google.dart.engine.internal.resolver.ResolutionVerifier;
import com.google.dart.engine.source.Source;
import com.google.dart.engine.type.Type;
import junit.framework.AssertionFailedError;
import static org.fest.assertions.Assertions.assertThat;
abstract public class AngularTest extends EngineTestCase {
/**
* Creates an HTML content that has Angular marker and script with "main.dart" reference.
*/
protected static String createHtmlWithAngular(String... lines) {
String source = createSource(//
"<html ng-app>",
" <body>");
source += createSource(lines);
source += createSource(//
" <script type='application/dart' src='main.dart'></script>",
" </body>",
"</html>");
return source;
}
/**
* Creates an HTML content that has Angular marker, script with "main.dart" reference and
* "MyController" injected.
*/
protected static String createHtmlWithMyController(String... lines) {
String source = createSource(//
"<html ng-app>",
" <body>",
" <div my-controller>");
source += createSource(lines);
source += createSource(//
" </div>",
" <script type='application/dart' src='main.dart'></script>",
" </body>",
"</html>");
return source;
}
/**
* Finds an {@link Element} with the given names inside of the given root {@link Element}.
* <p>
* TODO(scheglov) maybe move this method to Element
*
* @param root the root {@link Element} to start searching from
* @param kind the kind of the {@link Element} to find, if {@code null} then any kind
* @param name the name of an {@link Element} to find
* @return the found {@link Element} or {@code null} if not found
*/
@SuppressWarnings("unchecked")
protected static <T extends Element> T findElement(Element root, final ElementKind kind,
final String name) {
final Element[] result = {null};
root.accept(new GeneralizingElementVisitor<Void>() {
@Override
public Void visitElement(Element element) {
if ((kind == null || element.getKind() == kind) && name.equals(element.getName())) {
result[0] = element;
}
return super.visitElement(element);
}
});
return (T) result[0];
}
/**
* Finds an {@link Element} with the given names inside of the given root {@link Element}.
*
* @param root the root {@link Element} to start searching from
* @param name the name of an {@link Element} to find
* @return the found {@link Element} or {@code null} if not found
*/
protected static <T extends Element> T findElement(Element root, String name) {
return findElement(root, null, name);
}
/**
* @return the offset of given <code>search</code> string in <code>content</code>. Fails test if
* not found.
*/
protected static int findOffset(String content, String search) {
int offset = content.indexOf(search);
assertThat(offset).describedAs(content).isNotEqualTo(-1);
return offset;
}
protected AnalysisContextHelper contextHelper = new AnalysisContextHelper();
protected AnalysisContext context;
protected String mainContent;
protected Source mainSource;
protected CompilationUnit mainUnit;
protected CompilationUnitElement mainUnitElement;
protected String indexContent;
protected Source indexSource;
protected HtmlUnit indexUnit;
protected HtmlElement indexHtmlUnit;
protected CompilationUnitElement indexDartUnitElement;
/**
* Fills {@link #indexContent} and {@link #indexSource}.
*/
protected final void addIndexSource(String content) {
addIndexSource("/index.html", content);
}
/**
* Fills {@link #indexContent} and {@link #indexSource}.
*/
protected final void addIndexSource(String name, String content) {
indexContent = content;
indexSource = contextHelper.addSource(name, indexContent);
}
/**
* Fills {@link #mainContent} and {@link #mainSource}.
*/
protected final void addMainSource(String content) {
mainContent = content;
mainSource = contextHelper.addSource("/main.dart", content);
}
protected final void addMyController() throws Exception {
resolveMainSource(createSource("",//
"import 'angular.dart';",
"",
"class Item {",
" String name;",
" bool done;",
"}",
"",
"@Controller(",
" selector: '[my-controller]',",
" publishAs: 'ctrl')",
"class MyController {",
" String field;",
" List<String> names;",
" List<Item> items;",
" var untypedItems;",
" doSomething(event) {}",
"}"));
}
/**
* Assert that the number of errors reported against the given source matches the number of errors
* that are given and that they have the expected error codes. The order in which the errors were
* gathered is ignored.
*
* @param source the source against which the errors should have been reported
* @param expectedErrorCodes the error codes of the errors that should have been reported
* @throws AnalysisException if the reported errors could not be computed
* @throws AssertionFailedError if a different number of errors have been reported than were
* expected
*/
protected final void assertErrors(Source source, ErrorCode... expectedErrorCodes)
throws AnalysisException {
GatheringErrorListener errorListener = new GatheringErrorListener();
AnalysisErrorInfo errorsInfo = context.getErrors(source);
for (AnalysisError error : errorsInfo.getErrors()) {
errorListener.onError(error);
}
errorListener.assertErrorsWithCodes(expectedErrorCodes);
}
protected final void assertMainErrors(ErrorCode... expectedErrorCodes) throws AnalysisException {
assertErrors(mainSource, expectedErrorCodes);
}
/**
* Assert that no errors have been reported against the {@link #indexSource}.
*/
protected final void assertNoErrors() throws AnalysisException {
assertErrors(indexSource);
}
protected final void assertNoErrors(Source source) throws AnalysisException {
assertErrors(source);
}
/**
* Assert that no errors have been reported against the {@link #mainSource}.
*/
protected final void assertNoMainErrors() throws AnalysisException {
assertErrors(mainSource);
}
/**
* Checks that {@link #indexHtmlUnit} has {@link SimpleIdentifier} with given name, resolved to
* not {@code null} {@link Element}.
*/
protected final Element assertResolvedIdentifier(String name) {
SimpleIdentifier identifier = findIdentifier(name);
// check Element
Element element = identifier.getBestElement();
assertNotNull(element);
// return Element for further analysis
return element;
}
protected final Element assertResolvedIdentifier(String name, String expectedTypeName) {
SimpleIdentifier identifier = findIdentifier(name);
// check Element
Element element = identifier.getBestElement();
assertNotNull(element);
// check Type
Type type = identifier.getBestType();
assertNotNull(type);
assertEquals(expectedTypeName, type.toString());
// return Element for further analysis
return element;
}
/**
* @return {@link AstNode} which has required offset and type.
*/
protected final <E extends AstNode> E findExpression(int offset, Class<E> clazz) {
Expression expression = HtmlUnitUtils.getExpression(indexUnit, offset);
return expression != null ? expression.getAncestor(clazz) : null;
}
/**
* Returns the {@link SimpleIdentifier} at the given search pattern. Fails if not found.
*/
protected final SimpleIdentifier findIdentifier(String search) {
SimpleIdentifier identifier = findIdentifierMaybe(search);
assertNotNull(search + " in " + indexContent, identifier);
// check that offset/length of the identifier is valid
{
int offset = identifier.getOffset();
int end = identifier.getEnd();
String contentStr = indexContent.substring(offset, end);
assertEquals(identifier.getName(), contentStr);
}
// done
return identifier;
}
/**
* Returns the {@link SimpleIdentifier} at the given search pattern, or {@code null} if not found.
*/
protected final SimpleIdentifier findIdentifierMaybe(String search) {
return findExpression(findOffset(search), SimpleIdentifier.class);
}
/**
* Returns {@link Element} from {@link #indexDartUnitElement}.
*/
protected final <T extends Element> T findIndexElement(String name) throws AnalysisException {
return findElement(indexDartUnitElement, name);
}
/**
* Returns {@link Element} from {@link #mainUnitElement}.
*/
protected final <T extends Element> T findMainElement(ElementKind kind, String name)
throws AnalysisException {
return findElement(mainUnitElement, kind, name);
}
/**
* Returns {@link Element} from {@link #mainUnitElement}.
*/
protected final <T extends Element> T findMainElement(String name) throws AnalysisException {
return findElement(mainUnitElement, name);
}
/**
* @return the offset of given <code>search</code> string in {@link #mainContent}. Fails test if
* not found.
*/
protected final int findMainOffset(String search) {
return findOffset(mainContent, search);
}
/**
* @return the offset of given <code>search</code> string in {@link #indexContent}. Fails test if
* not found.
*/
protected final int findOffset(String search) {
return findOffset(indexContent, search);
}
/**
* Resolves {@link #indexSource}.
*/
protected final void resolveIndex() throws AnalysisException {
indexUnit = context.resolveHtmlUnit(indexSource);
indexHtmlUnit = indexUnit.getElement();
indexDartUnitElement = indexHtmlUnit.getAngularCompilationUnit();
}
protected final void resolveIndex(String content) throws Exception {
addIndexSource(content);
contextHelper.runTasks();
resolveIndex();
}
/**
* Resolves {@link #mainSource}.
*/
protected final void resolveMain() throws Exception {
mainUnit = contextHelper.resolveDefiningUnit(mainSource);
mainUnitElement = mainUnit.getElement();
}
/**
* Resolves {@link #mainSource}.
*/
protected final void resolveMainNoErrors() throws Exception {
resolveMain();
assertNoErrors(mainSource);
}
protected final void resolveMainSource(String content) throws Exception {
addMainSource(content);
resolveMain();
}
protected final void resolveMainSourceNoErrors(String content) throws Exception {
resolveMainSource(content);
assertNoErrors(mainSource);
}
@Override
protected void setUp() throws Exception {
super.setUp();
configureForAngular(contextHelper);
context = contextHelper.context;
}
@Override
protected void tearDown() throws Exception {
contextHelper = null;
context = null;
// main
mainContent = null;
mainSource = null;
mainUnit = null;
mainUnitElement = null;
// index
indexContent = null;
indexSource = null;
indexUnit = null;
indexHtmlUnit = null;
indexDartUnitElement = null;
// super
super.tearDown();
}
/**
* Verify that all of the identifiers in the HTML units associated with the given sources have
* been resolved.
*
* @param sources the sources identifying the compilation units to be verified
* @throws Exception if the contents of the compilation unit cannot be accessed
*/
protected final void verify(Source... sources) throws Exception {
final ResolutionVerifier verifier = new ResolutionVerifier();
for (Source source : sources) {
HtmlUnit htmlUnit = context.getResolvedHtmlUnit(source);
htmlUnit.accept(new ExpressionVisitor() {
@Override
public void visitExpression(Expression expression) {
expression.accept(verifier);
}
});
}
verifier.assertResolved();
}
private void configureForAngular(AnalysisContextHelper contextHelper) {
contextHelper.addSource(
"/angular.dart",
createSource(
"library angular;",
"",
"class Scope {",
" Map context;",
"}",
"",
"class Formatter {",
" final String name;",
" const Formatter({this.name});",
"}",
"",
"class Directive {",
" const Directive({",
" selector,",
" children,",
" visibility,",
" module,",
" map,",
" exportedExpressions,",
" exportedExpressionAttrs",
" });",
"}",
"",
"class Decorator {",
" const Decorator({",
" children/*: Directive.COMPILE_CHILDREN*/,",
" map,",
" selector,",
" module,",
" visibility,",
" exportedExpressions,",
" exportedExpressionAttrs",
" });",
"}",
"",
"class Controller {",
" const Controller({",
" children,",
" publishAs,",
" map,",
" selector,",
" visibility,",
" publishTypes,",
" exportedExpressions,",
" exportedExpressionAttrs",
" });",
"}",
"",
"class NgAttr {",
" const NgAttr(String name);",
"}",
"class NgCallback {",
" const NgCallback(String name);",
"}",
"class NgOneWay {",
" const NgOneWay(String name);",
"}",
"class NgOneWayOneTime {",
" const NgOneWayOneTime(String name);",
"}",
"class NgTwoWay {",
" const NgTwoWay(String name);",
"}",
"",
"class Component extends Directive {",
" const Component({",
" this.template,",
" this.templateUrl,",
" this.cssUrl,",
" this.applyAuthorStyles,",
" this.resetStyleInheritance,",
" publishAs,",
" module,",
" map,",
" selector,",
" visibility,",
" exportExpressions,",
" exportExpressionAttrs",
" }) : super(selector: selector,",
" children: null/*NgAnnotation.COMPILE_CHILDREN*/,",
" visibility: visibility,",
" map: map,",
" module: module,",
" exportExpressions: exportExpressions,",
" exportExpressionAttrs: exportExpressionAttrs);",
"}",
"",
"@Decorator(selector: '[ng-click]', map: const {'ng-click': '&onEvent'})",
"@Decorator(selector: '[ng-mouseout]', map: const {'ng-mouseout': '&onEvent'})",
"class NgEventDirective {",
" set onEvent(value) {}",
"}",
"",
"@Decorator(selector: '[ng-if]', map: const {'ng-if': '=>condition'})",
"class NgIfDirective {",
" set condition(value) {}",
"}",
"",
"@Decorator(selector: '[ng-show]', map: const {'ng-show': '=>show'})",
"class NgShowDirective {",
" set show(value) {}",
"}",
"",
"@Formatter(name: 'filter')",
"class FilterFormatter {}",
"",
"@Formatter(name: 'orderBy')",
"class OrderByFilter {}",
"",
"@Formatter(name: 'uppercase')",
"class UppercaseFilter {}",
"",
"class ViewFactory {",
" call(String templateUrl) => null;",
"}",
"",
"class Module {",
" install(Module m) {}",
" type(Type t) {}",
" value(Type t, value) {}",
"}",
"",
"class Injector {}",
"",
"Injector ngBootstrap({",
" Module module: null,",
" List<Module> modules: null,",
" /*dom.Element*/ element: null,",
" String selector: '[ng-app]',",
" /*Injector*/ injectorFactory/*(List<Module> modules): _defaultInjectorFactory*/}) {}",
""));
}
}