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:observe/observe.dart'; | 8 import 'package:observe/observe.dart'; |
9 import 'package:observe/mirrors_used.dart'; // make test smaller. | 9 import 'package:observe/mirrors_used.dart'; // make test smaller. |
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'; |
15 import 'package:smoke/mirrors.dart' as smoke; | |
16 | |
17 class TestScopeFactory implements ScopeFactory { | |
18 int scopeCount = 0; | |
19 | |
20 modelScope({Object model, Map<String, Object> variables}) { | |
21 scopeCount++; | |
22 return new Scope(model: model, variables: variables); | |
23 } | |
24 | |
25 childScope(Scope parent, String name, Object value) { | |
26 scopeCount++; | |
27 return parent.childScope(name, value); | |
28 } | |
29 } | |
14 | 30 |
15 main() { | 31 main() { |
16 useHtmlConfiguration(); | 32 useHtmlEnhancedConfiguration(); |
Jennifer Messerly
2014/04/24 00:51:34
i think i mentioned this in an earlier comment, bu
justinfagnani
2014/05/28 00:29:37
Done.
| |
33 smoke.useMirrors(); | |
17 | 34 |
18 group('PolymerExpressions', () { | 35 group('PolymerExpressions', () { |
19 var testDiv; | 36 DivElement testDiv; |
37 TestScopeFactory testScopeFactory; | |
20 | 38 |
21 setUp(() { | 39 setUp(() { |
22 document.body.append(testDiv = new DivElement()); | 40 document.body.append(testDiv = new DivElement()); |
41 testScopeFactory = new TestScopeFactory(); | |
23 }); | 42 }); |
24 | 43 |
25 tearDown(() { | 44 tearDown(() { |
26 testDiv.firstChild.remove(); | 45 testDiv.children.clear(); |
27 testDiv = null; | 46 testDiv = null; |
28 }); | 47 }); |
29 | 48 |
30 test('should make two-way bindings to inputs', () { | 49 Future<Element> setUpTest(String html, {model, Map globals}) { |
31 testDiv.nodes.add(new Element.html(''' | 50 var tag = new Element.html(html, |
32 <template id="test" bind> | 51 treeSanitizer: new NullNodeTreeSanitizer()); |
33 <input id="input" value="{{ firstName }}"> | 52 templateBind(tag) |
34 </template>''')); | 53 ..bindingDelegate = new PolymerExpressions(globals: globals, |
35 var person = new Person('John', 'Messerly', ['A', 'B', 'C']); | 54 scopeFactory: testScopeFactory) |
36 templateBind(querySelector('#test')) | 55 ..model = model; |
37 ..bindingDelegate = new PolymerExpressions() | 56 testDiv.children.clear(); |
38 ..model = person; | 57 testDiv.append(tag); |
39 return new Future(() {}).then((_) { | 58 return waitForChange(testDiv); |
40 InputElement input = querySelector('#input'); | 59 } |
41 expect(input.value, 'John'); | 60 |
42 input.focus(); | 61 group('scope creation', () { |
43 input.value = 'Justin'; | 62 // These tests are sensitive to some internals of the implementation that |
44 input.blur(); | 63 // might not be visible to applications, but are useful for verifying that |
45 var event = new Event('change'); | 64 // that we're not creating too many Scopes. |
46 // TODO(justin): figure out how to trigger keyboard events to test | 65 |
47 // two-way bindings | 66 // The reason that we create two Scopes in the cases with one binding is |
48 }); | 67 // that <template bind> has one scope for the context to evaluate the bind |
49 }); | 68 // binding in, and another scope for the bindings inside the template. |
50 | 69 |
51 test('should handle null collections in "in" expressions', () { | 70 // We could try to optimize the outer scope away in cases where the |
52 testDiv.nodes.add(new Element.html(''' | 71 // expression is empty, but there are a lot of special cases in the |
53 <template id="test" bind> | 72 // syntax code already. |
54 <template repeat="{{ item in items }}"> | 73 test('should create one scope for a single binding', () => |
55 {{ item }} | 74 setUpTest(''' |
56 </template> | 75 <template id="test" bind> |
57 </template>''')); | 76 <div>{{ data }}</div> |
58 templateBind(querySelector('#test')).bindingDelegate = | 77 </template>''', |
59 new PolymerExpressions(globals: {'items': null}); | 78 model: new Model('a')) |
60 // the template should be the only node | 79 .then((_) { |
61 expect(testDiv.nodes.length, 1); | 80 expect(testDiv.children.length, 2); |
62 expect(testDiv.nodes[0].id, 'test'); | 81 expect(testDiv.children[1].text, 'a'); |
63 }); | 82 expect(testScopeFactory.scopeCount, 1); |
64 | 83 })); |
65 test('should silently handle bad variable names', () { | 84 |
66 var completer = new Completer(); | 85 test('should only create a single scope for two bindings', () => |
67 runZoned(() { | 86 setUpTest(''' |
68 testDiv.nodes.add(new Element.html(''' | 87 <template id="test" bind> |
69 <template id="test" bind>{{ foo }}</template>''')); | 88 <div>{{ data }}</div> |
70 templateBind(querySelector('#test')) | 89 <div>{{ data }}</div> |
71 ..bindingDelegate = new PolymerExpressions() | 90 </template>''', |
72 ..model = []; | 91 model: new Model('a')) |
73 return new Future(() {}); | 92 .then((_) { |
74 }, onError: (e, s) { | 93 expect(testDiv.children.length, 3); |
75 expect('$e', contains('foo')); | 94 expect(testDiv.children[1].text, 'a'); |
76 completer.complete(true); | 95 expect(testDiv.children[2].text, 'a'); |
77 }); | 96 expect(testScopeFactory.scopeCount, 1); |
78 return completer.future; | 97 })); |
79 }); | 98 |
99 test('should create a new scope for a bind/as binding', () { | |
100 return setUpTest(''' | |
101 <template id="test" bind> | |
102 <div>{{ data }}</div> | |
103 <template bind="{{ data as a }}" id="inner"> | |
104 <div>{{ a }}</div> | |
105 <div>{{ data }}</div> | |
106 </template> | |
107 </template>''', | |
108 model: new Model('foo')) | |
109 .then((_) { | |
110 expect(testDiv.children.length, 5); | |
111 expect(testDiv.children[1].text, 'foo'); | |
112 expect(testDiv.children[3].text, 'foo'); | |
113 expect(testDiv.children[4].text, 'foo'); | |
114 expect(testScopeFactory.scopeCount, 2); | |
115 }); | |
116 }); | |
117 | |
118 test('should create scopes for a repeat/in binding', () { | |
119 return setUpTest(''' | |
120 <template id="test" bind> | |
121 <div>{{ data }}</div> | |
122 <template repeat="{{ i in items }}" id="inner"> | |
123 <div>{{ i }}</div> | |
124 <div>{{ data }}</div> | |
125 </template> | |
126 </template>''', | |
127 model: new Model('foo'), globals: {'items': ['a', 'b', 'c']}) | |
128 .then((_) { | |
129 expect(testDiv.children.length, 9); | |
130 expect(testDiv.children[1].text, 'foo'); | |
131 expect(testDiv.children[3].text, 'a'); | |
132 expect(testDiv.children[4].text, 'foo'); | |
133 expect(testDiv.children[5].text, 'b'); | |
134 expect(testDiv.children[6].text, 'foo'); | |
135 expect(testDiv.children[7].text, 'c'); | |
136 expect(testDiv.children[8].text, 'foo'); | |
137 // 1 scopes for <template bind>, 1 for each repeat | |
138 expect(testScopeFactory.scopeCount, 4); | |
139 }); | |
140 }); | |
141 | |
142 | |
143 }); | |
144 | |
145 group('with template bind', () { | |
146 | |
147 test('should show a simple binding on the model', () => | |
148 setUpTest(''' | |
149 <template id="test" bind> | |
150 <div>{{ data }}</div> | |
151 </template>''', | |
152 model: new Model('a')) | |
153 .then((_) { | |
154 expect(testDiv.children.length, 2); | |
155 expect(testDiv.children[1].text, 'a'); | |
156 })); | |
157 | |
158 test('should handle an empty binding on the model', () => | |
159 setUpTest(''' | |
160 <template id="test" bind> | |
161 <div>{{ }}</div> | |
162 </template>''', | |
163 model: 'a') | |
164 .then((_) { | |
165 expect(testDiv.children.length, 2); | |
166 expect(testDiv.children[1].text, 'a'); | |
167 })); | |
168 | |
169 test('should show a simple binding to a global', () => | |
170 setUpTest(''' | |
171 <template id="test" bind> | |
172 <div>{{ a }}</div> | |
173 </template>''', | |
174 globals: {'a': '123'}) | |
175 .then((_) { | |
176 expect(testDiv.children.length, 2); | |
177 expect(testDiv.children[1].text, '123'); | |
178 })); | |
179 | |
180 test('should show an expression binding', () => | |
181 setUpTest(''' | |
182 <template id="test" bind> | |
183 <div>{{ data + 'b' }}</div> | |
184 </template>''', | |
185 model: new Model('a')) | |
186 .then((_) { | |
187 expect(testDiv.children.length, 2); | |
188 expect(testDiv.children[1].text, 'ab'); | |
189 })); | |
190 | |
191 test('should handle an expression in the bind attribute', () => | |
192 setUpTest(''' | |
193 <template id="test" bind="{{ data }}"> | |
194 <div>{{ this }}</div> | |
195 </template>''', | |
196 model: new Model('a')) | |
197 .then((_) { | |
198 expect(testDiv.children.length, 2); | |
199 expect(testDiv.children[1].text, 'a'); | |
200 })); | |
201 | |
202 test('should handle a nested template with an expression in the bind ' | |
203 'attribute', () => | |
204 setUpTest(''' | |
205 <template id="test" bind> | |
206 <template id="inner" bind="{{ data }}"> | |
207 <div>{{ this }}</div> | |
208 </template> | |
209 </template>''', | |
210 model: new Model('a')) | |
211 .then((_) { | |
212 expect(testDiv.children.length, 3); | |
213 expect(testDiv.children[2].text, 'a'); | |
214 })); | |
215 | |
216 | |
217 test('should handle an "as" expression in the bind attribute', () => | |
218 setUpTest(''' | |
219 <template id="test" bind="{{ data as a }}"> | |
220 <div>{{ data }}b</div> | |
221 <div>{{ a }}c</div> | |
222 </template>''', | |
223 model: new Model('a')) | |
224 .then((_) { | |
225 expect(testDiv.children.length, 3); | |
226 expect(testDiv.children[1].text, 'ab'); | |
227 expect(testDiv.children[2].text, 'ac'); | |
228 })); | |
229 | |
230 test('should not resolve names in the outer template from within a nested' | |
231 ' template with a bind binding', () { | |
232 var completer = new Completer(); | |
233 var bindingErrorHappened = false; | |
234 var templateRendered = false; | |
235 maybeComplete() { | |
236 if (bindingErrorHappened && templateRendered) { | |
237 completer.complete(true); | |
238 } | |
239 } | |
240 runZoned(() { | |
241 setUpTest(''' | |
242 <template id="test" bind> | |
243 <div>{{ data }}</div> | |
244 <div>{{ b }}</div> | |
245 <template id="inner" bind="{{ b }}"> | |
246 <div>{{ data }}</div> | |
247 <div>{{ b }}</div> | |
248 <div>{{ this }}</div> | |
249 </template> | |
250 </template>''', | |
251 model: new Model('foo'), globals: {'b': 'bbb'}) | |
252 .then((_) { | |
253 expect(testDiv.children.map((c) => c.text), | |
254 ['', 'foo', 'bbb', '', '', 'bbb', 'bbb']); | |
255 templateRendered = true; | |
256 maybeComplete(); | |
257 }); | |
258 }, onError: (e, s) { | |
259 expect('$e', contains('data')); | |
260 bindingErrorHappened = true; | |
261 maybeComplete(); | |
262 }); | |
263 return completer.future; | |
264 }); | |
265 | |
266 test('should shadow names in the outer template from within a nested ' | |
267 'template', () => | |
268 setUpTest(''' | |
269 <template id="test" bind> | |
270 <div>{{ a }}</div> | |
271 <div>{{ b }}</div> | |
272 <template bind="{{ b as a }}"> | |
273 <div>{{ a }}</div> | |
274 <div>{{ b }}</div> | |
275 </template> | |
276 </template>''', | |
277 globals: {'a': 'aaa', 'b': 'bbb'}) | |
278 .then((_) { | |
279 expect(testDiv.children.map((c) => c.text), | |
280 ['', 'aaa', 'bbb', '', 'bbb', 'bbb']); | |
281 })); | |
282 | |
283 }); | |
284 | |
285 group('with template repeat', () { | |
286 | |
287 test('should not resolve names in the outer template from within a nested' | |
288 ' template with a repeat binding', () { | |
289 var completer = new Completer(); | |
290 var bindingErrorHappened = false; | |
291 var templateRendered = false; | |
292 maybeComplete() { | |
293 if (bindingErrorHappened && templateRendered) { | |
294 completer.complete(true); | |
295 } | |
296 } | |
297 runZoned(() { | |
298 setUpTest(''' | |
299 <template id="test" bind> | |
300 <div>{{ data }}</div> | |
301 <template repeat="{{ items }}"> | |
302 <div>{{ }}{{ data }}</div> | |
303 </template> | |
304 </template>''', | |
305 globals: {'items': [1, 2, 3]}, | |
306 model: new Model('a')) | |
307 .then((_) { | |
308 expect(testDiv.children.map((c) => c.text), | |
309 ['', 'a', '', '1', '2', '3']); | |
310 templateRendered = true; | |
311 maybeComplete(); | |
312 }); | |
313 }, onError: (e, s) { | |
314 expect('$e', contains('data')); | |
315 bindingErrorHappened = true; | |
316 maybeComplete(); | |
317 }); | |
318 return completer.future; | |
319 }); | |
320 | |
321 test('should handle repeat/in bindings', () => | |
322 setUpTest(''' | |
323 <template id="test" bind> | |
324 <div>{{ data }}</div> | |
325 <template repeat="{{ item in items }}"> | |
326 <div>{{ item }}{{ data }}</div> | |
327 </template> | |
328 </template>''', | |
329 globals: {'items': [1, 2, 3]}, | |
330 model: new Model('a')) | |
331 .then((_) { | |
332 // expect 6 children: two templates, a div and three instances | |
333 expect(testDiv.children.map((c) => c.text), | |
334 ['', 'a', '', '1a', '2a', '3a']); | |
335 })); | |
336 | |
337 test('should observe changes to lists in repeat bindings', () { | |
338 var items = new ObservableList.from([1, 2, 3]); | |
339 return setUpTest(''' | |
340 <template id="test" bind> | |
341 <template repeat="{{ items }}"> | |
342 <div>{{ }}</div> | |
343 </template> | |
344 </template>''', | |
345 globals: {'items': items}, | |
346 model: new Model('a')) | |
347 .then((_) { | |
348 expect(testDiv.children.map((c) => c.text), | |
349 ['', '', '1', '2', '3']); | |
350 items.add(4); | |
351 return waitForChange(testDiv); | |
352 }).then((_) { | |
353 expect(testDiv.children.map((c) => c.text), | |
354 ['', '', '1', '2', '3', '4']); | |
355 }); | |
356 }); | |
357 | |
358 test('should observe changes to lists in repeat/in bindings', () { | |
359 var items = new ObservableList.from([1, 2, 3]); | |
360 return setUpTest(''' | |
361 <template id="test" bind> | |
362 <template repeat="{{ item in items }}"> | |
363 <div>{{ item }}</div> | |
364 </template> | |
365 </template>''', | |
366 globals: {'items': items}, | |
367 model: new Model('a')) | |
368 .then((_) { | |
369 expect(testDiv.children.map((c) => c.text), | |
370 ['', '', '1', '2', '3']); | |
371 items.add(4); | |
372 return waitForChange(testDiv); | |
373 }).then((_) { | |
374 expect(testDiv.children.map((c) => c.text), | |
375 ['', '', '1', '2', '3', '4']); | |
376 }); | |
377 }); | |
378 }); | |
379 | |
380 group('with template if', () { | |
381 | |
382 Future doTest(value, bool shouldRender) => | |
383 setUpTest(''' | |
384 <template id="test" bind> | |
385 <div>{{ data }}</div> | |
386 <template if="{{ show }}"> | |
387 <div>{{ data }}</div> | |
388 </template> | |
389 </template>''', | |
390 globals: {'show': value}, | |
391 model: new Model('a')) | |
392 .then((_) { | |
393 if (shouldRender) { | |
394 expect(testDiv.children.length, 4); | |
395 expect(testDiv.children[1].text, 'a'); | |
396 expect(testDiv.children[3].text, 'a'); | |
397 } else { | |
398 expect(testDiv.children.length, 3); | |
399 expect(testDiv.children[1].text, 'a'); | |
400 } | |
401 }); | |
402 | |
403 test('should render for a true expression', | |
404 () => doTest(true, true)); | |
405 | |
406 test('should treat a non-null expression as truthy', | |
407 () => doTest('a', true)); | |
408 | |
409 test('should treat an empty list as truthy', | |
410 () => doTest([], true)); | |
411 | |
412 test('should handle a false expression', | |
413 () => doTest(false, false)); | |
414 | |
415 test('should treat null as falsey', | |
416 () => doTest(null, false)); | |
417 }); | |
418 | |
419 group('error handling', () { | |
420 | |
421 test('should silently handle bad variable names', () { | |
422 var completer = new Completer(); | |
423 runZoned(() { | |
424 testDiv.nodes.add(new Element.html(''' | |
425 <template id="test" bind>{{ foo }}</template>''')); | |
426 templateBind(query('#test')) | |
427 ..bindingDelegate = new PolymerExpressions() | |
428 ..model = []; | |
429 return new Future(() {}); | |
430 }, onError: (e, s) { | |
431 expect('$e', contains('foo')); | |
432 completer.complete(true); | |
433 }); | |
434 return completer.future; | |
435 }); | |
436 | |
437 test('should handle null collections in "in" expressions', () => | |
438 setUpTest(''' | |
439 <template id="test" bind> | |
440 <template repeat="{{ item in items }}"> | |
441 {{ item }} | |
442 </template> | |
443 </template>''', | |
444 globals: {'items': null}) | |
445 .then((_) { | |
446 expect(testDiv.children.length, 2); | |
447 expect(testDiv.children[0].id, 'test'); | |
448 })); | |
449 | |
450 }); | |
451 | |
452 group('special bindings', () { | |
453 | |
454 test('should handle class attributes with lists', () => | |
455 setUpTest(''' | |
456 <template id="test" bind> | |
457 <div class="{{ classes }}"> | |
458 </template>''', | |
459 globals: {'classes': ['a', 'b']}) | |
460 .then((_) { | |
461 expect(testDiv.children.length, 2); | |
462 expect(testDiv.children[1].attributes['class'], 'a b'); | |
463 expect(testDiv.children[1].classes, ['a', 'b']); | |
464 })); | |
465 | |
466 test('should handle class attributes with maps', () => | |
467 setUpTest(''' | |
468 <template id="test" bind> | |
469 <div class="{{ classes }}"> | |
470 </template>''', | |
471 globals: {'classes': {'a': true, 'b': false, 'c': true}}) | |
472 .then((_) { | |
473 expect(testDiv.children.length, 2); | |
474 expect(testDiv.children[1].attributes['class'], 'a c'); | |
475 expect(testDiv.children[1].classes, ['a', 'c']); | |
476 })); | |
477 | |
478 test('should handle style attributes with lists', () => | |
479 setUpTest(''' | |
480 <template id="test" bind> | |
481 <div style="{{ styles }}"> | |
482 </template>''', | |
483 globals: {'styles': ['display: none', 'color: black']}) | |
484 .then((_) { | |
485 expect(testDiv.children.length, 2); | |
486 expect(testDiv.children[1].attributes['style'], | |
487 'display: none;color: black'); | |
488 })); | |
489 | |
490 test('should handle style attributes with maps', () => | |
491 setUpTest(''' | |
492 <template id="test" bind> | |
493 <div style="{{ styles }}"> | |
494 </template>''', | |
495 globals: {'styles': {'display': 'none', 'color': 'black'}}) | |
496 .then((_) { | |
497 expect(testDiv.children.length, 2); | |
498 expect(testDiv.children[1].attributes['style'], | |
499 'display: none;color: black'); | |
500 })); | |
501 }); | |
502 | |
503 group('regression tests', () { | |
504 | |
505 test('should bind to literals', () => | |
506 setUpTest(''' | |
507 <template id="test" bind> | |
508 <div>{{ 123 }}</div> | |
509 <div>{{ 123.456 }}</div> | |
510 <div>{{ "abc" }}</div> | |
511 <div>{{ true }}</div> | |
512 <div>{{ null }}</div> | |
513 </template>''', | |
514 globals: {'items': null}) | |
515 .then((_) { | |
516 expect(testDiv.children.length, 6); | |
517 expect(testDiv.children[1].text, '123'); | |
518 expect(testDiv.children[2].text, '123.456'); | |
519 expect(testDiv.children[3].text, 'abc'); | |
520 expect(testDiv.children[4].text, 'true'); | |
521 expect(testDiv.children[5].text, ''); | |
522 })); | |
523 | |
524 }); | |
525 | |
80 }); | 526 }); |
81 } | 527 } |
82 | 528 |
529 Future<Element> waitForChange(Element e) { | |
530 var completer = new Completer<Element>(); | |
531 new MutationObserver((mutations, observer) { | |
532 observer.disconnect(); | |
533 completer.complete(e); | |
534 }).observe(e, childList: true); | |
535 return completer.future.timeout(new Duration(seconds: 1)); | |
536 } | |
537 | |
83 @reflectable | 538 @reflectable |
84 class Person extends ChangeNotifier { | 539 class Model extends ChangeNotifier { |
85 String _firstName; | 540 String _data; |
86 String _lastName; | 541 |
87 List<String> _items; | 542 Model(this._data); |
88 | 543 |
89 Person(this._firstName, this._lastName, this._items); | 544 String get data => _data; |
90 | 545 |
91 String get firstName => _firstName; | 546 void set data(String value) { |
92 | 547 _data = notifyPropertyChange(#data, _data, value); |
93 void set firstName(String value) { | |
94 _firstName = notifyPropertyChange(#firstName, _firstName, value); | |
95 } | 548 } |
96 | 549 |
97 String get lastName => _lastName; | 550 String toString() => "Model(data: $_data)"; |
98 | |
99 void set lastName(String value) { | |
100 _lastName = notifyPropertyChange(#lastName, _lastName, value); | |
101 } | |
102 | |
103 String getFullName() => '$_firstName $_lastName'; | |
104 | |
105 List<String> get items => _items; | |
106 | |
107 void set items(List<String> value) { | |
108 _items = notifyPropertyChange(#items, _items, value); | |
109 } | |
110 | |
111 String toString() => "Person(firstName: $_firstName, lastName: $_lastName)"; | |
112 } | 551 } |
552 | |
553 class NullNodeTreeSanitizer implements NodeTreeSanitizer { | |
554 | |
555 @override | |
556 void sanitizeTree(Node node) {} | |
557 } | |
OLD | NEW |