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 library bindings_test; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:html'; | |
9 | |
10 import 'package:observe/observe.dart'; | |
11 import 'package:observe/mirrors_used.dart'; // make test smaller. | |
12 import 'package:observe/src/dirty_check.dart' show dirtyCheckZone; | |
13 import 'package:polymer_expressions/polymer_expressions.dart'; | |
14 import 'package:smoke/mirrors.dart' as smoke; | |
15 import 'package:template_binding/template_binding.dart' show | |
16 TemplateBindExtension, templateBind; | |
17 import 'package:unittest/html_config.dart'; | |
18 | |
19 import 'package:unittest/unittest.dart'; | |
20 | |
21 var testDiv; | |
22 | |
23 main() => dirtyCheckZone().run(() { | |
24 useHtmlConfiguration(); | |
25 smoke.useMirrors(); | |
26 | |
27 group('bindings', () { | |
28 var stop = null; | |
29 setUp(() { | |
30 document.body.append(testDiv = new DivElement()); | |
31 }); | |
32 | |
33 tearDown(() { | |
34 testDiv.remove(); | |
35 testDiv = null; | |
36 }); | |
37 | |
38 test('should update binding when data changes', () { | |
39 var model = new NotifyModel(); | |
40 var binding = new PolymerExpressions() | |
41 .prepareBinding('x', null, null)(model, null, false); | |
42 expect(binding.value, isNull); | |
43 model.x = "hi"; | |
44 return new Future(() { | |
45 expect(binding.value, 'hi'); | |
46 }); | |
47 }); | |
48 | |
49 // regression test for issue 19296 | |
50 test('should not throw when data changes', () { | |
51 var model = new NotifyModel(); | |
52 testDiv.append(_createTemplateInstance( | |
53 '<template repeat="{{ i in x }}">{{ i }}</template>', model)); | |
54 | |
55 return new Future(() { | |
56 model.x = [1, 2, 3]; | |
57 }).then(_nextMicrotask).then((_) { | |
58 expect(testDiv.text,'123'); | |
59 }); | |
60 }); | |
61 | |
62 | |
63 test('should update text content when data changes', () { | |
64 var model = new NotifyModel('abcde'); | |
65 testDiv.append(_createTemplateInstance('<span>{{x}}</span>', model)); | |
66 | |
67 var el; | |
68 return new Future(() { | |
69 el = testDiv.query("span"); | |
70 expect(el.text, 'abcde'); | |
71 expect(model.x, 'abcde'); | |
72 model.x = '___'; | |
73 }).then(_nextMicrotask).then((_) { | |
74 expect(model.x, '___'); | |
75 expect(el.text, '___'); | |
76 }); | |
77 }); | |
78 | |
79 test('should log eval exceptions', () { | |
80 var model = new NotifyModel('abcde'); | |
81 var completer = new Completer(); | |
82 runZoned(() { | |
83 testDiv.append(_createTemplateInstance('<span>{{foo}}</span>', model)); | |
84 return _nextMicrotask(null); | |
85 }, onError: (e) { | |
86 expect('$e', startsWith("Error evaluating expression 'foo':")); | |
87 completer.complete(true); | |
88 }); | |
89 return completer.future; | |
90 }); | |
91 | |
92 test('detects changes to ObservableList', () { | |
93 var list = new ObservableList.from([1, 2, 3]); | |
94 var model = new NotifyModel(list); | |
95 testDiv.append(_createTemplateInstance('{{x[1]}}', model)); | |
96 | |
97 return new Future(() { | |
98 expect(testDiv.text, '2'); | |
99 list[1] = 10; | |
100 }).then(_nextMicrotask).then((_) { | |
101 expect(testDiv.text, '10'); | |
102 list[1] = 11; | |
103 }).then(_nextMicrotask).then((_) { | |
104 expect(testDiv.text, '11'); | |
105 list[0] = 9; | |
106 }).then(_nextMicrotask).then((_) { | |
107 expect(testDiv.text, '11'); | |
108 list.removeAt(0); | |
109 }).then(_nextMicrotask).then((_) { | |
110 expect(testDiv.text, '3'); | |
111 list.add(90); | |
112 list.removeAt(0); | |
113 }).then(_nextMicrotask).then((_) { | |
114 expect(testDiv.text, '90'); | |
115 }); | |
116 }); | |
117 | |
118 // Regression tests for issue 18792. | |
119 for (var usePolymer in [true, false]) { | |
120 // We run these tests both with PolymerExpressions and with the default | |
121 // delegate to ensure the results are consistent. When possible, the | |
122 // expressions on these tests use syntax common to both delegates. | |
123 var name = usePolymer ? 'polymer-expressions' : 'default'; | |
124 group('$name delegate', () { | |
125 // Use <option template repeat="{{y}}" value="{{}}">item {{}} | |
126 _initialSelectTest('{{y}}', '{{}}', usePolymer); | |
127 _updateSelectTest('{{y}}', '{{}}', usePolymer); | |
128 _detectKeyValueChanges(usePolymer); | |
129 if (usePolymer) _detectKeyValueChangesPolymerSyntax(); | |
130 _cursorPositionTest(usePolymer); | |
131 }); | |
132 } | |
133 | |
134 group('polymer-expressions delegate, polymer syntax', () { | |
135 // Use <option template repeat="{{i in y}}" value="{{i}}">item {{i}} | |
136 _initialSelectTest('{{i in y}}', '{{i}}', true); | |
137 _updateSelectTest('{{i in y}}', '{{i}}', true); | |
138 }); | |
139 }); | |
140 }); | |
141 | |
142 | |
143 _cursorPositionTest(bool usePolymer) { | |
144 test('should preserve the cursor position', () { | |
145 var model = new NotifyModel('abcde'); | |
146 testDiv.append(_createTemplateInstance( | |
147 '<input id="i1" value={{x}}>', model, usePolymer: usePolymer)); | |
148 var el; | |
149 return new Future(() { | |
150 el = testDiv.query("#i1"); | |
151 var subscription = el.onInput.listen(expectAsync((_) {}, count: 1)); | |
152 el.focus(); | |
153 | |
154 expect(el.value, 'abcde'); | |
155 expect(model.x, 'abcde'); | |
156 | |
157 el.selectionStart = 3; | |
158 el.selectionEnd = 3; | |
159 expect(el.selectionStart, 3); | |
160 expect(el.selectionEnd, 3); | |
161 | |
162 el.value = 'abc de'; | |
163 // Updating the input value programmatically (even to the same value in | |
164 // Chrome) loses the selection position. | |
165 expect(el.selectionStart, 6); | |
166 expect(el.selectionEnd, 6); | |
167 | |
168 el.selectionStart = 4; | |
169 el.selectionEnd = 4; | |
170 | |
171 expect(model.x, 'abcde'); | |
172 el.dispatchEvent(new Event('input')); | |
173 expect(model.x, 'abc de'); | |
174 expect(el.value, 'abc de'); | |
175 | |
176 // But propagating observable values through reassign the value and | |
177 // selection will be preserved. | |
178 expect(el.selectionStart, 4); | |
179 expect(el.selectionEnd, 4); | |
180 subscription.cancel(); | |
181 }).then(_nextMicrotask).then((_) { | |
182 // Nothing changes on the next micro task. | |
183 expect(el.selectionStart, 4); | |
184 expect(el.selectionEnd, 4); | |
185 }).then((_) => window.animationFrame).then((_) { | |
186 // ... or on the next animation frame. | |
187 expect(el.selectionStart, 4); | |
188 expect(el.selectionEnd, 4); | |
189 }).then(_afterTimeout).then((_) { | |
190 // ... or later. | |
191 expect(el.selectionStart, 4); | |
192 expect(el.selectionEnd, 4); | |
193 }); | |
194 }); | |
195 } | |
196 | |
197 _initialSelectTest(String repeatExp, String valueExp, bool usePolymer) { | |
198 test('initial select value is set correctly', () { | |
199 var list = const ['a', 'b']; | |
200 var model = new NotifyModel('b', list); | |
201 testDiv.append(_createTemplateInstance('<select value="{{x}}">' | |
202 '<option template repeat="$repeatExp" value="$valueExp">item $valueExp' | |
203 '</option></select>', | |
204 model, usePolymer: usePolymer)); | |
205 | |
206 expect(testDiv.querySelector('select').value, 'b'); | |
207 return new Future(() { | |
208 expect(model.x, 'b'); | |
209 expect(testDiv.querySelector('select').value, 'b'); | |
210 }); | |
211 }); | |
212 } | |
213 | |
214 _updateSelectTest(String repeatExp, String valueExp, bool usePolymer) { | |
215 test('updates to select value propagate correctly', () { | |
216 var list = const ['a', 'b']; | |
217 var model = new NotifyModel('a', list); | |
218 | |
219 testDiv.append(_createTemplateInstance('<select value="{{x}}">' | |
220 '<option template repeat="$repeatExp" value="$valueExp">item $valueExp' | |
221 '</option></select></template>', model, usePolymer: usePolymer)); | |
222 | |
223 expect(testDiv.querySelector('select').value, 'a'); | |
224 return new Future(() { | |
225 expect(testDiv.querySelector('select').value, 'a'); | |
226 model.x = 'b'; | |
227 }).then(_nextMicrotask).then((_) { | |
228 expect(testDiv.querySelector('select').value, 'b'); | |
229 }); | |
230 }); | |
231 } | |
232 | |
233 _detectKeyValueChanges(bool usePolymer) { | |
234 test('detects changes to ObservableMap keys', () { | |
235 var map = new ObservableMap.from({'a': 1, 'b': 2}); | |
236 var model = new NotifyModel(map); | |
237 testDiv.append(_createTemplateInstance( | |
238 '<template repeat="{{x.keys}}">{{}},</template>', | |
239 model, usePolymer: usePolymer)); | |
240 | |
241 return new Future(() { | |
242 expect(testDiv.text, 'a,b,'); | |
243 map.remove('b'); | |
244 map['c'] = 3; | |
245 }).then(_nextMicrotask).then((_) { | |
246 expect(testDiv.text, 'a,c,'); | |
247 map['a'] = 4; | |
248 }).then(_nextMicrotask).then((_) { | |
249 expect(testDiv.text, 'a,c,'); | |
250 }); | |
251 }); | |
252 } | |
253 | |
254 // This test uses 'in', which is a polymer_expressions only feature. | |
255 _detectKeyValueChangesPolymerSyntax() { | |
256 test('detects changes to ObservableMap values', () { | |
257 var map = new ObservableMap.from({'a': 1, 'b': 2}); | |
258 var model = new NotifyModel(map); | |
259 testDiv.append(_createTemplateInstance( | |
260 '<template repeat="{{k in x.keys}}">{{x[k]}},</template>', model)); | |
261 | |
262 return new Future(() { | |
263 expect(testDiv.text, '1,2,'); | |
264 map.remove('b'); | |
265 map['c'] = 3; | |
266 }).then(_nextMicrotask).then((_) { | |
267 expect(testDiv.text, '1,3,'); | |
268 map['a'] = 4; | |
269 }).then(_nextMicrotask).then((_) { | |
270 expect(testDiv.text, '4,3,'); | |
271 }); | |
272 }); | |
273 } | |
274 | |
275 _createTemplateInstance(String templateBody, model, {bool usePolymer: true}) { | |
276 var tag = new Element.html('<template>$templateBody</template>', | |
277 treeSanitizer: _nullTreeSanitizer); | |
278 TemplateBindExtension.bootstrap(tag); | |
279 var template = templateBind(tag); | |
280 var delegate = usePolymer ? new PolymerExpressions() : null; | |
281 return template.createInstance(model, delegate); | |
282 } | |
283 | |
284 _nextMicrotask(_) => new Future(() {}); | |
285 _afterTimeout(_) => new Future.delayed(new Duration(milliseconds: 30), () {}); | |
286 | |
287 @reflectable | |
288 class NotifyModel extends ChangeNotifier { | |
289 var _x; | |
290 var _y; | |
291 NotifyModel([this._x, this._y]); | |
292 | |
293 get x => _x; | |
294 set x(value) { | |
295 _x = notifyPropertyChange(#x, _x, value); | |
296 } | |
297 | |
298 get y => _y; | |
299 set y(value) { | |
300 _y = notifyPropertyChange(#y, _y, value); | |
301 } | |
302 } | |
303 | |
304 class _NullTreeSanitizer implements NodeTreeSanitizer { | |
305 void sanitizeTree(Node node) {} | |
306 } | |
307 final _nullTreeSanitizer = new _NullTreeSanitizer(); | |
OLD | NEW |