Chromium Code Reviews| 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 |