OLD | NEW |
1 library watch_group_spec; | 1 library watch_group_spec; |
2 | 2 |
3 import '../_specs.dart'; | 3 import '../_specs.dart'; |
| 4 import 'dart:collection'; |
4 import 'package:angular/change_detection/watch_group.dart'; | 5 import 'package:angular/change_detection/watch_group.dart'; |
5 import 'package:angular/change_detection/dirty_checking_change_detector.dart'; | 6 import 'package:angular/change_detection/dirty_checking_change_detector.dart'; |
| 7 import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.
dart'; |
6 import 'dirty_checking_change_detector_spec.dart' hide main; | 8 import 'dirty_checking_change_detector_spec.dart' hide main; |
7 | 9 import 'package:angular/core/parser/parser_dynamic.dart' show DynamicClosureMap; |
8 main() => describe('WatchGroup', () { | 10 |
9 var context; | 11 class TestData { |
10 var watchGrp; | 12 sub1(a, {b: 0}) => a - b; |
11 DirtyCheckingChangeDetector changeDetector; | 13 sub2({a: 0, b: 0}) => a - b; |
12 Logger logger; | 14 } |
13 | 15 |
14 AST parse(String expression) { | 16 void main() { |
15 var currentAST = new ContextReferenceAST(); | 17 describe('WatchGroup', () { |
16 expression.split('.').forEach((name) { | 18 var context; |
17 currentAST = new FieldReadAST(currentAST, name); | 19 var watchGrp; |
| 20 DirtyCheckingChangeDetector changeDetector; |
| 21 Logger logger; |
| 22 Parser parser; |
| 23 ExpressionVisitor visitor; |
| 24 |
| 25 beforeEach(inject((Logger _logger, Parser _parser) { |
| 26 context = {}; |
| 27 var getterFactory = new DynamicFieldGetterFactory(); |
| 28 changeDetector = new DirtyCheckingChangeDetector(getterFactory); |
| 29 watchGrp = new RootWatchGroup(getterFactory, changeDetector, context); |
| 30 visitor = new ExpressionVisitor(new DynamicClosureMap()); |
| 31 logger = _logger; |
| 32 parser = _parser; |
| 33 })); |
| 34 |
| 35 AST parse(String expression) => visitor.visit(parser(expression)); |
| 36 |
| 37 eval(String expression, [evalContext]) { |
| 38 AST ast = parse(expression); |
| 39 |
| 40 if (evalContext == null) evalContext = context; |
| 41 WatchGroup group = watchGrp.newGroup(evalContext); |
| 42 |
| 43 List log = []; |
| 44 Watch watch = group.watch(ast, (v, p) => log.add(v)); |
| 45 |
| 46 watchGrp.detectChanges(); |
| 47 group.remove(); |
| 48 |
| 49 if (log.isEmpty) { |
| 50 throw new StateError('Expression <$expression> was not evaluated'); |
| 51 } else if (log.length > 1) { |
| 52 throw new StateError('Expression <$expression> produced too many values:
$log'); |
| 53 } else { |
| 54 return log.first; |
| 55 } |
| 56 } |
| 57 |
| 58 expectOrder(list) { |
| 59 logger.clear(); |
| 60 watchGrp.detectChanges(); // Clear the initial queue |
| 61 logger.clear(); |
| 62 watchGrp.detectChanges(); |
| 63 expect(logger).toEqual(list); |
| 64 } |
| 65 |
| 66 beforeEach(inject((Logger _logger) { |
| 67 context = {}; |
| 68 var getterFactory = new DynamicFieldGetterFactory(); |
| 69 changeDetector = new DirtyCheckingChangeDetector(getterFactory); |
| 70 watchGrp = new RootWatchGroup(getterFactory, changeDetector, context); |
| 71 logger = _logger; |
| 72 })); |
| 73 |
| 74 describe('watch lifecycle', () { |
| 75 it('should prevent reaction fn on removed', () { |
| 76 context['a'] = 'hello'; |
| 77 var watch ; |
| 78 watchGrp.watch(parse('a'), (v, p) { |
| 79 logger('removed'); |
| 80 watch.remove(); |
| 81 }); |
| 82 watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); |
| 83 watchGrp.detectChanges(); |
| 84 expect(logger).toEqual(['removed']); |
| 85 }); |
18 }); | 86 }); |
19 return currentAST; | 87 |
20 } | 88 describe('property chaining', () { |
21 | 89 it('should read property', () { |
22 expectOrder(list) { | 90 context['a'] = 'hello'; |
23 logger.clear(); | 91 |
24 watchGrp.detectChanges(); // Clear the initial queue | 92 // should fire on initial adding |
25 logger.clear(); | 93 expect(watchGrp.fieldCost).toEqual(0); |
26 watchGrp.detectChanges(); | 94 var watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); |
27 expect(logger).toEqual(list); | 95 expect(watch.expression).toEqual('a'); |
28 } | 96 expect(watchGrp.fieldCost).toEqual(1); |
29 | 97 watchGrp.detectChanges(); |
30 beforeEach(inject((Logger _logger) { | 98 expect(logger).toEqual(['hello']); |
31 context = {}; | 99 |
32 changeDetector = new DirtyCheckingChangeDetector(new GetterCache({})); | 100 // make sore no new changes are logged on extra detectChanges |
33 watchGrp = new RootWatchGroup(changeDetector, context); | 101 watchGrp.detectChanges(); |
34 logger = _logger; | 102 expect(logger).toEqual(['hello']); |
35 })); | 103 |
36 | 104 // Should detect value change |
37 describe('watch lifecycle', () { | 105 context['a'] = 'bye'; |
38 it('should prevent reaction fn on removed', () { | 106 watchGrp.detectChanges(); |
39 context['a'] = 'hello'; | 107 expect(logger).toEqual(['hello', 'bye']); |
40 var watch ; | 108 |
41 watchGrp.watch(parse('a'), (v, p) { | 109 // should cleanup after itself |
42 logger('removed'); | |
43 watch.remove(); | 110 watch.remove(); |
44 }); | 111 expect(watchGrp.fieldCost).toEqual(0); |
45 watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); | 112 context['a'] = 'cant see me'; |
46 watchGrp.detectChanges(); | 113 watchGrp.detectChanges(); |
47 expect(logger).toEqual(['removed']); | 114 expect(logger).toEqual(['hello', 'bye']); |
| 115 }); |
| 116 |
| 117 describe('sequence mutations and ref changes', () { |
| 118 it('should handle a simultaneous map mutation and reference change', ()
{ |
| 119 context['a'] = context['b'] = {1: 10, 2: 20}; |
| 120 var watchA = watchGrp.watch(new CollectionAST(parse('a')), (v, p) => l
ogger(v)); |
| 121 var watchB = watchGrp.watch(new CollectionAST(parse('b')), (v, p) => l
ogger(v)); |
| 122 |
| 123 watchGrp.detectChanges(); |
| 124 expect(logger.length).toEqual(2); |
| 125 expect(logger[0], toEqualMapRecord( |
| 126 map: ['1', '2'], |
| 127 previous: ['1', '2'])); |
| 128 expect(logger[1], toEqualMapRecord( |
| 129 map: ['1', '2'], |
| 130 previous: ['1', '2'])); |
| 131 logger.clear(); |
| 132 |
| 133 // context['a'] is set to a copy with an addition. |
| 134 context['a'] = new Map.from(context['a'])..[3] = 30; |
| 135 // context['b'] still has the original collection. We'll mutate it. |
| 136 context['b'].remove(1); |
| 137 |
| 138 watchGrp.detectChanges(); |
| 139 expect(logger.length).toEqual(2); |
| 140 expect(logger[0], toEqualMapRecord( |
| 141 map: ['1', '2', '3[null -> 30]'], |
| 142 previous: ['1', '2'], |
| 143 additions: ['3[null -> 30]'])); |
| 144 expect(logger[1], toEqualMapRecord( |
| 145 map: ['2'], |
| 146 previous: ['1[10 -> null]', '2'], |
| 147 removals: ['1[10 -> null]'])); |
| 148 logger.clear(); |
| 149 }); |
| 150 |
| 151 it('should handle a simultaneous list mutation and reference change', ()
{ |
| 152 context['a'] = context['b'] = [0, 1]; |
| 153 var watchA = watchGrp.watch(new CollectionAST(parse('a')), (v, p) => l
ogger(v)); |
| 154 var watchB = watchGrp.watch(new CollectionAST(parse('b')), (v, p) => l
ogger(v)); |
| 155 |
| 156 watchGrp.detectChanges(); |
| 157 expect(logger.length).toEqual(2); |
| 158 expect(logger[0], toEqualCollectionRecord( |
| 159 collection: ['0', '1'], |
| 160 previous: ['0', '1'], |
| 161 additions: [], moves: [], removals: [])); |
| 162 expect(logger[1], toEqualCollectionRecord( |
| 163 collection: ['0', '1'], |
| 164 previous: ['0', '1'], |
| 165 additions: [], moves: [], removals: [])); |
| 166 logger.clear(); |
| 167 |
| 168 // context['a'] is set to a copy with an addition. |
| 169 context['a'] = context['a'].toList()..add(2); |
| 170 // context['b'] still has the original collection. We'll mutate it. |
| 171 context['b'].remove(0); |
| 172 |
| 173 watchGrp.detectChanges(); |
| 174 expect(logger.length).toEqual(2); |
| 175 expect(logger[0], toEqualCollectionRecord( |
| 176 collection: ['0', '1', '2[null -> 2]'], |
| 177 previous: ['0', '1'], |
| 178 additions: ['2[null -> 2]'], |
| 179 moves: [], |
| 180 removals: [])); |
| 181 expect(logger[1], toEqualCollectionRecord( |
| 182 collection: ['1[1 -> 0]'], |
| 183 previous: ['0[0 -> null]', '1[1 -> 0]'], |
| 184 additions: [], |
| 185 moves: ['1[1 -> 0]'], |
| 186 removals: ['0[0 -> null]'])); |
| 187 logger.clear(); |
| 188 }); |
| 189 |
| 190 it('should work correctly with UnmodifiableListView', () { |
| 191 context['a'] = new UnmodifiableListView([0, 1]); |
| 192 var watch = watchGrp.watch(new CollectionAST(parse('a')), (v, p) => lo
gger(v)); |
| 193 |
| 194 watchGrp.detectChanges(); |
| 195 expect(logger.length).toEqual(1); |
| 196 expect(logger[0], toEqualCollectionRecord( |
| 197 collection: ['0', '1'], |
| 198 previous: ['0', '1'])); |
| 199 logger.clear(); |
| 200 |
| 201 context['a'] = new UnmodifiableListView([1, 0]); |
| 202 |
| 203 watchGrp.detectChanges(); |
| 204 expect(logger.length).toEqual(1); |
| 205 expect(logger[0], toEqualCollectionRecord( |
| 206 collection: ['1[1 -> 0]', '0[0 -> 1]'], |
| 207 previous: ['0[0 -> 1]', '1[1 -> 0]'], |
| 208 moves: ['1[1 -> 0]', '0[0 -> 1]'])); |
| 209 logger.clear(); |
| 210 }); |
| 211 |
| 212 }); |
| 213 |
| 214 it('should read property chain', () { |
| 215 context['a'] = {'b': 'hello'}; |
| 216 |
| 217 // should fire on initial adding |
| 218 expect(watchGrp.fieldCost).toEqual(0); |
| 219 expect(changeDetector.count).toEqual(0); |
| 220 var watch = watchGrp.watch(parse('a.b'), (v, p) => logger(v)); |
| 221 expect(watch.expression).toEqual('a.b'); |
| 222 expect(watchGrp.fieldCost).toEqual(2); |
| 223 expect(changeDetector.count).toEqual(2); |
| 224 watchGrp.detectChanges(); |
| 225 expect(logger).toEqual(['hello']); |
| 226 |
| 227 // make sore no new changes are logged on extra detectChanges |
| 228 watchGrp.detectChanges(); |
| 229 expect(logger).toEqual(['hello']); |
| 230 |
| 231 // make sure no changes or logged when intermediary object changes |
| 232 context['a'] = {'b': 'hello'}; |
| 233 watchGrp.detectChanges(); |
| 234 expect(logger).toEqual(['hello']); |
| 235 |
| 236 // Should detect value change |
| 237 context['a'] = {'b': 'hello2'}; |
| 238 watchGrp.detectChanges(); |
| 239 expect(logger).toEqual(['hello', 'hello2']); |
| 240 |
| 241 // Should detect value change |
| 242 context['a']['b'] = 'bye'; |
| 243 watchGrp.detectChanges(); |
| 244 expect(logger).toEqual(['hello', 'hello2', 'bye']); |
| 245 |
| 246 // should cleanup after itself |
| 247 watch.remove(); |
| 248 expect(watchGrp.fieldCost).toEqual(0); |
| 249 context['a']['b'] = 'cant see me'; |
| 250 watchGrp.detectChanges(); |
| 251 expect(logger).toEqual(['hello', 'hello2', 'bye']); |
| 252 }); |
| 253 |
| 254 it('should reuse handlers', () { |
| 255 var user1 = {'first': 'misko', 'last': 'hevery'}; |
| 256 var user2 = {'first': 'misko', 'last': 'Hevery'}; |
| 257 |
| 258 context['user'] = user1; |
| 259 |
| 260 // should fire on initial adding |
| 261 expect(watchGrp.fieldCost).toEqual(0); |
| 262 var watch = watchGrp.watch(parse('user'), (v, p) => logger(v)); |
| 263 var watchFirst = watchGrp.watch(parse('user.first'), (v, p) => logger(v)
); |
| 264 var watchLast = watchGrp.watch(parse('user.last'), (v, p) => logger(v)); |
| 265 expect(watchGrp.fieldCost).toEqual(3); |
| 266 |
| 267 watchGrp.detectChanges(); |
| 268 expect(logger).toEqual([user1, 'misko', 'hevery']); |
| 269 logger.clear(); |
| 270 |
| 271 context['user'] = user2; |
| 272 watchGrp.detectChanges(); |
| 273 expect(logger).toEqual([user2, 'Hevery']); |
| 274 |
| 275 |
| 276 watch.remove(); |
| 277 expect(watchGrp.fieldCost).toEqual(3); |
| 278 |
| 279 watchFirst.remove(); |
| 280 expect(watchGrp.fieldCost).toEqual(2); |
| 281 |
| 282 watchLast.remove(); |
| 283 expect(watchGrp.fieldCost).toEqual(0); |
| 284 |
| 285 expect(() => watch.remove()).toThrow('Already deleted!'); |
| 286 }); |
| 287 |
| 288 it('should eval pure FunctionApply', () { |
| 289 context['a'] = {'val': 1}; |
| 290 |
| 291 FunctionApply fn = new LoggingFunctionApply(logger); |
| 292 var watch = watchGrp.watch( |
| 293 new PureFunctionAST('add', fn, [parse('a.val')]), |
| 294 (v, p) => logger(v) |
| 295 ); |
| 296 |
| 297 // a; a.val; b; b.val; |
| 298 expect(watchGrp.fieldCost).toEqual(2); |
| 299 // add |
| 300 expect(watchGrp.evalCost).toEqual(1); |
| 301 |
| 302 watchGrp.detectChanges(); |
| 303 expect(logger).toEqual([[1], null]); |
| 304 logger.clear(); |
| 305 |
| 306 context['a'] = {'val': 2}; |
| 307 watchGrp.detectChanges(); |
| 308 expect(logger).toEqual([[2]]); |
| 309 }); |
| 310 |
| 311 |
| 312 it('should eval pure function', () { |
| 313 context['a'] = {'val': 1}; |
| 314 context['b'] = {'val': 2}; |
| 315 |
| 316 var watch = watchGrp.watch( |
| 317 new PureFunctionAST('add', |
| 318 (a, b) { logger('+'); return a+b; }, |
| 319 [parse('a.val'), parse('b.val')] |
| 320 ), |
| 321 (v, p) => logger(v) |
| 322 ); |
| 323 |
| 324 // a; a.val; b; b.val; |
| 325 expect(watchGrp.fieldCost).toEqual(4); |
| 326 // add |
| 327 expect(watchGrp.evalCost).toEqual(1); |
| 328 |
| 329 watchGrp.detectChanges(); |
| 330 expect(logger).toEqual(['+', 3]); |
| 331 |
| 332 // extra checks should not trigger functions |
| 333 watchGrp.detectChanges(); |
| 334 watchGrp.detectChanges(); |
| 335 expect(logger).toEqual(['+', 3]); |
| 336 |
| 337 // multiple arg changes should only trigger function once. |
| 338 context['a']['val'] = 3; |
| 339 context['b']['val'] = 4; |
| 340 |
| 341 watchGrp.detectChanges(); |
| 342 expect(logger).toEqual(['+', 3, '+', 7]); |
| 343 |
| 344 watch.remove(); |
| 345 expect(watchGrp.fieldCost).toEqual(0); |
| 346 expect(watchGrp.evalCost).toEqual(0); |
| 347 |
| 348 context['a']['val'] = 0; |
| 349 context['b']['val'] = 0; |
| 350 |
| 351 watchGrp.detectChanges(); |
| 352 expect(logger).toEqual(['+', 3, '+', 7]); |
| 353 }); |
| 354 |
| 355 |
| 356 it('should eval closure', () { |
| 357 context['a'] = {'val': 1}; |
| 358 context['b'] = {'val': 2}; |
| 359 var innerState = 1; |
| 360 |
| 361 var watch = watchGrp.watch( |
| 362 new ClosureAST('sum', |
| 363 (a, b) { logger('+'); return innerState+a+b; }, |
| 364 [parse('a.val'), parse('b.val')] |
| 365 ), |
| 366 (v, p) => logger(v) |
| 367 ); |
| 368 |
| 369 // a; a.val; b; b.val; |
| 370 expect(watchGrp.fieldCost).toEqual(4); |
| 371 // add |
| 372 expect(watchGrp.evalCost).toEqual(1); |
| 373 |
| 374 watchGrp.detectChanges(); |
| 375 expect(logger).toEqual(['+', 4]); |
| 376 |
| 377 // extra checks should trigger closures |
| 378 watchGrp.detectChanges(); |
| 379 watchGrp.detectChanges(); |
| 380 expect(logger).toEqual(['+', 4, '+', '+']); |
| 381 logger.clear(); |
| 382 |
| 383 // multiple arg changes should only trigger function once. |
| 384 context['a']['val'] = 3; |
| 385 context['b']['val'] = 4; |
| 386 |
| 387 watchGrp.detectChanges(); |
| 388 expect(logger).toEqual(['+', 8]); |
| 389 logger.clear(); |
| 390 |
| 391 // inner state change should only trigger function once. |
| 392 innerState = 2; |
| 393 |
| 394 watchGrp.detectChanges(); |
| 395 expect(logger).toEqual(['+', 9]); |
| 396 logger.clear(); |
| 397 |
| 398 watch.remove(); |
| 399 expect(watchGrp.fieldCost).toEqual(0); |
| 400 expect(watchGrp.evalCost).toEqual(0); |
| 401 |
| 402 context['a']['val'] = 0; |
| 403 context['b']['val'] = 0; |
| 404 |
| 405 watchGrp.detectChanges(); |
| 406 expect(logger).toEqual([]); |
| 407 }); |
| 408 |
| 409 |
| 410 it('should eval chained pure function', () { |
| 411 context['a'] = {'val': 1}; |
| 412 context['b'] = {'val': 2}; |
| 413 context['c'] = {'val': 3}; |
| 414 |
| 415 var a_plus_b = new PureFunctionAST('add1', |
| 416 (a, b) { logger('$a+$b'); return a + b; }, |
| 417 [parse('a.val'), parse('b.val')]); |
| 418 |
| 419 var a_plus_b_plus_c = new PureFunctionAST('add2', |
| 420 (b, c) { logger('$b+$c'); return b + c; }, |
| 421 [a_plus_b, parse('c.val')]); |
| 422 |
| 423 var watch = watchGrp.watch(a_plus_b_plus_c, (v, p) => logger(v)); |
| 424 |
| 425 // a; a.val; b; b.val; c; c.val; |
| 426 expect(watchGrp.fieldCost).toEqual(6); |
| 427 // add |
| 428 expect(watchGrp.evalCost).toEqual(2); |
| 429 |
| 430 watchGrp.detectChanges(); |
| 431 expect(logger).toEqual(['1+2', '3+3', 6]); |
| 432 logger.clear(); |
| 433 |
| 434 // extra checks should not trigger functions |
| 435 watchGrp.detectChanges(); |
| 436 watchGrp.detectChanges(); |
| 437 expect(logger).toEqual([]); |
| 438 logger.clear(); |
| 439 |
| 440 // multiple arg changes should only trigger function once. |
| 441 context['a']['val'] = 3; |
| 442 context['b']['val'] = 4; |
| 443 context['c']['val'] = 5; |
| 444 watchGrp.detectChanges(); |
| 445 expect(logger).toEqual(['3+4', '7+5', 12]); |
| 446 logger.clear(); |
| 447 |
| 448 context['a']['val'] = 9; |
| 449 watchGrp.detectChanges(); |
| 450 expect(logger).toEqual(['9+4', '13+5', 18]); |
| 451 logger.clear(); |
| 452 |
| 453 context['c']['val'] = 9; |
| 454 watchGrp.detectChanges(); |
| 455 expect(logger).toEqual(['13+9', 22]); |
| 456 logger.clear(); |
| 457 |
| 458 |
| 459 watch.remove(); |
| 460 expect(watchGrp.fieldCost).toEqual(0); |
| 461 expect(watchGrp.evalCost).toEqual(0); |
| 462 |
| 463 context['a']['val'] = 0; |
| 464 context['b']['val'] = 0; |
| 465 |
| 466 watchGrp.detectChanges(); |
| 467 expect(logger).toEqual([]); |
| 468 }); |
| 469 |
| 470 |
| 471 it('should eval closure', () { |
| 472 var obj; |
| 473 obj = { |
| 474 'methodA': (arg1) { |
| 475 logger('methodA($arg1) => ${obj['valA']}'); |
| 476 return obj['valA']; |
| 477 }, |
| 478 'valA': 'A' |
| 479 }; |
| 480 context['obj'] = obj; |
| 481 context['arg0'] = 1; |
| 482 |
| 483 var watch = watchGrp.watch( |
| 484 new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), |
| 485 (v, p) => logger(v) |
| 486 ); |
| 487 |
| 488 // obj, arg0; |
| 489 expect(watchGrp.fieldCost).toEqual(2); |
| 490 // methodA() |
| 491 expect(watchGrp.evalCost).toEqual(1); |
| 492 |
| 493 watchGrp.detectChanges(); |
| 494 expect(logger).toEqual(['methodA(1) => A', 'A']); |
| 495 logger.clear(); |
| 496 |
| 497 watchGrp.detectChanges(); |
| 498 watchGrp.detectChanges(); |
| 499 expect(logger).toEqual(['methodA(1) => A', 'methodA(1) => A']); |
| 500 logger.clear(); |
| 501 |
| 502 obj['valA'] = 'B'; |
| 503 context['arg0'] = 2; |
| 504 |
| 505 watchGrp.detectChanges(); |
| 506 expect(logger).toEqual(['methodA(2) => B', 'B']); |
| 507 logger.clear(); |
| 508 |
| 509 watch.remove(); |
| 510 expect(watchGrp.fieldCost).toEqual(0); |
| 511 expect(watchGrp.evalCost).toEqual(0); |
| 512 |
| 513 obj['valA'] = 'C'; |
| 514 context['arg0'] = 3; |
| 515 |
| 516 watchGrp.detectChanges(); |
| 517 expect(logger).toEqual([]); |
| 518 }); |
| 519 |
| 520 |
| 521 it('should eval method', () { |
| 522 var obj = new MyClass(logger); |
| 523 obj.valA = 'A'; |
| 524 context['obj'] = obj; |
| 525 context['arg0'] = 1; |
| 526 |
| 527 var watch = watchGrp.watch( |
| 528 new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), |
| 529 (v, p) => logger(v) |
| 530 ); |
| 531 |
| 532 // obj, arg0; |
| 533 expect(watchGrp.fieldCost).toEqual(2); |
| 534 // methodA() |
| 535 expect(watchGrp.evalCost).toEqual(1); |
| 536 |
| 537 watchGrp.detectChanges(); |
| 538 expect(logger).toEqual(['methodA(1) => A', 'A']); |
| 539 logger.clear(); |
| 540 |
| 541 watchGrp.detectChanges(); |
| 542 watchGrp.detectChanges(); |
| 543 expect(logger).toEqual(['methodA(1) => A', 'methodA(1) => A']); |
| 544 logger.clear(); |
| 545 |
| 546 obj.valA = 'B'; |
| 547 context['arg0'] = 2; |
| 548 |
| 549 watchGrp.detectChanges(); |
| 550 expect(logger).toEqual(['methodA(2) => B', 'B']); |
| 551 logger.clear(); |
| 552 |
| 553 watch.remove(); |
| 554 expect(watchGrp.fieldCost).toEqual(0); |
| 555 expect(watchGrp.evalCost).toEqual(0); |
| 556 |
| 557 obj.valA = 'C'; |
| 558 context['arg0'] = 3; |
| 559 |
| 560 watchGrp.detectChanges(); |
| 561 expect(logger).toEqual([]); |
| 562 }); |
| 563 |
| 564 it('should eval method chain', () { |
| 565 var obj1 = new MyClass(logger); |
| 566 var obj2 = new MyClass(logger); |
| 567 obj1.valA = obj2; |
| 568 obj2.valA = 'A'; |
| 569 context['obj'] = obj1; |
| 570 context['arg0'] = 0; |
| 571 context['arg1'] = 1; |
| 572 |
| 573 // obj.methodA(arg0) |
| 574 var ast = new MethodAST(parse('obj'), 'methodA', [parse('arg0')]); |
| 575 ast = new MethodAST(ast, 'methodA', [parse('arg1')]); |
| 576 var watch = watchGrp.watch(ast, (v, p) => logger(v)); |
| 577 |
| 578 // obj, arg0, arg1; |
| 579 expect(watchGrp.fieldCost).toEqual(3); |
| 580 // methodA(), methodA() |
| 581 expect(watchGrp.evalCost).toEqual(2); |
| 582 |
| 583 watchGrp.detectChanges(); |
| 584 expect(logger).toEqual(['methodA(0) => MyClass', 'methodA(1) => A', 'A']
); |
| 585 logger.clear(); |
| 586 |
| 587 watchGrp.detectChanges(); |
| 588 watchGrp.detectChanges(); |
| 589 expect(logger).toEqual(['methodA(0) => MyClass', 'methodA(1) => A', |
| 590 'methodA(0) => MyClass', 'methodA(1) => A']); |
| 591 logger.clear(); |
| 592 |
| 593 obj2.valA = 'B'; |
| 594 context['arg0'] = 10; |
| 595 context['arg1'] = 11; |
| 596 |
| 597 watchGrp.detectChanges(); |
| 598 expect(logger).toEqual(['methodA(10) => MyClass', 'methodA(11) => B', 'B
']); |
| 599 logger.clear(); |
| 600 |
| 601 watch.remove(); |
| 602 expect(watchGrp.fieldCost).toEqual(0); |
| 603 expect(watchGrp.evalCost).toEqual(0); |
| 604 |
| 605 obj2.valA = 'C'; |
| 606 context['arg0'] = 20; |
| 607 context['arg1'] = 21; |
| 608 |
| 609 watchGrp.detectChanges(); |
| 610 expect(logger).toEqual([]); |
| 611 }); |
| 612 |
| 613 it('should not return null when evaling method first time', () { |
| 614 context['text'] ='abc'; |
| 615 var ast = new MethodAST(parse('text'), 'toUpperCase', []); |
| 616 var watch = watchGrp.watch(ast, (v, p) => logger(v)); |
| 617 |
| 618 watchGrp.detectChanges(); |
| 619 expect(logger).toEqual(['ABC']); |
| 620 }); |
| 621 |
| 622 it('should not eval a function if registered during reaction', () { |
| 623 context['text'] ='abc'; |
| 624 var ast = new MethodAST(parse('text'), 'toLowerCase', []); |
| 625 var watch = watchGrp.watch(ast, (v, p) { |
| 626 var ast = new MethodAST(parse('text'), 'toUpperCase', []); |
| 627 watchGrp.watch(ast, (v, p) { |
| 628 logger(v); |
| 629 }); |
| 630 }); |
| 631 |
| 632 watchGrp.detectChanges(); |
| 633 watchGrp.detectChanges(); |
| 634 expect(logger).toEqual(['ABC']); |
| 635 }); |
| 636 |
| 637 |
| 638 it('should eval function eagerly when registered during reaction', () { |
| 639 var fn = (arg) { logger('fn($arg)'); return arg; }; |
| 640 context['obj'] = {'fn': fn}; |
| 641 context['arg1'] = 'OUT'; |
| 642 context['arg2'] = 'IN'; |
| 643 var ast = new MethodAST(parse('obj'), 'fn', [parse('arg1')]); |
| 644 var watch = watchGrp.watch(ast, (v, p) { |
| 645 var ast = new MethodAST(parse('obj'), 'fn', [parse('arg2')]); |
| 646 watchGrp.watch(ast, (v, p) { |
| 647 logger('reaction: $v'); |
| 648 }); |
| 649 }); |
| 650 |
| 651 expect(logger).toEqual([]); |
| 652 watchGrp.detectChanges(); |
| 653 expect(logger).toEqual(['fn(OUT)', 'fn(IN)', 'reaction: IN']); |
| 654 logger.clear(); |
| 655 watchGrp.detectChanges(); |
| 656 expect(logger).toEqual(['fn(OUT)', 'fn(IN)']); |
| 657 }); |
| 658 |
| 659 |
| 660 it('should read constant', () { |
| 661 // should fire on initial adding |
| 662 expect(watchGrp.fieldCost).toEqual(0); |
| 663 var watch = watchGrp.watch(new ConstantAST(123), (v, p) => logger(v)); |
| 664 expect(watch.expression).toEqual('123'); |
| 665 expect(watchGrp.fieldCost).toEqual(0); |
| 666 watchGrp.detectChanges(); |
| 667 expect(logger).toEqual([123]); |
| 668 |
| 669 // make sore no new changes are logged on extra detectChanges |
| 670 watchGrp.detectChanges(); |
| 671 expect(logger).toEqual([123]); |
| 672 }); |
| 673 |
| 674 it('should wrap iterable in ObservableList', () { |
| 675 context['list'] = []; |
| 676 var watch = watchGrp.watch(new CollectionAST(parse('list')), (v, p) => l
ogger(v)); |
| 677 |
| 678 expect(watchGrp.fieldCost).toEqual(1); |
| 679 expect(watchGrp.collectionCost).toEqual(1); |
| 680 expect(watchGrp.evalCost).toEqual(0); |
| 681 |
| 682 watchGrp.detectChanges(); |
| 683 expect(logger.length).toEqual(1); |
| 684 expect(logger[0], toEqualCollectionRecord( |
| 685 collection: [], |
| 686 additions: [], |
| 687 moves: [], |
| 688 removals: [])); |
| 689 logger.clear(); |
| 690 |
| 691 context['list'] = [1]; |
| 692 watchGrp.detectChanges(); |
| 693 expect(logger.length).toEqual(1); |
| 694 expect(logger[0], toEqualCollectionRecord( |
| 695 collection: ['1[null -> 0]'], |
| 696 additions: ['1[null -> 0]'], |
| 697 moves: [], |
| 698 removals: [])); |
| 699 logger.clear(); |
| 700 |
| 701 watch.remove(); |
| 702 expect(watchGrp.fieldCost).toEqual(0); |
| 703 expect(watchGrp.collectionCost).toEqual(0); |
| 704 expect(watchGrp.evalCost).toEqual(0); |
| 705 }); |
| 706 |
| 707 it('should watch literal arrays made of expressions', () { |
| 708 context['a'] = 1; |
| 709 var ast = new CollectionAST( |
| 710 new PureFunctionAST('[a]', new ArrayFn(), [parse('a')]) |
| 711 ); |
| 712 var watch = watchGrp.watch(ast, (v, p) => logger(v)); |
| 713 watchGrp.detectChanges(); |
| 714 expect(logger[0], toEqualCollectionRecord( |
| 715 collection: ['1[null -> 0]'], |
| 716 additions: ['1[null -> 0]'], |
| 717 moves: [], |
| 718 removals: [])); |
| 719 logger.clear(); |
| 720 |
| 721 context['a'] = 2; |
| 722 watchGrp.detectChanges(); |
| 723 expect(logger[0], toEqualCollectionRecord( |
| 724 collection: ['2[null -> 0]'], |
| 725 previous: ['1[0 -> null]'], |
| 726 additions: ['2[null -> 0]'], |
| 727 moves: [], |
| 728 removals: ['1[0 -> null]'])); |
| 729 logger.clear(); |
| 730 }); |
| 731 |
| 732 it('should watch pure function whose result goes to pure function', () { |
| 733 context['a'] = 1; |
| 734 var ast = new PureFunctionAST( |
| 735 '-', |
| 736 (v) => -v, |
| 737 [new PureFunctionAST('++', (v) => v + 1, [parse('a')])] |
| 738 ); |
| 739 var watch = watchGrp.watch(ast, (v, p) => logger(v)); |
| 740 |
| 741 expect(watchGrp.detectChanges()).not.toBe(null); |
| 742 expect(logger).toEqual([-2]); |
| 743 logger.clear(); |
| 744 |
| 745 context['a'] = 2; |
| 746 expect(watchGrp.detectChanges()).not.toBe(null); |
| 747 expect(logger).toEqual([-3]); |
| 748 }); |
48 }); | 749 }); |
| 750 |
| 751 describe('evaluation', () { |
| 752 it('should support simple literals', () { |
| 753 expect(eval('42')).toBe(42); |
| 754 expect(eval('87')).toBe(87); |
| 755 }); |
| 756 |
| 757 it('should support context access', () { |
| 758 context['x'] = 42; |
| 759 expect(eval('x')).toBe(42); |
| 760 context['y'] = 87; |
| 761 expect(eval('y')).toBe(87); |
| 762 }); |
| 763 |
| 764 it('should support custom context', () { |
| 765 expect(eval('x', {'x': 42})).toBe(42); |
| 766 expect(eval('x', {'x': 87})).toBe(87); |
| 767 }); |
| 768 |
| 769 it('should support named arguments for scope calls', () { |
| 770 var data = new TestData(); |
| 771 expect(eval("sub1(1)", data)).toEqual(1); |
| 772 expect(eval("sub1(3, b: 2)", data)).toEqual(1); |
| 773 |
| 774 expect(eval("sub2()", data)).toEqual(0); |
| 775 expect(eval("sub2(a: 3)", data)).toEqual(3); |
| 776 expect(eval("sub2(a: 3, b: 2)", data)).toEqual(1); |
| 777 expect(eval("sub2(b: 4)", data)).toEqual(-4); |
| 778 }); |
| 779 |
| 780 it('should support named arguments for scope calls (map)', () { |
| 781 context["sub1"] = (a, {b: 0}) => a - b; |
| 782 expect(eval("sub1(1)")).toEqual(1); |
| 783 expect(eval("sub1(3, b: 2)")).toEqual(1); |
| 784 |
| 785 context["sub2"] = ({a: 0, b: 0}) => a - b; |
| 786 expect(eval("sub2()")).toEqual(0); |
| 787 expect(eval("sub2(a: 3)")).toEqual(3); |
| 788 expect(eval("sub2(a: 3, b: 2)")).toEqual(1); |
| 789 expect(eval("sub2(b: 4)")).toEqual(-4); |
| 790 }); |
| 791 |
| 792 it('should support named arguments for member calls', () { |
| 793 context['o'] = new TestData(); |
| 794 expect(eval("o.sub1(1)")).toEqual(1); |
| 795 expect(eval("o.sub1(3, b: 2)")).toEqual(1); |
| 796 |
| 797 expect(eval("o.sub2()")).toEqual(0); |
| 798 expect(eval("o.sub2(a: 3)")).toEqual(3); |
| 799 expect(eval("o.sub2(a: 3, b: 2)")).toEqual(1); |
| 800 expect(eval("o.sub2(b: 4)")).toEqual(-4); |
| 801 }); |
| 802 |
| 803 it('should support named arguments for member calls (map)', () { |
| 804 context['o'] = { |
| 805 'sub1': (a, {b: 0}) => a - b, |
| 806 'sub2': ({a: 0, b: 0}) => a - b |
| 807 }; |
| 808 expect(eval("o.sub1(1)")).toEqual(1); |
| 809 expect(eval("o.sub1(3, b: 2)")).toEqual(1); |
| 810 |
| 811 expect(eval("o.sub2()")).toEqual(0); |
| 812 expect(eval("o.sub2(a: 3)")).toEqual(3); |
| 813 expect(eval("o.sub2(a: 3, b: 2)")).toEqual(1); |
| 814 expect(eval("o.sub2(b: 4)")).toEqual(-4); |
| 815 }); |
| 816 }); |
| 817 |
| 818 describe('child group', () { |
| 819 it('should remove all field watches in group and group\'s children', () { |
| 820 watchGrp.watch(parse('a'), (v, p) => logger('0a')); |
| 821 var child1a = watchGrp.newGroup(new PrototypeMap(context)); |
| 822 var child1b = watchGrp.newGroup(new PrototypeMap(context)); |
| 823 var child2 = child1a.newGroup(new PrototypeMap(context)); |
| 824 child1a.watch(parse('a'), (v, p) => logger('1a')); |
| 825 child1b.watch(parse('a'), (v, p) => logger('1b')); |
| 826 watchGrp.watch(parse('a'), (v, p) => logger('0A')); |
| 827 child1a.watch(parse('a'), (v, p) => logger('1A')); |
| 828 child2.watch(parse('a'), (v, p) => logger('2A')); |
| 829 |
| 830 // flush initial reaction functions |
| 831 expect(watchGrp.detectChanges()).toEqual(6); |
| 832 // expect(logger).toEqual(['0a', '0A', '1a', '1A', '2A', '1b']); |
| 833 expect(logger).toEqual(['0a', '1a', '1b', '0A', '1A', '2A']); // we go b
y registration order |
| 834 expect(watchGrp.fieldCost).toEqual(1); |
| 835 expect(watchGrp.totalFieldCost).toEqual(4); |
| 836 logger.clear(); |
| 837 |
| 838 context['a'] = 1; |
| 839 expect(watchGrp.detectChanges()).toEqual(6); |
| 840 expect(logger).toEqual(['0a', '0A', '1a', '1A', '2A', '1b']); // we go b
y group order |
| 841 logger.clear(); |
| 842 |
| 843 context['a'] = 2; |
| 844 child1a.remove(); // should also remove child2 |
| 845 expect(watchGrp.detectChanges()).toEqual(3); |
| 846 expect(logger).toEqual(['0a', '0A', '1b']); |
| 847 expect(watchGrp.fieldCost).toEqual(1); |
| 848 expect(watchGrp.totalFieldCost).toEqual(2); |
| 849 }); |
| 850 |
| 851 it('should remove all method watches in group and group\'s children', () { |
| 852 context['my'] = new MyClass(logger); |
| 853 AST countMethod = new MethodAST(parse('my'), 'count', []); |
| 854 watchGrp.watch(countMethod, (v, p) => logger('0a')); |
| 855 expectOrder(['0a']); |
| 856 |
| 857 var child1a = watchGrp.newGroup(new PrototypeMap(context)); |
| 858 var child1b = watchGrp.newGroup(new PrototypeMap(context)); |
| 859 var child2 = child1a.newGroup(new PrototypeMap(context)); |
| 860 var child3 = child2.newGroup(new PrototypeMap(context)); |
| 861 child1a.watch(countMethod, (v, p) => logger('1a')); |
| 862 expectOrder(['0a', '1a']); |
| 863 child1b.watch(countMethod, (v, p) => logger('1b')); |
| 864 expectOrder(['0a', '1a', '1b']); |
| 865 watchGrp.watch(countMethod, (v, p) => logger('0A')); |
| 866 expectOrder(['0a', '0A', '1a', '1b']); |
| 867 child1a.watch(countMethod, (v, p) => logger('1A')); |
| 868 expectOrder(['0a', '0A', '1a', '1A', '1b']); |
| 869 child2.watch(countMethod, (v, p) => logger('2A')); |
| 870 expectOrder(['0a', '0A', '1a', '1A', '2A', '1b']); |
| 871 child3.watch(countMethod, (v, p) => logger('3')); |
| 872 expectOrder(['0a', '0A', '1a', '1A', '2A', '3', '1b']); |
| 873 |
| 874 // flush initial reaction functions |
| 875 expect(watchGrp.detectChanges()).toEqual(7); |
| 876 expectOrder(['0a', '0A', '1a', '1A', '2A', '3', '1b']); |
| 877 |
| 878 child1a.remove(); // should also remove child2 and child 3 |
| 879 expect(watchGrp.detectChanges()).toEqual(3); |
| 880 expectOrder(['0a', '0A', '1b']); |
| 881 }); |
| 882 |
| 883 it('should add watches within its own group', () { |
| 884 context['my'] = new MyClass(logger); |
| 885 AST countMethod = new MethodAST(parse('my'), 'count', []); |
| 886 var ra = watchGrp.watch(countMethod, (v, p) => logger('a')); |
| 887 var child = watchGrp.newGroup(new PrototypeMap(context)); |
| 888 var cb = child.watch(countMethod, (v, p) => logger('b')); |
| 889 |
| 890 expectOrder(['a', 'b']); |
| 891 expectOrder(['a', 'b']); |
| 892 |
| 893 ra.remove(); |
| 894 expectOrder(['b']); |
| 895 |
| 896 cb.remove(); |
| 897 expectOrder([]); |
| 898 |
| 899 // TODO: add them back in wrong order, assert events in right order |
| 900 cb = child.watch(countMethod, (v, p) => logger('b')); |
| 901 ra = watchGrp.watch(countMethod, (v, p) => logger('a'));; |
| 902 expectOrder(['a', 'b']); |
| 903 }); |
| 904 |
| 905 |
| 906 it('should not call reaction function on removed group', () { |
| 907 var log = []; |
| 908 context['name'] = 'misko'; |
| 909 var child = watchGrp.newGroup(context); |
| 910 watchGrp.watch(parse('name'), (v, _) { |
| 911 log.add('root $v'); |
| 912 if (v == 'destroy') { |
| 913 child.remove(); |
| 914 } |
| 915 }); |
| 916 child.watch(parse('name'), (v, _) => log.add('child $v')); |
| 917 watchGrp.detectChanges(); |
| 918 expect(log).toEqual(['root misko', 'child misko']); |
| 919 log.clear(); |
| 920 |
| 921 context['name'] = 'destroy'; |
| 922 watchGrp.detectChanges(); |
| 923 expect(log).toEqual(['root destroy']); |
| 924 }); |
| 925 |
| 926 |
| 927 |
| 928 it('should watch children', () { |
| 929 var childContext = new PrototypeMap(context); |
| 930 context['a'] = 'OK'; |
| 931 context['b'] = 'BAD'; |
| 932 childContext['b'] = 'OK'; |
| 933 watchGrp.watch(parse('a'), (v, p) => logger(v)); |
| 934 watchGrp.newGroup(childContext).watch(parse('b'), (v, p) => logger(v)); |
| 935 |
| 936 watchGrp.detectChanges(); |
| 937 expect(logger).toEqual(['OK', 'OK']); |
| 938 logger.clear(); |
| 939 |
| 940 context['a'] = 'A'; |
| 941 childContext['b'] = 'B'; |
| 942 |
| 943 watchGrp.detectChanges(); |
| 944 expect(logger).toEqual(['A', 'B']); |
| 945 logger.clear(); |
| 946 }); |
| 947 }); |
| 948 |
49 }); | 949 }); |
50 | 950 } |
51 describe('property chaining', () { | |
52 it('should read property', () { | |
53 context['a'] = 'hello'; | |
54 | |
55 // should fire on initial adding | |
56 expect(watchGrp.fieldCost).toEqual(0); | |
57 var watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); | |
58 expect(watch.expression).toEqual('a'); | |
59 expect(watchGrp.fieldCost).toEqual(1); | |
60 watchGrp.detectChanges(); | |
61 expect(logger).toEqual(['hello']); | |
62 | |
63 // make sore no new changes are logged on extra detectChanges | |
64 watchGrp.detectChanges(); | |
65 expect(logger).toEqual(['hello']); | |
66 | |
67 // Should detect value change | |
68 context['a'] = 'bye'; | |
69 watchGrp.detectChanges(); | |
70 expect(logger).toEqual(['hello', 'bye']); | |
71 | |
72 // should cleanup after itself | |
73 watch.remove(); | |
74 expect(watchGrp.fieldCost).toEqual(0); | |
75 context['a'] = 'cant see me'; | |
76 watchGrp.detectChanges(); | |
77 expect(logger).toEqual(['hello', 'bye']); | |
78 }); | |
79 | |
80 it('should read property chain', () { | |
81 context['a'] = {'b': 'hello'}; | |
82 | |
83 // should fire on initial adding | |
84 expect(watchGrp.fieldCost).toEqual(0); | |
85 expect(changeDetector.count).toEqual(0); | |
86 var watch = watchGrp.watch(parse('a.b'), (v, p) => logger(v)); | |
87 expect(watch.expression).toEqual('a.b'); | |
88 expect(watchGrp.fieldCost).toEqual(2); | |
89 expect(changeDetector.count).toEqual(2); | |
90 watchGrp.detectChanges(); | |
91 expect(logger).toEqual(['hello']); | |
92 | |
93 // make sore no new changes are logged on extra detectChanges | |
94 watchGrp.detectChanges(); | |
95 expect(logger).toEqual(['hello']); | |
96 | |
97 // make sure no changes or logged when intermediary object changes | |
98 context['a'] = {'b': 'hello'}; | |
99 watchGrp.detectChanges(); | |
100 expect(logger).toEqual(['hello']); | |
101 | |
102 // Should detect value change | |
103 context['a'] = {'b': 'hello2'}; | |
104 watchGrp.detectChanges(); | |
105 expect(logger).toEqual(['hello', 'hello2']); | |
106 | |
107 // Should detect value change | |
108 context['a']['b'] = 'bye'; | |
109 watchGrp.detectChanges(); | |
110 expect(logger).toEqual(['hello', 'hello2', 'bye']); | |
111 | |
112 // should cleanup after itself | |
113 watch.remove(); | |
114 expect(watchGrp.fieldCost).toEqual(0); | |
115 context['a']['b'] = 'cant see me'; | |
116 watchGrp.detectChanges(); | |
117 expect(logger).toEqual(['hello', 'hello2', 'bye']); | |
118 }); | |
119 | |
120 it('should reuse handlers', () { | |
121 var user1 = {'first': 'misko', 'last': 'hevery'}; | |
122 var user2 = {'first': 'misko', 'last': 'Hevery'}; | |
123 | |
124 context['user'] = user1; | |
125 | |
126 // should fire on initial adding | |
127 expect(watchGrp.fieldCost).toEqual(0); | |
128 var watch = watchGrp.watch(parse('user'), (v, p) => logger(v)); | |
129 var watchFirst = watchGrp.watch(parse('user.first'), (v, p) => logger(v)); | |
130 var watchLast = watchGrp.watch(parse('user.last'), (v, p) => logger(v)); | |
131 expect(watchGrp.fieldCost).toEqual(3); | |
132 | |
133 watchGrp.detectChanges(); | |
134 expect(logger).toEqual([user1, 'misko', 'hevery']); | |
135 logger.clear(); | |
136 | |
137 context['user'] = user2; | |
138 watchGrp.detectChanges(); | |
139 expect(logger).toEqual([user2, 'Hevery']); | |
140 | |
141 | |
142 watch.remove(); | |
143 expect(watchGrp.fieldCost).toEqual(3); | |
144 | |
145 watchFirst.remove(); | |
146 expect(watchGrp.fieldCost).toEqual(2); | |
147 | |
148 watchLast.remove(); | |
149 expect(watchGrp.fieldCost).toEqual(0); | |
150 | |
151 expect(() => watch.remove()).toThrow('Already deleted!'); | |
152 }); | |
153 | |
154 it('should eval pure FunctionApply', () { | |
155 context['a'] = {'val': 1}; | |
156 | |
157 FunctionApply fn = new LoggingFunctionApply(logger); | |
158 var watch = watchGrp.watch( | |
159 new PureFunctionAST('add', fn, [parse('a.val')]), | |
160 (v, p) => logger(v) | |
161 ); | |
162 | |
163 // a; a.val; b; b.val; | |
164 expect(watchGrp.fieldCost).toEqual(2); | |
165 // add | |
166 expect(watchGrp.evalCost).toEqual(1); | |
167 | |
168 watchGrp.detectChanges(); | |
169 expect(logger).toEqual([[1], null]); | |
170 logger.clear(); | |
171 | |
172 context['a'] = {'val': 2}; | |
173 watchGrp.detectChanges(); | |
174 expect(logger).toEqual([[2]]); | |
175 }); | |
176 | |
177 | |
178 it('should eval pure function', () { | |
179 context['a'] = {'val': 1}; | |
180 context['b'] = {'val': 2}; | |
181 | |
182 var watch = watchGrp.watch( | |
183 new PureFunctionAST('add', | |
184 (a, b) { logger('+'); return a+b; }, | |
185 [parse('a.val'), parse('b.val')] | |
186 ), | |
187 (v, p) => logger(v) | |
188 ); | |
189 | |
190 // a; a.val; b; b.val; | |
191 expect(watchGrp.fieldCost).toEqual(4); | |
192 // add | |
193 expect(watchGrp.evalCost).toEqual(1); | |
194 | |
195 watchGrp.detectChanges(); | |
196 expect(logger).toEqual(['+', 3]); | |
197 | |
198 // extra checks should not trigger functions | |
199 watchGrp.detectChanges(); | |
200 watchGrp.detectChanges(); | |
201 expect(logger).toEqual(['+', 3]); | |
202 | |
203 // multiple arg changes should only trigger function once. | |
204 context['a']['val'] = 3; | |
205 context['b']['val'] = 4; | |
206 | |
207 watchGrp.detectChanges(); | |
208 expect(logger).toEqual(['+', 3, '+', 7]); | |
209 | |
210 watch.remove(); | |
211 expect(watchGrp.fieldCost).toEqual(0); | |
212 expect(watchGrp.evalCost).toEqual(0); | |
213 | |
214 context['a']['val'] = 0; | |
215 context['b']['val'] = 0; | |
216 | |
217 watchGrp.detectChanges(); | |
218 expect(logger).toEqual(['+', 3, '+', 7]); | |
219 }); | |
220 | |
221 | |
222 it('should eval chained pure function', () { | |
223 context['a'] = {'val': 1}; | |
224 context['b'] = {'val': 2}; | |
225 context['c'] = {'val': 3}; | |
226 | |
227 var a_plus_b = new PureFunctionAST('add1', | |
228 (a, b) { logger('$a+$b'); return a + b; }, | |
229 [parse('a.val'), parse('b.val')]); | |
230 | |
231 var a_plus_b_plus_c = new PureFunctionAST('add2', | |
232 (b, c) { logger('$b+$c'); return b + c; }, | |
233 [a_plus_b, parse('c.val')]); | |
234 | |
235 var watch = watchGrp.watch(a_plus_b_plus_c, (v, p) => logger(v)); | |
236 | |
237 // a; a.val; b; b.val; c; c.val; | |
238 expect(watchGrp.fieldCost).toEqual(6); | |
239 // add | |
240 expect(watchGrp.evalCost).toEqual(2); | |
241 | |
242 watchGrp.detectChanges(); | |
243 expect(logger).toEqual(['1+2', '3+3', 6]); | |
244 logger.clear(); | |
245 | |
246 // extra checks should not trigger functions | |
247 watchGrp.detectChanges(); | |
248 watchGrp.detectChanges(); | |
249 expect(logger).toEqual([]); | |
250 logger.clear(); | |
251 | |
252 // multiple arg changes should only trigger function once. | |
253 context['a']['val'] = 3; | |
254 context['b']['val'] = 4; | |
255 context['c']['val'] = 5; | |
256 watchGrp.detectChanges(); | |
257 expect(logger).toEqual(['3+4', '7+5', 12]); | |
258 logger.clear(); | |
259 | |
260 context['a']['val'] = 9; | |
261 watchGrp.detectChanges(); | |
262 expect(logger).toEqual(['9+4', '13+5', 18]); | |
263 logger.clear(); | |
264 | |
265 context['c']['val'] = 9; | |
266 watchGrp.detectChanges(); | |
267 expect(logger).toEqual(['13+9', 22]); | |
268 logger.clear(); | |
269 | |
270 | |
271 watch.remove(); | |
272 expect(watchGrp.fieldCost).toEqual(0); | |
273 expect(watchGrp.evalCost).toEqual(0); | |
274 | |
275 context['a']['val'] = 0; | |
276 context['b']['val'] = 0; | |
277 | |
278 watchGrp.detectChanges(); | |
279 expect(logger).toEqual([]); | |
280 }); | |
281 | |
282 | |
283 it('should eval closure', () { | |
284 var obj; | |
285 obj = { | |
286 'methodA': (arg1) { | |
287 logger('methodA($arg1) => ${obj['valA']}'); | |
288 return obj['valA']; | |
289 }, | |
290 'valA': 'A' | |
291 }; | |
292 context['obj'] = obj; | |
293 context['arg0'] = 1; | |
294 | |
295 var watch = watchGrp.watch( | |
296 new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), | |
297 (v, p) => logger(v) | |
298 ); | |
299 | |
300 // obj, arg0; | |
301 expect(watchGrp.fieldCost).toEqual(2); | |
302 // methodA() | |
303 expect(watchGrp.evalCost).toEqual(1); | |
304 | |
305 watchGrp.detectChanges(); | |
306 expect(logger).toEqual(['methodA(1) => A', 'A']); | |
307 logger.clear(); | |
308 | |
309 watchGrp.detectChanges(); | |
310 watchGrp.detectChanges(); | |
311 expect(logger).toEqual(['methodA(1) => A', 'methodA(1) => A']); | |
312 logger.clear(); | |
313 | |
314 obj['valA'] = 'B'; | |
315 context['arg0'] = 2; | |
316 | |
317 watchGrp.detectChanges(); | |
318 expect(logger).toEqual(['methodA(2) => B', 'B']); | |
319 logger.clear(); | |
320 | |
321 watch.remove(); | |
322 expect(watchGrp.fieldCost).toEqual(0); | |
323 expect(watchGrp.evalCost).toEqual(0); | |
324 | |
325 obj['valA'] = 'C'; | |
326 context['arg0'] = 3; | |
327 | |
328 watchGrp.detectChanges(); | |
329 expect(logger).toEqual([]); | |
330 }); | |
331 | |
332 | |
333 it('should eval method', () { | |
334 var obj = new MyClass(logger); | |
335 obj.valA = 'A'; | |
336 context['obj'] = obj; | |
337 context['arg0'] = 1; | |
338 | |
339 var watch = watchGrp.watch( | |
340 new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), | |
341 (v, p) => logger(v) | |
342 ); | |
343 | |
344 // obj, arg0; | |
345 expect(watchGrp.fieldCost).toEqual(2); | |
346 // methodA() | |
347 expect(watchGrp.evalCost).toEqual(1); | |
348 | |
349 watchGrp.detectChanges(); | |
350 expect(logger).toEqual(['methodA(1) => A', 'A']); | |
351 logger.clear(); | |
352 | |
353 watchGrp.detectChanges(); | |
354 watchGrp.detectChanges(); | |
355 expect(logger).toEqual(['methodA(1) => A', 'methodA(1) => A']); | |
356 logger.clear(); | |
357 | |
358 obj.valA = 'B'; | |
359 context['arg0'] = 2; | |
360 | |
361 watchGrp.detectChanges(); | |
362 expect(logger).toEqual(['methodA(2) => B', 'B']); | |
363 logger.clear(); | |
364 | |
365 watch.remove(); | |
366 expect(watchGrp.fieldCost).toEqual(0); | |
367 expect(watchGrp.evalCost).toEqual(0); | |
368 | |
369 obj.valA = 'C'; | |
370 context['arg0'] = 3; | |
371 | |
372 watchGrp.detectChanges(); | |
373 expect(logger).toEqual([]); | |
374 }); | |
375 | |
376 it('should eval method chain', () { | |
377 var obj1 = new MyClass(logger); | |
378 var obj2 = new MyClass(logger); | |
379 obj1.valA = obj2; | |
380 obj2.valA = 'A'; | |
381 context['obj'] = obj1; | |
382 context['arg0'] = 0; | |
383 context['arg1'] = 1; | |
384 | |
385 // obj.methodA(arg0) | |
386 var ast = new MethodAST(parse('obj'), 'methodA', [parse('arg0')]); | |
387 ast = new MethodAST(ast, 'methodA', [parse('arg1')]); | |
388 var watch = watchGrp.watch(ast, (v, p) => logger(v)); | |
389 | |
390 // obj, arg0, arg1; | |
391 expect(watchGrp.fieldCost).toEqual(3); | |
392 // methodA(), mothodA() | |
393 expect(watchGrp.evalCost).toEqual(2); | |
394 | |
395 watchGrp.detectChanges(); | |
396 expect(logger).toEqual(['methodA(0) => MyClass', 'methodA(1) => A', 'A']); | |
397 logger.clear(); | |
398 | |
399 watchGrp.detectChanges(); | |
400 watchGrp.detectChanges(); | |
401 expect(logger).toEqual(['methodA(0) => MyClass', 'methodA(1) => A', | |
402 'methodA(0) => MyClass', 'methodA(1) => A']); | |
403 logger.clear(); | |
404 | |
405 obj2.valA = 'B'; | |
406 context['arg0'] = 10; | |
407 context['arg1'] = 11; | |
408 | |
409 watchGrp.detectChanges(); | |
410 expect(logger).toEqual(['methodA(10) => MyClass', 'methodA(11) => B', 'B']
); | |
411 logger.clear(); | |
412 | |
413 watch.remove(); | |
414 expect(watchGrp.fieldCost).toEqual(0); | |
415 expect(watchGrp.evalCost).toEqual(0); | |
416 | |
417 obj2.valA = 'C'; | |
418 context['arg0'] = 20; | |
419 context['arg1'] = 21; | |
420 | |
421 watchGrp.detectChanges(); | |
422 expect(logger).toEqual([]); | |
423 }); | |
424 | |
425 it('should read connstant', () { | |
426 // should fire on initial adding | |
427 expect(watchGrp.fieldCost).toEqual(0); | |
428 var watch = watchGrp.watch(new ConstantAST(123), (v, p) => logger(v)); | |
429 expect(watch.expression).toEqual('123'); | |
430 expect(watchGrp.fieldCost).toEqual(0); | |
431 watchGrp.detectChanges(); | |
432 expect(logger).toEqual([123]); | |
433 | |
434 // make sore no new changes are logged on extra detectChanges | |
435 watchGrp.detectChanges(); | |
436 expect(logger).toEqual([123]); | |
437 }); | |
438 | |
439 it('should wrap iterable in ObservableList', () { | |
440 context['list'] = []; | |
441 var watch = watchGrp.watch(new CollectionAST(parse('list')), (v, p) => log
ger(v)); | |
442 | |
443 expect(watchGrp.fieldCost).toEqual(1); | |
444 expect(watchGrp.collectionCost).toEqual(1); | |
445 expect(watchGrp.evalCost).toEqual(0); | |
446 | |
447 watchGrp.detectChanges(); | |
448 expect(logger.length).toEqual(1); | |
449 expect(logger[0], toEqualCollectionRecord( | |
450 collection: [], | |
451 additions: [], | |
452 moves: [], | |
453 removals: [])); | |
454 logger.clear(); | |
455 | |
456 context['list'] = [1]; | |
457 watchGrp.detectChanges(); | |
458 expect(logger.length).toEqual(1); | |
459 expect(logger[0], toEqualCollectionRecord( | |
460 collection: ['1[null -> 0]'], | |
461 additions: ['1[null -> 0]'], | |
462 moves: [], | |
463 removals: [])); | |
464 logger.clear(); | |
465 | |
466 watch.remove(); | |
467 expect(watchGrp.fieldCost).toEqual(0); | |
468 expect(watchGrp.collectionCost).toEqual(0); | |
469 expect(watchGrp.evalCost).toEqual(0); | |
470 }); | |
471 | |
472 it('should watch literal arrays made of expressions', () { | |
473 context['a'] = 1; | |
474 var ast = new CollectionAST( | |
475 new PureFunctionAST('[a]', new ArrayFn(), [parse('a')]) | |
476 ); | |
477 var watch = watchGrp.watch(ast, (v, p) => logger(v)); | |
478 watchGrp.detectChanges(); | |
479 expect(logger[0], toEqualCollectionRecord( | |
480 collection: ['1[null -> 0]'], | |
481 additions: ['1[null -> 0]'], | |
482 moves: [], | |
483 removals: [])); | |
484 logger.clear(); | |
485 | |
486 context['a'] = 2; | |
487 watchGrp.detectChanges(); | |
488 expect(logger[0], toEqualCollectionRecord( | |
489 collection: ['2[null -> 0]'], | |
490 additions: ['2[null -> 0]'], | |
491 moves: [], | |
492 removals: ['1[0 -> null]'])); | |
493 logger.clear(); | |
494 }); | |
495 | |
496 it('should watch pure function whose result goes to pure function', () { | |
497 context['a'] = 1; | |
498 var ast = new PureFunctionAST( | |
499 '-', | |
500 (v) => -v, | |
501 [new PureFunctionAST('++', (v) => v + 1, [parse('a')])] | |
502 ); | |
503 var watch = watchGrp.watch(ast, (v, p) => logger(v)); | |
504 | |
505 expect(watchGrp.detectChanges()).not.toBe(null); | |
506 expect(logger).toEqual([-2]); | |
507 logger.clear(); | |
508 | |
509 context['a'] = 2; | |
510 expect(watchGrp.detectChanges()).not.toBe(null); | |
511 expect(logger).toEqual([-3]); | |
512 }); | |
513 }); | |
514 | |
515 describe('child group', () { | |
516 it('should remove all field watches in group and group\'s children', () { | |
517 watchGrp.watch(parse('a'), (v, p) => logger('0a')); | |
518 var child1a = watchGrp.newGroup(new PrototypeMap(context)); | |
519 var child1b = watchGrp.newGroup(new PrototypeMap(context)); | |
520 var child2 = child1a.newGroup(new PrototypeMap(context)); | |
521 child1a.watch(parse('a'), (v, p) => logger('1a')); | |
522 child1b.watch(parse('a'), (v, p) => logger('1b')); | |
523 watchGrp.watch(parse('a'), (v, p) => logger('0A')); | |
524 child1a.watch(parse('a'), (v, p) => logger('1A')); | |
525 child2.watch(parse('a'), (v, p) => logger('2A')); | |
526 | |
527 // flush initial reaction functions | |
528 expect(watchGrp.detectChanges()).toEqual(6); | |
529 // expect(logger).toEqual(['0a', '0A', '1a', '1A', '2A', '1b']); | |
530 expect(logger).toEqual(['0a', '1a', '1b', '0A', '1A', '2A']); // we go by
registration order | |
531 expect(watchGrp.fieldCost).toEqual(1); | |
532 expect(watchGrp.totalFieldCost).toEqual(4); | |
533 logger.clear(); | |
534 | |
535 context['a'] = 1; | |
536 expect(watchGrp.detectChanges()).toEqual(6); | |
537 expect(logger).toEqual(['0a', '0A', '1a', '1A', '2A', '1b']); // we go by
group order | |
538 logger.clear(); | |
539 | |
540 context['a'] = 2; | |
541 child1a.remove(); // should also remove child2 | |
542 expect(watchGrp.detectChanges()).toEqual(3); | |
543 expect(logger).toEqual(['0a', '0A', '1b']); | |
544 expect(watchGrp.fieldCost).toEqual(1); | |
545 expect(watchGrp.totalFieldCost).toEqual(2); | |
546 }); | |
547 | |
548 it('should remove all method watches in group and group\'s children', () { | |
549 context['my'] = new MyClass(logger); | |
550 AST countMethod = new MethodAST(parse('my'), 'count', []); | |
551 watchGrp.watch(countMethod, (v, p) => logger('0a')); | |
552 expectOrder(['0a']); | |
553 | |
554 var child1a = watchGrp.newGroup(new PrototypeMap(context)); | |
555 var child1b = watchGrp.newGroup(new PrototypeMap(context)); | |
556 var child2 = child1a.newGroup(new PrototypeMap(context)); | |
557 child1a.watch(countMethod, (v, p) => logger('1a')); | |
558 expectOrder(['0a', '1a']); | |
559 child1b.watch(countMethod, (v, p) => logger('1b')); | |
560 expectOrder(['0a', '1a', '1b']); | |
561 watchGrp.watch(countMethod, (v, p) => logger('0A')); | |
562 expectOrder(['0a', '0A', '1a', '1b']); | |
563 child1a.watch(countMethod, (v, p) => logger('1A')); | |
564 expectOrder(['0a', '0A', '1a', '1A', '1b']); | |
565 child2.watch(countMethod, (v, p) => logger('2A')); | |
566 expectOrder(['0a', '0A', '1a', '1A', '2A', '1b']); | |
567 | |
568 // flush initial reaction functions | |
569 expect(watchGrp.detectChanges()).toEqual(6); | |
570 expectOrder(['0a', '0A', '1a', '1A', '2A', '1b']); | |
571 | |
572 child1a.remove(); // should also remove child2 | |
573 expect(watchGrp.detectChanges()).toEqual(3); | |
574 expectOrder(['0a', '0A', '1b']); | |
575 }); | |
576 | |
577 it('should add watches within its own group', () { | |
578 context['my'] = new MyClass(logger); | |
579 AST countMethod = new MethodAST(parse('my'), 'count', []); | |
580 var ra = watchGrp.watch(countMethod, (v, p) => logger('a')); | |
581 var child = watchGrp.newGroup(new PrototypeMap(context)); | |
582 var cb = child.watch(countMethod, (v, p) => logger('b')); | |
583 | |
584 expectOrder(['a', 'b']); | |
585 expectOrder(['a', 'b']); | |
586 | |
587 ra.remove(); | |
588 expectOrder(['b']); | |
589 | |
590 cb.remove(); | |
591 expectOrder([]); | |
592 | |
593 // TODO: add them back in wrong order, assert events in right order | |
594 cb = child.watch(countMethod, (v, p) => logger('b')); | |
595 ra = watchGrp.watch(countMethod, (v, p) => logger('a'));; | |
596 expectOrder(['a', 'b']); | |
597 }); | |
598 | |
599 | |
600 it('should not call reaction function on removed group', () { | |
601 var log = []; | |
602 context['name'] = 'misko'; | |
603 var child = watchGrp.newGroup(context); | |
604 watchGrp.watch(parse('name'), (v, _) { | |
605 log.add('root $v'); | |
606 if (v == 'destroy') { | |
607 child.remove(); | |
608 } | |
609 }); | |
610 child.watch(parse('name'), (v, _) => log.add('child $v')); | |
611 watchGrp.detectChanges(); | |
612 expect(log).toEqual(['root misko', 'child misko']); | |
613 log.clear(); | |
614 | |
615 context['name'] = 'destroy'; | |
616 watchGrp.detectChanges(); | |
617 expect(log).toEqual(['root destroy']); | |
618 }); | |
619 | |
620 | |
621 | |
622 it('should watch children', () { | |
623 var childContext = new PrototypeMap(context); | |
624 context['a'] = 'OK'; | |
625 context['b'] = 'BAD'; | |
626 childContext['b'] = 'OK'; | |
627 watchGrp.watch(parse('a'), (v, p) => logger(v)); | |
628 watchGrp.newGroup(childContext).watch(parse('b'), (v, p) => logger(v)); | |
629 | |
630 watchGrp.detectChanges(); | |
631 expect(logger).toEqual(['OK', 'OK']); | |
632 logger.clear(); | |
633 | |
634 context['a'] = 'A'; | |
635 childContext['b'] = 'B'; | |
636 | |
637 watchGrp.detectChanges(); | |
638 expect(logger).toEqual(['A', 'B']); | |
639 logger.clear(); | |
640 }); | |
641 }); | |
642 | |
643 }); | |
644 | 951 |
645 class MyClass { | 952 class MyClass { |
646 final Logger logger; | 953 final Logger logger; |
647 var valA; | 954 var valA; |
648 int _count = 0; | 955 int _count = 0; |
649 | 956 |
650 MyClass(this.logger); | 957 MyClass(this.logger); |
651 | 958 |
652 methodA(arg1) { | 959 methodA(arg1) { |
653 logger('methodA($arg1) => $valA'); | 960 logger('methodA($arg1) => $valA'); |
654 return valA; | 961 return valA; |
655 } | 962 } |
656 | 963 |
657 count() => _count++; | 964 count() => _count++; |
658 | 965 |
659 String toString() => 'MyClass'; | 966 String toString() => 'MyClass'; |
660 } | 967 } |
661 | 968 |
662 class LoggingFunctionApply extends FunctionApply { | 969 class LoggingFunctionApply extends FunctionApply { |
663 Logger logger; | 970 Logger logger; |
664 LoggingFunctionApply(this.logger); | 971 LoggingFunctionApply(this.logger); |
665 apply(List args) => logger(args); | 972 apply(List args) => logger(args); |
666 } | 973 } |
OLD | NEW |