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

Side by Side 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, 6 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 unified diff | Download patch | Annotate | Revision Log
OLDNEW
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_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 useHtmlConfiguration();
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
34 </template>''')); 53 // make sure templates behave in the polyfill
35 var person = new Person('John', 'Messerly', ['A', 'B', 'C']); 54 TemplateBindExtension.bootstrap(tag);
36 templateBind(querySelector('#test')) 55
37 ..bindingDelegate = new PolymerExpressions() 56 templateBind(tag)
38 ..model = person; 57 ..bindingDelegate = new PolymerExpressions(globals: globals,
39 return new Future(() {}).then((_) { 58 scopeFactory: testScopeFactory)
40 InputElement input = querySelector('#input'); 59 ..model = model;
41 expect(input.value, 'John'); 60 testDiv.children.clear();
42 input.focus(); 61 testDiv.append(tag);
43 input.value = 'Justin'; 62 return waitForChange(testDiv);
44 input.blur(); 63 }
45 var event = new Event('change'); 64
46 // TODO(justin): figure out how to trigger keyboard events to test 65 group('scope creation', () {
47 // two-way bindings 66 // These tests are sensitive to some internals of the implementation that
48 }); 67 // might not be visible to applications, but are useful for verifying that
49 }); 68 // that we're not creating too many Scopes.
50 69
51 test('should handle null collections in "in" expressions', () { 70 // The reason that we create two Scopes in the cases with one binding is
52 testDiv.nodes.add(new Element.html(''' 71 // that <template bind> has one scope for the context to evaluate the bind
53 <template id="test" bind> 72 // binding in, and another scope for the bindings inside the template.
54 <template repeat="{{ item in items }}"> 73
55 {{ item }} 74 // We could try to optimize the outer scope away in cases where the
56 </template> 75 // expression is empty, but there are a lot of special cases in the
57 </template>''')); 76 // syntax code already.
58 templateBind(querySelector('#test')).bindingDelegate = 77 test('should create one scope for a single binding', () =>
59 new PolymerExpressions(globals: {'items': null}); 78 setUpTest('''
60 // the template should be the only node 79 <template id="test" bind>
61 expect(testDiv.nodes.length, 1); 80 <div>{{ data }}</div>
62 expect(testDiv.nodes[0].id, 'test'); 81 </template>''',
63 }); 82 model: new Model('a'))
64 83 .then((_) {
65 test('should silently handle bad variable names', () { 84 expect(testDiv.children.length, 2);
66 var completer = new Completer(); 85 expect(testDiv.children[1].text, 'a');
67 runZoned(() { 86 expect(testScopeFactory.scopeCount, 1);
68 testDiv.nodes.add(new Element.html(''' 87 }));
69 <template id="test" bind>{{ foo }}</template>''')); 88
70 templateBind(querySelector('#test')) 89 test('should only create a single scope for two bindings', () =>
71 ..bindingDelegate = new PolymerExpressions() 90 setUpTest('''
72 ..model = []; 91 <template id="test" bind>
73 return new Future(() {}); 92 <div>{{ data }}</div>
74 }, onError: (e, s) { 93 <div>{{ data }}</div>
75 expect('$e', contains('foo')); 94 </template>''',
76 completer.complete(true); 95 model: new Model('a'))
77 }); 96 .then((_) {
78 return completer.future; 97 expect(testDiv.children.length, 3);
79 }); 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
80 }); 578 });
81 } 579 }
82 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
83 @reflectable 590 @reflectable
84 class Person extends ChangeNotifier { 591 class Model extends ChangeNotifier {
85 String _firstName; 592 String _data;
86 String _lastName; 593
87 List<String> _items; 594 Model(this._data);
88 595
89 Person(this._firstName, this._lastName, this._items); 596 String get data => _data;
90 597
91 String get firstName => _firstName; 598 void set data(String value) {
92 599 _data = notifyPropertyChange(#data, _data, value);
93 void set firstName(String value) {
94 _firstName = notifyPropertyChange(#firstName, _firstName, value);
95 } 600 }
96 601
97 String get lastName => _lastName; 602 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 } 603 }
604
605 class NullNodeTreeSanitizer implements NodeTreeSanitizer {
606
607 @override
608 void sanitizeTree(Node node) {}
609 }
OLDNEW
« 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