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

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: Roll to latest version, simplified much of the scope creation Created 6 years, 8 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_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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698