Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 import 'dart:async'; | 5 import 'dart:async'; |
| 6 import 'dart:html'; | 6 import 'dart:html'; |
| 7 | 7 |
| 8 import 'package:logging/logging.dart'; | 8 import 'package:logging/logging.dart'; |
| 9 import 'package:observe/observe.dart'; | 9 import 'package:observe/observe.dart'; |
| 10 import 'package:polymer_expressions/polymer_expressions.dart'; | 10 import 'package:polymer_expressions/polymer_expressions.dart'; |
| 11 import 'package:polymer_expressions/eval.dart'; | |
| 11 import 'package:template_binding/template_binding.dart'; | 12 import 'package:template_binding/template_binding.dart'; |
| 12 import 'package:unittest/html_config.dart'; | 13 import 'package:unittest/html_enhanced_config.dart'; |
| 13 import 'package:unittest/unittest.dart'; | 14 import 'package:unittest/unittest.dart'; |
| 14 | 15 |
| 15 main() { | 16 main() { |
| 16 useHtmlConfiguration(); | 17 useHtmlEnhancedConfiguration(); |
| 17 | 18 |
| 18 group('PolymerExpressions', () { | 19 group('PolymerExpressions', () { |
| 19 var testDiv; | 20 DivElement testDiv; |
| 21 int _scope_id; | |
| 20 | 22 |
| 21 setUp(() { | 23 setUp(() { |
| 22 document.body.append(testDiv = new DivElement()); | 24 document.body.append(testDiv = new DivElement()); |
| 25 _scope_id = 0; | |
| 23 }); | 26 }); |
| 24 | 27 |
| 25 tearDown(() { | 28 tearDown(() { |
| 26 testDiv.firstChild.remove(); | 29 testDiv.children.clear(); |
| 27 testDiv = null; | 30 testDiv = null; |
| 28 }); | 31 }); |
| 29 | 32 |
| 30 test('should make two-way bindings to inputs', () { | 33 Scope testScopeFactory({model, Map<String, Object> variables, |
| 31 testDiv.nodes.add(new Element.html(''' | 34 Scope parent}) { |
| 32 <template id="test" bind> | 35 var testVariables = variables == null ? {} : new Map.from(variables); |
| 33 <input id="input" value="{{ firstName }}"> | 36 testVariables['_scope_id'] = _scope_id++; |
| 34 </template>''')); | 37 var scope = new Scope(model: model, variables: testVariables, |
| 35 var person = new Person('John', 'Messerly', ['A', 'B', 'C']); | 38 parent: parent); |
| 36 templateBind(query('#test')) | 39 return scope; |
| 37 ..bindingDelegate = new PolymerExpressions() | 40 } |
| 38 ..model = person; | 41 |
| 39 return new Future.delayed(new Duration()).then((_) { | 42 Future<Element> setUpTest(String html, {model, Map globals}) { |
| 40 InputElement input = query('#input'); | 43 var tag = new Element.html(html); |
| 41 expect(input.value, 'John'); | 44 templateBind(tag) |
| 42 input.focus(); | 45 ..bindingDelegate = new PolymerExpressions(globals: globals, |
| 43 input.value = 'Justin'; | 46 scopeFactory: testScopeFactory) |
| 44 input.blur(); | 47 ..model = model; |
| 45 var event = new Event('change'); | 48 testDiv.children.clear(); |
| 46 // TODO(justin): figure out how to trigger keyboard events to test | 49 testDiv.append(tag); |
| 47 // two-way bindings | 50 return waitForChange(testDiv); |
| 48 }); | 51 } |
| 49 }); | 52 |
| 50 | 53 group('scope creation', () { |
| 51 test('should handle null collections in "in" expressions', () { | 54 // These tests are sensitive to some internals of the implementation that |
| 52 testDiv.nodes.add(new Element.html(''' | 55 // might not be visible to applications, but are useful for verifying that |
| 53 <template id="test" bind> | 56 // that we're not creating too many Scopes. |
| 54 <template repeat="{{ item in items }}"> | 57 |
| 55 {{ item }} | 58 // The reason that we create two Scopes in the cases with one binding is |
| 56 </template> | 59 // that <template bind> has one scope for the context to evaluate the bind |
| 57 </template>''')); | 60 // binding in, and another scope for the bindings inside the template. |
| 58 templateBind(query('#test')).bindingDelegate = | 61 |
| 59 new PolymerExpressions(globals: {'items': null}); | 62 // We could try to optimize the outer scope away in cases where the |
| 60 // the template should be the only node | 63 // expression is empty, but there are a lot of special cases in the |
| 61 expect(testDiv.nodes.length, 1); | 64 // syntax code already. |
| 62 expect(testDiv.nodes[0].id, 'test'); | 65 test('should create two scopes for a single binding', () => |
| 63 }); | 66 setUpTest(''' |
| 64 | 67 <template id="test" bind> |
| 65 test('should silently handle bad variable names', () { | 68 <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
| |
| 66 var logger = new Logger('polymer_expressions'); | 69 </template>''', |
| 67 var logFuture = logger.onRecord.toList(); | 70 model: new Model('a')) |
| 68 testDiv.nodes.add(new Element.html(''' | 71 .then((_) { |
| 69 <template id="test" bind>{{ foo }}</template>''')); | 72 expect(testDiv.children.length, 2); |
| 70 templateBind(query('#test')) | 73 expect(testDiv.children[1].text, '1'); |
| 71 ..bindingDelegate = new PolymerExpressions() | 74 })); |
| 72 ..model = []; | 75 |
| 73 return new Future(() { | 76 test('should create a single scope for two bindings', () => |
| 74 logger.clearListeners(); | 77 setUpTest(''' |
| 75 return logFuture.then((records) { | 78 <template id="test" bind> |
| 76 expect(records.length, 1); | 79 <div>{{ _scope_id }}</div> |
| 77 expect(records.first.message, | 80 <div>{{ _scope_id }}</div> |
| 78 contains('Error evaluating expression')); | 81 </template>''', |
| 79 expect(records.first.message, contains('foo')); | 82 model: new Model('a')) |
| 83 .then((_) { | |
| 84 expect(testDiv.children.length, 3); | |
| 85 expect(testDiv.children[1].text, '1'); | |
| 86 expect(testDiv.children[2].text, '1'); | |
| 87 })); | |
| 88 | |
| 89 test('should create a new scope for a bind/as binding', () { | |
| 90 return setUpTest(''' | |
| 91 <template id="test" bind> | |
| 92 <div>{{ _scope_id }}</div> | |
| 93 <template bind="{{ data as a }}" id="inner"> | |
| 94 <div>{{ _scope_id }}</div> | |
| 95 <div>{{ a }}</div> | |
| 96 <div>{{ data }}</div> | |
| 97 </template> | |
| 98 </template>''', | |
| 99 model: new Model('foo')) | |
| 100 .then((_) { | |
| 101 expect(testDiv.children.length, 6); | |
| 102 expect(testDiv.children[1].text, '1'); | |
| 103 expect(testDiv.children[3].text, '2'); | |
| 104 expect(testDiv.children[4].text, 'foo'); | |
| 105 expect(testDiv.children[5].text, 'foo'); | |
| 80 }); | 106 }); |
| 81 }); | 107 }); |
| 82 }); | 108 |
| 109 test('should create scopes for a repeat/in binding', () { | |
| 110 return setUpTest(''' | |
| 111 <template id="test" bind> | |
| 112 <div>{{ _scope_id }}</div> | |
| 113 <template repeat="{{ i in items }}" id="inner"> | |
| 114 <div>{{ _scope_id }}</div> | |
| 115 <div>{{ i }}</div> | |
| 116 <div>{{ data }}</div> | |
| 117 </template> | |
| 118 </template>''', | |
| 119 model: new Model('foo'), globals: {'items': ['a', 'b', 'c']}) | |
| 120 .then((_) { | |
| 121 expect(testDiv.children.length, 12); | |
| 122 expect(testDiv.children[1].text, '1'); | |
| 123 expect(testDiv.children[3].text, '2'); | |
| 124 expect(testDiv.children[4].text, 'a'); | |
| 125 expect(testDiv.children[5].text, 'foo'); | |
| 126 expect(testDiv.children[6].text, '3'); | |
| 127 expect(testDiv.children[7].text, 'b'); | |
| 128 expect(testDiv.children[8].text, 'foo'); | |
| 129 expect(testDiv.children[9].text, '4'); | |
| 130 expect(testDiv.children[10].text, 'c'); | |
| 131 expect(testDiv.children[11].text, 'foo'); | |
| 132 }); | |
| 133 }); | |
| 134 | |
| 135 | |
| 136 }); | |
| 137 | |
| 138 group('with template bind', () { | |
| 139 | |
| 140 test('should show a simple binding on the model', () => | |
| 141 setUpTest(''' | |
| 142 <template id="test" bind> | |
| 143 <div>{{ data }}</div> | |
| 144 </template>''', | |
| 145 model: new Model('a')) | |
| 146 .then((_) { | |
| 147 expect(testDiv.children.length, 2); | |
| 148 expect(testDiv.children[1].text, 'a'); | |
| 149 })); | |
| 150 | |
| 151 test('should handle an empty binding on the model', () => | |
| 152 setUpTest(''' | |
| 153 <template id="test" bind> | |
| 154 <div>{{ }}</div> | |
| 155 </template>''', | |
| 156 model: 'a') | |
| 157 .then((_) { | |
| 158 expect(testDiv.children.length, 2); | |
| 159 expect(testDiv.children[1].text, 'a'); | |
| 160 })); | |
| 161 | |
| 162 test('should show a simple binding to a global', () => | |
| 163 setUpTest(''' | |
| 164 <template id="test" bind> | |
| 165 <div>{{ a }}</div> | |
| 166 </template>''', | |
| 167 globals: {'a': '123'}) | |
| 168 .then((_) { | |
| 169 expect(testDiv.children.length, 2); | |
| 170 expect(testDiv.children[1].text, '123'); | |
| 171 })); | |
| 172 | |
| 173 test('should show an expression binding', () => | |
| 174 setUpTest(''' | |
| 175 <template id="test" bind> | |
| 176 <div>{{ data + 'b' }}</div> | |
| 177 </template>''', | |
| 178 model: new Model('a')) | |
| 179 .then((_) { | |
| 180 expect(testDiv.children.length, 2); | |
| 181 expect(testDiv.children[1].text, 'ab'); | |
| 182 })); | |
| 183 | |
| 184 test('should handle an expression in the bind attribute', () => | |
| 185 setUpTest(''' | |
| 186 <template id="test" bind="{{ data }}"> | |
| 187 <div>{{ this }}</div> | |
| 188 </template>''', | |
| 189 model: new Model('a')) | |
| 190 .then((_) { | |
| 191 expect(testDiv.children.length, 2); | |
| 192 expect(testDiv.children[1].text, 'a'); | |
| 193 })); | |
| 194 | |
| 195 test('should handle a nested template with an expression in the bind ' | |
| 196 'attribute', () => | |
| 197 setUpTest(''' | |
| 198 <template id="test" bind> | |
| 199 <template id="inner" bind="{{ data }}"> | |
| 200 <div>{{ this }}</div> | |
| 201 </template> | |
| 202 </template>''', | |
| 203 model: new Model('a')) | |
| 204 .then((_) { | |
| 205 expect(testDiv.children.length, 3); | |
| 206 expect(testDiv.children[2].text, 'a'); | |
| 207 })); | |
| 208 | |
| 209 | |
| 210 test('should handle an "as" expression in the bind attribute', () => | |
| 211 setUpTest(''' | |
| 212 <template id="test" bind="{{ data as a }}"> | |
| 213 <div>{{ data }}b</div> | |
| 214 <div>{{ a }}c</div> | |
| 215 </template>''', | |
| 216 model: new Model('a')) | |
| 217 .then((_) { | |
| 218 expect(testDiv.children.length, 3); | |
| 219 expect(testDiv.children[1].text, 'ab'); | |
| 220 expect(testDiv.children[2].text, 'ac'); | |
| 221 })); | |
| 222 | |
| 223 /** | |
| 224 * This test fails in dart2js because we appear to be tickling a bug in | |
| 225 * mirrors via _TemplateIterator. | |
| 226 */ | |
| 227 test('should not resolve names in the outer template from within a nested ' | |
| 228 'template with a bind binding', () => | |
| 229 setUpTest(''' | |
| 230 <template id="test" bind> | |
| 231 <div>{{ data }}</div> | |
| 232 <div>{{ b }}</div> | |
| 233 <template id="inner" bind="{{ b }}"> | |
| 234 <div>{{ data }}</div> | |
| 235 <div>{{ b }}</div> | |
| 236 <div>{{ this }}</div> | |
| 237 </template> | |
| 238 </template>''', | |
| 239 model: new Model('foo'), globals: {'b': 'bbb'}) | |
| 240 .then((_) { | |
| 241 expect(testDiv.children.map((c) => c.text), | |
| 242 ['', 'foo', 'bbb', '', '', 'bbb', 'bbb']); | |
| 243 })); | |
| 244 | |
| 245 test('should shadow names in the outer template from within a nested ' | |
| 246 'template', () => | |
| 247 setUpTest(''' | |
| 248 <template id="test" bind> | |
| 249 <div>{{ a }}</div> | |
| 250 <div>{{ b }}</div> | |
| 251 <template bind="{{ b as a }}"> | |
| 252 <div>{{ a }}</div> | |
| 253 <div>{{ b }}</div> | |
| 254 </template> | |
| 255 </template>''', | |
| 256 globals: {'a': 'aaa', 'b': 'bbb'}) | |
| 257 .then((_) { | |
| 258 expect(testDiv.children.map((c) => c.text), | |
| 259 ['', 'aaa', 'bbb', '', 'bbb', 'bbb']); | |
| 260 })); | |
| 261 | |
| 262 }); | |
| 263 | |
| 264 group('with template repeat', () { | |
| 265 | |
| 266 test('should handle "in" expressions', () => | |
| 267 setUpTest(''' | |
| 268 <template id="test" bind> | |
| 269 <div>{{ data }}</div> | |
| 270 <template repeat="{{ item in items }}"> | |
| 271 <div>{{ item }}{{ data }}</div> | |
| 272 </template> | |
| 273 </template>''', | |
| 274 globals: {'items': [1, 2, 3]}, | |
| 275 model: new Model('a')) | |
| 276 .then((_) { | |
| 277 // expect 6 children: two templates, a div and three instances | |
| 278 expect(testDiv.children.map((c) => c.text), | |
| 279 ['', 'a', '', '1a', '2a', '3a']); | |
| 280 })); | |
| 281 | |
| 282 }); | |
| 283 | |
| 284 group('with template if', () { | |
| 285 | |
| 286 Future doTest(value, bool shouldRender) => | |
| 287 setUpTest(''' | |
| 288 <template id="test" bind> | |
| 289 <div>{{ data }}</div> | |
| 290 <template if="{{ show }}"> | |
| 291 <div>{{ data }}</div> | |
| 292 </template> | |
| 293 </template>''', | |
| 294 globals: {'show': value}, | |
| 295 model: new Model('a')) | |
| 296 .then((_) { | |
| 297 if (shouldRender) { | |
| 298 expect(testDiv.children.length, 4); | |
| 299 expect(testDiv.children[1].text, 'a'); | |
| 300 expect(testDiv.children[3].text, 'a'); | |
| 301 } else { | |
| 302 expect(testDiv.children.length, 3); | |
| 303 expect(testDiv.children[1].text, 'a'); | |
| 304 } | |
| 305 }); | |
| 306 | |
| 307 test('should render for a true expression', | |
| 308 () => doTest(true, true)); | |
| 309 | |
| 310 test('should treat a non-null expression as truthy', | |
| 311 () => doTest('a', true)); | |
| 312 | |
| 313 test('should treat an empty list as truthy', | |
| 314 () => doTest([], true)); | |
| 315 | |
| 316 test('should handle a false expression', | |
| 317 () => doTest(false, false)); | |
| 318 | |
| 319 test('should treat null as falsey', | |
| 320 () => doTest(null, false)); | |
| 321 }); | |
| 322 | |
| 323 group('error handling', () { | |
| 324 | |
| 325 test('should handle and log bad variable names', () { | |
| 326 var logger = new Logger('polymer_expressions'); | |
| 327 var logFuture = logger.onRecord.toList(); | |
| 328 return setUpTest(''' | |
| 329 <template id="test" bind> | |
| 330 <span>A</span> | |
| 331 <span>{{ foo }}</span> | |
| 332 <span>B</span> | |
| 333 </template>''') | |
| 334 .then((_) { | |
| 335 expect(testDiv.children.length, 4); | |
| 336 expect(testDiv.children.skip(1).map((c) => c.text), ['A', '', 'B']); | |
| 337 logger.clearListeners(); | |
| 338 return logFuture.then((records) { | |
| 339 expect(records.length, 1); | |
| 340 expect(records.first.message, | |
| 341 contains('Error evaluating expression')); | |
| 342 expect(records.first.message, contains('foo')); | |
| 343 }); | |
| 344 }); | |
| 345 }); | |
| 346 | |
| 347 test('should handle null collections in "in" expressions', () => | |
| 348 setUpTest(''' | |
| 349 <template id="test" bind> | |
| 350 <template repeat="{{ item in items }}"> | |
| 351 {{ item }} | |
| 352 </template> | |
| 353 </template>''', | |
| 354 globals: {'items': null}) | |
| 355 .then((_) { | |
| 356 expect(testDiv.children.length, 2); | |
| 357 expect(testDiv.children[0].id, 'test'); | |
| 358 })); | |
| 359 | |
| 360 }); | |
| 361 | |
| 362 group('regression tests', () { | |
| 363 | |
| 364 test('should bind to literals', () => | |
| 365 setUpTest(''' | |
| 366 <template id="test" bind> | |
| 367 <div>{{ 123 }}</div> | |
| 368 <div>{{ 123.456 }}</div> | |
| 369 <div>{{ "abc" }}</div> | |
| 370 <div>{{ true }}</div> | |
| 371 <div>{{ null }}</div> | |
| 372 </template>''', | |
| 373 globals: {'items': null}) | |
| 374 .then((_) { | |
| 375 expect(testDiv.children.length, 6); | |
| 376 expect(testDiv.children[1].text, '123'); | |
| 377 expect(testDiv.children[2].text, '123.456'); | |
| 378 expect(testDiv.children[3].text, 'abc'); | |
| 379 expect(testDiv.children[4].text, 'true'); | |
| 380 expect(testDiv.children[5].text, ''); | |
| 381 })); | |
| 382 | |
| 383 }); | |
| 384 | |
| 83 }); | 385 }); |
| 84 } | 386 } |
| 85 | 387 |
| 388 Future<Element> waitForChange(Element e) { | |
| 389 var completer = new Completer<Element>(); | |
| 390 new MutationObserver((mutations, observer) { | |
| 391 observer.disconnect(); | |
| 392 completer.complete(e); | |
| 393 }).observe(e, childList: true); | |
| 394 return completer.future.timeout(new Duration(seconds: 1)); | |
| 395 } | |
| 396 | |
| 86 @reflectable | 397 @reflectable |
| 87 class Person extends ChangeNotifier { | 398 class Model extends ChangeNotifier { |
| 88 String _firstName; | 399 String _data; |
| 89 String _lastName; | 400 |
| 90 List<String> _items; | 401 Model(this._data); |
| 91 | 402 |
| 92 Person(this._firstName, this._lastName, this._items); | 403 String get data => _data; |
| 93 | 404 |
| 94 String get firstName => _firstName; | 405 void set data(String value) { |
| 95 | 406 _data = notifyPropertyChange(#data, _data, value); |
| 96 void set firstName(String value) { | |
| 97 _firstName = notifyPropertyChange(#firstName, _firstName, value); | |
| 98 } | 407 } |
| 99 | 408 |
| 100 String get lastName => _lastName; | 409 String toString() => "Model(data: $_data)"; |
| 101 | |
| 102 void set lastName(String value) { | |
| 103 _lastName = notifyPropertyChange(#lastName, _lastName, value); | |
| 104 } | |
| 105 | |
| 106 String getFullName() => '$_firstName $_lastName'; | |
| 107 | |
| 108 List<String> get items => _items; | |
| 109 | |
| 110 void set items(List<String> value) { | |
| 111 _items = notifyPropertyChange(#items, _items, value); | |
| 112 } | |
| 113 | |
| 114 String toString() => "Person(firstName: $_firstName, lastName: $_lastName)"; | |
| 115 } | 410 } |
| OLD | NEW |