Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(102)

Unified Diff: pkg/polymer_expressions/test/syntax_test.dart

Issue 141703024: Refactor of PolymerExpressions. Adds "as" expressions. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: syntax, bindings, and globals tests now passing in Safari Created 6 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « pkg/polymer_expressions/test/parser_test.dart ('k') | pkg/polymer_expressions/test/tokenizer_test.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: pkg/polymer_expressions/test/syntax_test.dart
diff --git a/pkg/polymer_expressions/test/syntax_test.dart b/pkg/polymer_expressions/test/syntax_test.dart
index 5d15e172c5eb6e66849d7a697b6ecbcf02725f7e..d4f8d9e9a95a252cb386b78eac7488fd7759928f 100644
--- a/pkg/polymer_expressions/test/syntax_test.dart
+++ b/pkg/polymer_expressions/test/syntax_test.dart
@@ -8,105 +8,602 @@ import 'dart:html';
import 'package:observe/observe.dart';
import 'package:observe/mirrors_used.dart'; // make test smaller.
import 'package:polymer_expressions/polymer_expressions.dart';
+import 'package:polymer_expressions/eval.dart';
import 'package:template_binding/template_binding.dart';
import 'package:unittest/html_config.dart';
import 'package:unittest/unittest.dart';
+import 'package:smoke/mirrors.dart' as smoke;
+
+class TestScopeFactory implements ScopeFactory {
+ int scopeCount = 0;
+
+ modelScope({Object model, Map<String, Object> variables}) {
+ scopeCount++;
+ return new Scope(model: model, variables: variables);
+ }
+
+ childScope(Scope parent, String name, Object value) {
+ scopeCount++;
+ return parent.childScope(name, value);
+ }
+}
main() {
useHtmlConfiguration();
+ smoke.useMirrors();
group('PolymerExpressions', () {
- var testDiv;
+ DivElement testDiv;
+ TestScopeFactory testScopeFactory;
setUp(() {
document.body.append(testDiv = new DivElement());
+ testScopeFactory = new TestScopeFactory();
});
tearDown(() {
- testDiv.firstChild.remove();
+ testDiv.children.clear();
testDiv = null;
});
- test('should make two-way bindings to inputs', () {
- testDiv.nodes.add(new Element.html('''
- <template id="test" bind>
- <input id="input" value="{{ firstName }}">
- </template>'''));
- var person = new Person('John', 'Messerly', ['A', 'B', 'C']);
- templateBind(querySelector('#test'))
- ..bindingDelegate = new PolymerExpressions()
- ..model = person;
- return new Future(() {}).then((_) {
- InputElement input = querySelector('#input');
- expect(input.value, 'John');
- input.focus();
- input.value = 'Justin';
- input.blur();
- var event = new Event('change');
- // TODO(justin): figure out how to trigger keyboard events to test
- // two-way bindings
+ Future<Element> setUpTest(String html, {model, Map globals}) {
+ var tag = new Element.html(html,
+ treeSanitizer: new NullNodeTreeSanitizer());
+
+ // make sure templates behave in the polyfill
+ TemplateBindExtension.bootstrap(tag);
+
+ templateBind(tag)
+ ..bindingDelegate = new PolymerExpressions(globals: globals,
+ scopeFactory: testScopeFactory)
+ ..model = model;
+ testDiv.children.clear();
+ testDiv.append(tag);
+ return waitForChange(testDiv);
+ }
+
+ group('scope creation', () {
+ // These tests are sensitive to some internals of the implementation that
+ // might not be visible to applications, but are useful for verifying that
+ // that we're not creating too many Scopes.
+
+ // The reason that we create two Scopes in the cases with one binding is
+ // that <template bind> has one scope for the context to evaluate the bind
+ // binding in, and another scope for the bindings inside the template.
+
+ // We could try to optimize the outer scope away in cases where the
+ // expression is empty, but there are a lot of special cases in the
+ // syntax code already.
+ test('should create one scope for a single binding', () =>
+ setUpTest('''
+ <template id="test" bind>
+ <div>{{ data }}</div>
+ </template>''',
+ model: new Model('a'))
+ .then((_) {
+ expect(testDiv.children.length, 2);
+ expect(testDiv.children[1].text, 'a');
+ expect(testScopeFactory.scopeCount, 1);
+ }));
+
+ test('should only create a single scope for two bindings', () =>
+ setUpTest('''
+ <template id="test" bind>
+ <div>{{ data }}</div>
+ <div>{{ data }}</div>
+ </template>''',
+ model: new Model('a'))
+ .then((_) {
+ expect(testDiv.children.length, 3);
+ expect(testDiv.children[1].text, 'a');
+ expect(testDiv.children[2].text, 'a');
+ expect(testScopeFactory.scopeCount, 1);
+ }));
+
+ test('should create a new scope for a bind/as binding', () {
+ return setUpTest('''
+ <template id="test" bind>
+ <div>{{ data }}</div>
+ <template bind="{{ data as a }}" id="inner">
+ <div>{{ a }}</div>
+ <div>{{ data }}</div>
+ </template>
+ </template>''',
+ model: new Model('foo'))
+ .then((_) {
+ expect(testDiv.children.length, 5);
+ expect(testDiv.children[1].text, 'foo');
+ expect(testDiv.children[3].text, 'foo');
+ expect(testDiv.children[4].text, 'foo');
+ expect(testScopeFactory.scopeCount, 2);
+ });
});
+
+ test('should create scopes for a repeat/in binding', () {
+ return setUpTest('''
+ <template id="test" bind>
+ <div>{{ data }}</div>
+ <template repeat="{{ i in items }}" id="inner">
+ <div>{{ i }}</div>
+ <div>{{ data }}</div>
+ </template>
+ </template>''',
+ model: new Model('foo'), globals: {'items': ['a', 'b', 'c']})
+ .then((_) {
+ expect(testDiv.children.length, 9);
+ expect(testDiv.children[1].text, 'foo');
+ expect(testDiv.children[3].text, 'a');
+ expect(testDiv.children[4].text, 'foo');
+ expect(testDiv.children[5].text, 'b');
+ expect(testDiv.children[6].text, 'foo');
+ expect(testDiv.children[7].text, 'c');
+ expect(testDiv.children[8].text, 'foo');
+ // 1 scopes for <template bind>, 1 for each repeat
+ expect(testScopeFactory.scopeCount, 4);
+ });
+ });
+
+
});
- test('should handle null collections in "in" expressions', () {
- testDiv.nodes.add(new Element.html('''
- <template id="test" bind>
- <template repeat="{{ item in items }}">
- {{ item }}
- </template>
- </template>'''));
- templateBind(querySelector('#test')).bindingDelegate =
- new PolymerExpressions(globals: {'items': null});
- // the template should be the only node
- expect(testDiv.nodes.length, 1);
- expect(testDiv.nodes[0].id, 'test');
+ group('with template bind', () {
+
+ test('should show a simple binding on the model', () =>
+ setUpTest('''
+ <template id="test" bind>
+ <div>{{ data }}</div>
+ </template>''',
+ model: new Model('a'))
+ .then((_) {
+ expect(testDiv.children.length, 2);
+ expect(testDiv.children[1].text, 'a');
+ }));
+
+ test('should handle an empty binding on the model', () =>
+ setUpTest('''
+ <template id="test" bind>
+ <div>{{ }}</div>
+ </template>''',
+ model: 'a')
+ .then((_) {
+ expect(testDiv.children.length, 2);
+ expect(testDiv.children[1].text, 'a');
+ }));
+
+ test('should show a simple binding to a global', () =>
+ setUpTest('''
+ <template id="test" bind>
+ <div>{{ a }}</div>
+ </template>''',
+ globals: {'a': '123'})
+ .then((_) {
+ expect(testDiv.children.length, 2);
+ expect(testDiv.children[1].text, '123');
+ }));
+
+ test('should show an expression binding', () =>
+ setUpTest('''
+ <template id="test" bind>
+ <div>{{ data + 'b' }}</div>
+ </template>''',
+ model: new Model('a'))
+ .then((_) {
+ expect(testDiv.children.length, 2);
+ expect(testDiv.children[1].text, 'ab');
+ }));
+
+ test('should handle an expression in the bind attribute', () =>
+ setUpTest('''
+ <template id="test" bind="{{ data }}">
+ <div>{{ this }}</div>
+ </template>''',
+ model: new Model('a'))
+ .then((_) {
+ expect(testDiv.children.length, 2);
+ expect(testDiv.children[1].text, 'a');
+ }));
+
+ test('should handle a nested template with an expression in the bind '
+ 'attribute', () =>
+ setUpTest('''
+ <template id="test" bind>
+ <template id="inner" bind="{{ data }}">
+ <div>{{ this }}</div>
+ </template>
+ </template>''',
+ model: new Model('a'))
+ .then((_) {
+ expect(testDiv.children.length, 3);
+ expect(testDiv.children[2].text, 'a');
+ }));
+
+
+ test('should handle an "as" expression in the bind attribute', () =>
+ setUpTest('''
+ <template id="test" bind="{{ data as a }}">
+ <div>{{ data }}b</div>
+ <div>{{ a }}c</div>
+ </template>''',
+ model: new Model('a'))
+ .then((_) {
+ expect(testDiv.children.length, 3);
+ expect(testDiv.children[1].text, 'ab');
+ expect(testDiv.children[2].text, 'ac');
+ }));
+
+ // passes safari
+ test('should not resolve names in the outer template from within a nested'
+ ' template with a bind binding', () {
+ var completer = new Completer();
+ var bindingErrorHappened = false;
+ var templateRendered = false;
+ maybeComplete() {
+ if (bindingErrorHappened && templateRendered) {
+ completer.complete(true);
+ }
+ }
+ runZoned(() {
+ setUpTest('''
+ <template id="test" bind>
+ <div>{{ data }}</div>
+ <div>{{ b }}</div>
+ <template id="inner" bind="{{ b }}">
+ <div>{{ data }}</div>
+ <div>{{ b }}</div>
+ <div>{{ this }}</div>
+ </template>
+ </template>''',
+ model: new Model('foo'), globals: {'b': 'bbb'})
+ .then((_) {
+ expect(testDiv.children[0].text, '');
+ expect(testDiv.children[1].text, 'foo');
+ expect(testDiv.children[2].text, 'bbb');
+ // Something very strage is happening in the template bindings
+ // polyfill, and the template is being stamped out, inside the
+ // template tag (which shouldn't happen), and outside the template
+ //expect(testDiv.children[3].text, '');
+ expect(testDiv.children[3].tagName.toLowerCase(), 'template');
+ expect(testDiv.children[4].text, '');
+ expect(testDiv.children[5].text, 'bbb');
+ expect(testDiv.children[6].text, 'bbb');
+ templateRendered = true;
+ maybeComplete();
+ });
+ }, onError: (e, s) {
+ expect('$e', contains('data'));
+ bindingErrorHappened = true;
+ maybeComplete();
+ });
+ return completer.future;
+ });
+
+ // passes safari
+ test('should shadow names in the outer template from within a nested '
+ 'template', () =>
+ setUpTest('''
+ <template id="test" bind>
+ <div>{{ a }}</div>
+ <div>{{ b }}</div>
+ <template bind="{{ b as a }}">
+ <div>{{ a }}</div>
+ <div>{{ b }}</div>
+ </template>
+ </template>''',
+ globals: {'a': 'aaa', 'b': 'bbb'})
+ .then((_) {
+ expect(testDiv.children[0].text, '');
+ expect(testDiv.children[1].text, 'aaa');
+ expect(testDiv.children[2].text, 'bbb');
+ expect(testDiv.children[3].tagName.toLowerCase(), 'template');
+ expect(testDiv.children[4].text, 'bbb');
+ expect(testDiv.children[5].text, 'bbb');
+ }));
+
});
- test('should silently handle bad variable names', () {
- var completer = new Completer();
- runZoned(() {
- testDiv.nodes.add(new Element.html('''
- <template id="test" bind>{{ foo }}</template>'''));
- templateBind(querySelector('#test'))
- ..bindingDelegate = new PolymerExpressions()
- ..model = [];
- return new Future(() {});
- }, onError: (e, s) {
- expect('$e', contains('foo'));
- completer.complete(true);
+ group('with template repeat', () {
+
+ // passes safari
+ test('should not resolve names in the outer template from within a nested'
+ ' template with a repeat binding', () {
+ var completer = new Completer();
+ var bindingErrorHappened = false;
+ var templateRendered = false;
+ maybeComplete() {
+ if (bindingErrorHappened && templateRendered) {
+ completer.complete(true);
+ }
+ }
+ runZoned(() {
+ setUpTest('''
+ <template id="test" bind>
+ <div>{{ data }}</div>
+ <template repeat="{{ items }}">
+ <div>{{ }}{{ data }}</div>
+ </template>
+ </template>''',
+ globals: {'items': [1, 2, 3]},
+ model: new Model('a'))
+ .then((_) {
+ expect(testDiv.children[0].text, '');
+ expect(testDiv.children[1].text, 'a');
+ expect(testDiv.children[2].tagName.toLowerCase(), 'template');
+ expect(testDiv.children[3].text, '1');
+ expect(testDiv.children[4].text, '2');
+ expect(testDiv.children[5].text, '3');
+ templateRendered = true;
+ maybeComplete();
+ });
+ }, onError: (e, s) {
+ expect('$e', contains('data'));
+ bindingErrorHappened = true;
+ maybeComplete();
+ });
+ return completer.future;
+ });
+
+ test('should handle repeat/in bindings', () =>
+ setUpTest('''
+ <template id="test" bind>
+ <div>{{ data }}</div>
+ <template repeat="{{ item in items }}">
+ <div>{{ item }}{{ data }}</div>
+ </template>
+ </template>''',
+ globals: {'items': [1, 2, 3]},
+ model: new Model('a'))
+ .then((_) {
+ // expect 6 children: two templates, a div and three instances
+ expect(testDiv.children[0].text, '');
+ expect(testDiv.children[1].text, 'a');
+ expect(testDiv.children[2].tagName.toLowerCase(), 'template');
+ expect(testDiv.children[3].text, '1a');
+ expect(testDiv.children[4].text, '2a');
+ expect(testDiv.children[5].text, '3a');
+// expect(testDiv.children.map((c) => c.text),
+// ['', 'a', '', '1a', '2a', '3a']);
+ }));
+
+ test('should observe changes to lists in repeat bindings', () {
+ var items = new ObservableList.from([1, 2, 3]);
+ return setUpTest('''
+ <template id="test" bind>
+ <template repeat="{{ items }}">
+ <div>{{ }}</div>
+ </template>
+ </template>''',
+ globals: {'items': items},
+ model: new Model('a'))
+ .then((_) {
+ expect(testDiv.children[0].text, '');
+ expect(testDiv.children[1].tagName.toLowerCase(), 'template');
+ expect(testDiv.children[2].text, '1');
+ expect(testDiv.children[3].text, '2');
+ expect(testDiv.children[4].text, '3');
+// expect(testDiv.children.map((c) => c.text),
+// ['', '', '1', '2', '3']);
+ items.add(4);
+ return waitForChange(testDiv);
+ }).then((_) {
+ expect(testDiv.children[0].text, '');
+ expect(testDiv.children[1].tagName.toLowerCase(), 'template');
+ expect(testDiv.children[2].text, '1');
+ expect(testDiv.children[3].text, '2');
+ expect(testDiv.children[4].text, '3');
+ expect(testDiv.children[5].text, '4');
+// expect(testDiv.children.map((c) => c.text),
+// ['', '', '1', '2', '3', '4']);
+ });
+ });
+
+ test('should observe changes to lists in repeat/in bindings', () {
+ var items = new ObservableList.from([1, 2, 3]);
+ return setUpTest('''
+ <template id="test" bind>
+ <template repeat="{{ item in items }}">
+ <div>{{ item }}</div>
+ </template>
+ </template>''',
+ globals: {'items': items},
+ model: new Model('a'))
+ .then((_) {
+ expect(testDiv.children[0].text, '');
+ expect(testDiv.children[1].tagName.toLowerCase(), 'template');
+ expect(testDiv.children[2].text, '1');
+ expect(testDiv.children[3].text, '2');
+ expect(testDiv.children[4].text, '3');
+// expect(testDiv.children.map((c) => c.text),
+// ['', '', '1', '2', '3']);
+ items.add(4);
+ return waitForChange(testDiv);
+ }).then((_) {
+ expect(testDiv.children[0].text, '');
+ expect(testDiv.children[1].tagName.toLowerCase(), 'template');
+ expect(testDiv.children[2].text, '1');
+ expect(testDiv.children[3].text, '2');
+ expect(testDiv.children[4].text, '3');
+ expect(testDiv.children[5].text, '4');
+// expect(testDiv.children.map((c) => c.text),
+// ['', '', '1', '2', '3', '4']);
+ });
});
- return completer.future;
});
- });
-}
-@reflectable
-class Person extends ChangeNotifier {
- String _firstName;
- String _lastName;
- List<String> _items;
+ group('with template if', () {
- Person(this._firstName, this._lastName, this._items);
+ Future doTest(value, bool shouldRender) =>
+ setUpTest('''
+ <template id="test" bind>
+ <div>{{ data }}</div>
+ <template if="{{ show }}">
+ <div>{{ data }}</div>
+ </template>
+ </template>''',
+ globals: {'show': value},
+ model: new Model('a'))
+ .then((_) {
+ if (shouldRender) {
+ expect(testDiv.children.length, 4);
+ expect(testDiv.children[1].text, 'a');
+ expect(testDiv.children[3].text, 'a');
+ } else {
+ expect(testDiv.children.length, 3);
+ expect(testDiv.children[1].text, 'a');
+ }
+ });
- String get firstName => _firstName;
+ test('should render for a true expression',
+ () => doTest(true, true));
- void set firstName(String value) {
- _firstName = notifyPropertyChange(#firstName, _firstName, value);
- }
+ test('should treat a non-null expression as truthy',
+ () => doTest('a', true));
- String get lastName => _lastName;
+ test('should treat an empty list as truthy',
+ () => doTest([], true));
- void set lastName(String value) {
- _lastName = notifyPropertyChange(#lastName, _lastName, value);
- }
+ test('should handle a false expression',
+ () => doTest(false, false));
+
+ test('should treat null as falsey',
+ () => doTest(null, false));
+ });
- String getFullName() => '$_firstName $_lastName';
+ group('error handling', () {
- List<String> get items => _items;
+ test('should silently handle bad variable names', () {
+ var completer = new Completer();
+ runZoned(() {
+ testDiv.nodes.add(new Element.html('''
+ <template id="test" bind>{{ foo }}</template>'''));
+ templateBind(query('#test'))
+ ..bindingDelegate = new PolymerExpressions()
+ ..model = [];
+ return new Future(() {});
+ }, onError: (e, s) {
+ expect('$e', contains('foo'));
+ completer.complete(true);
+ });
+ return completer.future;
+ });
- void set items(List<String> value) {
- _items = notifyPropertyChange(#items, _items, value);
+ test('should handle null collections in "in" expressions', () =>
+ setUpTest('''
+ <template id="test" bind>
+ <template repeat="{{ item in items }}">
+ {{ item }}
+ </template>
+ </template>''',
+ globals: {'items': null})
+ .then((_) {
+ expect(testDiv.children.length, 2);
+ expect(testDiv.children[0].id, 'test');
+ }));
+
+ });
+
+ group('special bindings', () {
+
+ test('should handle class attributes with lists', () =>
+ setUpTest('''
+ <template id="test" bind>
+ <div class="{{ classes }}">
+ </template>''',
+ globals: {'classes': ['a', 'b']})
+ .then((_) {
+ expect(testDiv.children.length, 2);
+ expect(testDiv.children[1].attributes['class'], 'a b');
+ expect(testDiv.children[1].classes, ['a', 'b']);
+ }));
+
+ test('should handle class attributes with maps', () =>
+ setUpTest('''
+ <template id="test" bind>
+ <div class="{{ classes }}">
+ </template>''',
+ globals: {'classes': {'a': true, 'b': false, 'c': true}})
+ .then((_) {
+ expect(testDiv.children.length, 2);
+ expect(testDiv.children[1].attributes['class'], 'a c');
+ expect(testDiv.children[1].classes, ['a', 'c']);
+ }));
+
+ test('should handle style attributes with lists', () =>
+ setUpTest('''
+ <template id="test" bind>
+ <div style="{{ styles }}">
+ </template>''',
+ globals: {'styles': ['display: none', 'color: black']})
+ .then((_) {
+ expect(testDiv.children.length, 2);
+ expect(testDiv.children[1].attributes['style'],
+ 'display: none;color: black');
+ }));
+
+ test('should handle style attributes with maps', () =>
+ setUpTest('''
+ <template id="test" bind>
+ <div style="{{ styles }}">
+ </template>''',
+ globals: {'styles': {'display': 'none', 'color': 'black'}})
+ .then((_) {
+ expect(testDiv.children.length, 2);
+ expect(testDiv.children[1].attributes['style'],
+ 'display: none;color: black');
+ }));
+ });
+
+ group('regression tests', () {
+
+ test('should bind to literals', () =>
+ setUpTest('''
+ <template id="test" bind>
+ <div>{{ 123 }}</div>
+ <div>{{ 123.456 }}</div>
+ <div>{{ "abc" }}</div>
+ <div>{{ true }}</div>
+ <div>{{ null }}</div>
+ </template>''',
+ globals: {'items': null})
+ .then((_) {
+ expect(testDiv.children.length, 6);
+ expect(testDiv.children[1].text, '123');
+ expect(testDiv.children[2].text, '123.456');
+ expect(testDiv.children[3].text, 'abc');
+ expect(testDiv.children[4].text, 'true');
+ expect(testDiv.children[5].text, '');
+ }));
+
+ });
+
+ });
+}
+
+Future<Element> waitForChange(Element e) {
+ var completer = new Completer<Element>();
+ new MutationObserver((mutations, observer) {
+ observer.disconnect();
+ completer.complete(e);
+ }).observe(e, childList: true);
+ return completer.future.timeout(new Duration(seconds: 1));
+}
+
+@reflectable
+class Model extends ChangeNotifier {
+ String _data;
+
+ Model(this._data);
+
+ String get data => _data;
+
+ void set data(String value) {
+ _data = notifyPropertyChange(#data, _data, value);
}
- String toString() => "Person(firstName: $_firstName, lastName: $_lastName)";
+ String toString() => "Model(data: $_data)";
}
+
+class NullNodeTreeSanitizer implements NodeTreeSanitizer {
+
+ @override
+ void sanitizeTree(Node node) {}
+}
« no previous file with comments | « pkg/polymer_expressions/test/parser_test.dart ('k') | pkg/polymer_expressions/test/tokenizer_test.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698