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