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 50972e3246040550e81eb23e4ef6e87f9dfea038..0815869ec8681555807abcd8345243ee7214723a 100644 |
--- a/pkg/polymer_expressions/test/syntax_test.dart |
+++ b/pkg/polymer_expressions/test/syntax_test.dart |
@@ -8,108 +8,403 @@ import 'dart:html'; |
import 'package:logging/logging.dart'; |
import 'package:observe/observe.dart'; |
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/html_enhanced_config.dart'; |
import 'package:unittest/unittest.dart'; |
main() { |
- useHtmlConfiguration(); |
+ useHtmlEnhancedConfiguration(); |
group('PolymerExpressions', () { |
- var testDiv; |
+ DivElement testDiv; |
+ int _scope_id; |
setUp(() { |
document.body.append(testDiv = new DivElement()); |
+ _scope_id = 0; |
}); |
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(query('#test')) |
- ..bindingDelegate = new PolymerExpressions() |
- ..model = person; |
- return new Future.delayed(new Duration()).then((_) { |
- InputElement input = query('#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 |
+ Scope testScopeFactory({model, Map<String, Object> variables, |
+ Scope parent}) { |
+ var testVariables = variables == null ? {} : new Map.from(variables); |
+ testVariables['_scope_id'] = _scope_id++; |
+ var scope = new Scope(model: model, variables: testVariables, |
+ parent: parent); |
+ return scope; |
+ } |
+ |
+ Future<Element> setUpTest(String html, {model, Map globals}) { |
+ var tag = new Element.html(html); |
+ 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 two scopes for a single binding', () => |
+ setUpTest(''' |
+ <template id="test" bind> |
+ <div>{{ _scope_id }}</div> |
Siggi Cherem (dart-lang)
2014/03/19 18:37:27
FYI - just ran this test locally in dart2js, seems
justinfagnani
2014/04/24 00:26:13
This has been fixed in template binding, and I no
|
+ </template>''', |
+ model: new Model('a')) |
+ .then((_) { |
+ expect(testDiv.children.length, 2); |
+ expect(testDiv.children[1].text, '1'); |
+ })); |
+ |
+ test('should create a single scope for two bindings', () => |
+ setUpTest(''' |
+ <template id="test" bind> |
+ <div>{{ _scope_id }}</div> |
+ <div>{{ _scope_id }}</div> |
+ </template>''', |
+ model: new Model('a')) |
+ .then((_) { |
+ expect(testDiv.children.length, 3); |
+ expect(testDiv.children[1].text, '1'); |
+ expect(testDiv.children[2].text, '1'); |
+ })); |
+ |
+ test('should create a new scope for a bind/as binding', () { |
+ return setUpTest(''' |
+ <template id="test" bind> |
+ <div>{{ _scope_id }}</div> |
+ <template bind="{{ data as a }}" id="inner"> |
+ <div>{{ _scope_id }}</div> |
+ <div>{{ a }}</div> |
+ <div>{{ data }}</div> |
+ </template> |
+ </template>''', |
+ model: new Model('foo')) |
+ .then((_) { |
+ expect(testDiv.children.length, 6); |
+ expect(testDiv.children[1].text, '1'); |
+ expect(testDiv.children[3].text, '2'); |
+ expect(testDiv.children[4].text, 'foo'); |
+ expect(testDiv.children[5].text, 'foo'); |
+ }); |
}); |
+ |
+ test('should create scopes for a repeat/in binding', () { |
+ return setUpTest(''' |
+ <template id="test" bind> |
+ <div>{{ _scope_id }}</div> |
+ <template repeat="{{ i in items }}" id="inner"> |
+ <div>{{ _scope_id }}</div> |
+ <div>{{ i }}</div> |
+ <div>{{ data }}</div> |
+ </template> |
+ </template>''', |
+ model: new Model('foo'), globals: {'items': ['a', 'b', 'c']}) |
+ .then((_) { |
+ expect(testDiv.children.length, 12); |
+ expect(testDiv.children[1].text, '1'); |
+ expect(testDiv.children[3].text, '2'); |
+ expect(testDiv.children[4].text, 'a'); |
+ expect(testDiv.children[5].text, 'foo'); |
+ expect(testDiv.children[6].text, '3'); |
+ expect(testDiv.children[7].text, 'b'); |
+ expect(testDiv.children[8].text, 'foo'); |
+ expect(testDiv.children[9].text, '4'); |
+ expect(testDiv.children[10].text, 'c'); |
+ expect(testDiv.children[11].text, 'foo'); |
+ }); |
+ }); |
+ |
+ |
}); |
- 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(query('#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'); |
+ })); |
+ |
+ /** |
+ * This test fails in dart2js because we appear to be tickling a bug in |
+ * mirrors via _TemplateIterator. |
+ */ |
+ test('should not resolve names in the outer template from within a nested ' |
+ 'template with a bind binding', () => |
+ 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.map((c) => c.text), |
+ ['', 'foo', 'bbb', '', '', 'bbb', 'bbb']); |
+ })); |
+ |
+ 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.map((c) => c.text), |
+ ['', 'aaa', 'bbb', '', 'bbb', 'bbb']); |
+ })); |
+ |
}); |
- test('should silently handle bad variable names', () { |
- var logger = new Logger('polymer_expressions'); |
- var logFuture = logger.onRecord.toList(); |
- testDiv.nodes.add(new Element.html(''' |
- <template id="test" bind>{{ foo }}</template>''')); |
- templateBind(query('#test')) |
- ..bindingDelegate = new PolymerExpressions() |
- ..model = []; |
- return new Future(() { |
- logger.clearListeners(); |
- return logFuture.then((records) { |
- expect(records.length, 1); |
- expect(records.first.message, |
- contains('Error evaluating expression')); |
- expect(records.first.message, contains('foo')); |
+ group('with template repeat', () { |
+ |
+ test('should handle "in" expressions', () => |
+ 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.map((c) => c.text), |
+ ['', 'a', '', '1a', '2a', '3a']); |
+ })); |
+ |
+ }); |
+ |
+ group('with template if', () { |
+ |
+ 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'); |
+ } |
+ }); |
+ |
+ test('should render for a true expression', |
+ () => doTest(true, true)); |
+ |
+ test('should treat a non-null expression as truthy', |
+ () => doTest('a', true)); |
+ |
+ test('should treat an empty list as truthy', |
+ () => doTest([], true)); |
+ |
+ test('should handle a false expression', |
+ () => doTest(false, false)); |
+ |
+ test('should treat null as falsey', |
+ () => doTest(null, false)); |
+ }); |
+ |
+ group('error handling', () { |
+ |
+ test('should handle and log bad variable names', () { |
+ var logger = new Logger('polymer_expressions'); |
+ var logFuture = logger.onRecord.toList(); |
+ return setUpTest(''' |
+ <template id="test" bind> |
+ <span>A</span> |
+ <span>{{ foo }}</span> |
+ <span>B</span> |
+ </template>''') |
+ .then((_) { |
+ expect(testDiv.children.length, 4); |
+ expect(testDiv.children.skip(1).map((c) => c.text), ['A', '', 'B']); |
+ logger.clearListeners(); |
+ return logFuture.then((records) { |
+ expect(records.length, 1); |
+ expect(records.first.message, |
+ contains('Error evaluating expression')); |
+ expect(records.first.message, contains('foo')); |
+ }); |
}); |
}); |
+ |
+ 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'); |
+ })); |
+ |
}); |
- }); |
-} |
-@reflectable |
-class Person extends ChangeNotifier { |
- String _firstName; |
- String _lastName; |
- List<String> _items; |
+ group('regression tests', () { |
- Person(this._firstName, this._lastName, this._items); |
+ 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, ''); |
+ })); |
- String get firstName => _firstName; |
+ }); |
- void set firstName(String value) { |
- _firstName = notifyPropertyChange(#firstName, _firstName, value); |
- } |
+ }); |
+} |
- String get lastName => _lastName; |
+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)); |
+} |
- void set lastName(String value) { |
- _lastName = notifyPropertyChange(#lastName, _lastName, value); |
- } |
+@reflectable |
+class Model extends ChangeNotifier { |
+ String _data; |
- String getFullName() => '$_firstName $_lastName'; |
+ Model(this._data); |
- List<String> get items => _items; |
+ String get data => _data; |
- void set items(List<String> value) { |
- _items = notifyPropertyChange(#items, _items, value); |
+ void set data(String value) { |
+ _data = notifyPropertyChange(#data, _data, value); |
} |
- String toString() => "Person(firstName: $_firstName, lastName: $_lastName)"; |
+ String toString() => "Model(data: $_data)"; |
} |