| 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 eval_test; | |
| 6 | |
| 7 import 'dart:async'; | |
| 8 | |
| 9 // Import mirrors to cause all mirrors to be retained by dart2js. | |
| 10 // The tests reflect on LinkedHashMap.length and String.length. | |
| 11 import 'dart:mirrors'; | |
| 12 | |
| 13 import 'package:polymer_expressions/eval.dart'; | |
| 14 import 'package:polymer_expressions/filter.dart'; | |
| 15 import 'package:polymer_expressions/parser.dart'; | |
| 16 import 'package:unittest/unittest.dart'; | |
| 17 import 'package:observe/observe.dart'; | |
| 18 import 'package:observe/mirrors_used.dart'; // make test smaller. | |
| 19 | |
| 20 main() { | |
| 21 reflectClass(Object); // suppress unused import warning | |
| 22 | |
| 23 group('eval', () { | |
| 24 test('should return the model for an empty expression', () { | |
| 25 expectEval('', 'model', 'model'); | |
| 26 }); | |
| 27 | |
| 28 test('should handle the "this" keyword', () { | |
| 29 expectEval('this', 'model', 'model'); | |
| 30 expectEval('this.name', 'foo', new Foo(name: 'foo')); | |
| 31 expectEval('this["a"]', 'x', {'a': 'x'}); | |
| 32 }); | |
| 33 | |
| 34 test('should return a literal int', () { | |
| 35 expectEval('1', 1); | |
| 36 expectEval('+1', 1); | |
| 37 expectEval('-1', -1); | |
| 38 }); | |
| 39 | |
| 40 test('should return a literal double', () { | |
| 41 expectEval('1.2', 1.2); | |
| 42 expectEval('+1.2', 1.2); | |
| 43 expectEval('-1.2', -1.2); | |
| 44 }); | |
| 45 | |
| 46 test('should return a literal string', () { | |
| 47 expectEval('"hello"', "hello"); | |
| 48 expectEval("'hello'", "hello"); | |
| 49 }); | |
| 50 | |
| 51 test('should return a literal boolean', () { | |
| 52 expectEval('true', true); | |
| 53 expectEval('false', false); | |
| 54 }); | |
| 55 | |
| 56 test('should return a literal null', () { | |
| 57 expectEval('null', null); | |
| 58 }); | |
| 59 | |
| 60 test('should return a literal list', () { | |
| 61 expectEval('[1, 2, 3]', equals([1, 2, 3])); | |
| 62 }); | |
| 63 | |
| 64 test('should return a literal map', () { | |
| 65 expectEval('{"a": 1}', equals(new Map.from({'a': 1}))); | |
| 66 expectEval('{"a": 1}', containsPair('a', 1)); | |
| 67 }); | |
| 68 | |
| 69 test('should call methods on a literal map', () { | |
| 70 expectEval('{"a": 1}.length', 1); | |
| 71 }); | |
| 72 | |
| 73 test('should evaluate unary operators', () { | |
| 74 expectEval('+a', 2, null, {'a': 2}); | |
| 75 expectEval('-a', -2, null, {'a': 2}); | |
| 76 expectEval('!a', false, null, {'a': true}); | |
| 77 }); | |
| 78 | |
| 79 test('should evaluate binary operators', () { | |
| 80 expectEval('1 + 2', 3); | |
| 81 expectEval('2 - 1', 1); | |
| 82 expectEval('4 / 2', 2); | |
| 83 expectEval('2 * 3', 6); | |
| 84 expectEval('5 % 2', 1); | |
| 85 expectEval('5 % -2', 1); | |
| 86 expectEval('-5 % 2', 1); | |
| 87 | |
| 88 expectEval('1 == 1', true); | |
| 89 expectEval('1 == 2', false); | |
| 90 expectEval('1 == null', false); | |
| 91 expectEval('1 != 1', false); | |
| 92 expectEval('1 != 2', true); | |
| 93 expectEval('1 != null', true); | |
| 94 | |
| 95 var x = {}; | |
| 96 var y = {}; | |
| 97 expectEval('x === y', true, null, {'x': x, 'y': x}); | |
| 98 expectEval('x !== y', false, null, {'x': x, 'y': x}); | |
| 99 expectEval('x === y', false, null, {'x': x, 'y': y}); | |
| 100 expectEval('x !== y', true, null, {'x': x, 'y': y}); | |
| 101 | |
| 102 expectEval('1 > 1', false); | |
| 103 expectEval('1 > 2', false); | |
| 104 expectEval('2 > 1', true); | |
| 105 expectEval('1 >= 1', true); | |
| 106 expectEval('1 >= 2', false); | |
| 107 expectEval('2 >= 1', true); | |
| 108 expectEval('1 < 1', false); | |
| 109 expectEval('1 < 2', true); | |
| 110 expectEval('2 < 1', false); | |
| 111 expectEval('1 <= 1', true); | |
| 112 expectEval('1 <= 2', true); | |
| 113 expectEval('2 <= 1', false); | |
| 114 | |
| 115 expectEval('true || true', true); | |
| 116 expectEval('true || false', true); | |
| 117 expectEval('false || true', true); | |
| 118 expectEval('false || false', false); | |
| 119 | |
| 120 expectEval('true && true', true); | |
| 121 expectEval('true && false', false); | |
| 122 expectEval('false && true', false); | |
| 123 expectEval('false && false', false); | |
| 124 }); | |
| 125 | |
| 126 test('should evaulate ternary operators', () { | |
| 127 expectEval('true ? 1 : 2', 1); | |
| 128 expectEval('false ? 1 : 2', 2); | |
| 129 expectEval('true ? true ? 1 : 2 : 3', 1); | |
| 130 expectEval('true ? false ? 1 : 2 : 3', 2); | |
| 131 expectEval('false ? true ? 1 : 2 : 3', 3); | |
| 132 expectEval('false ? 1 : true ? 2 : 3', 2); | |
| 133 expectEval('false ? 1 : false ? 2 : 3', 3); | |
| 134 expectEval('null ? 1 : 2', 2); | |
| 135 // TODO(justinfagnani): re-enable and check for an EvalError when | |
| 136 // we implement the final bool conversion rules and this expression | |
| 137 // throws in both checked and unchecked mode | |
| 138 // expect(() => eval(parse('42 ? 1 : 2'), null), throws); | |
| 139 }); | |
| 140 | |
| 141 test('should invoke a method on the model', () { | |
| 142 var foo = new Foo(name: 'foo', age: 2); | |
| 143 expectEval('x()', foo.x(), foo); | |
| 144 expectEval('name', foo.name, foo); | |
| 145 }); | |
| 146 | |
| 147 test('should invoke chained methods', () { | |
| 148 var foo = new Foo(name: 'foo', age: 2); | |
| 149 expectEval('name.length', foo.name.length, foo); | |
| 150 expectEval('x().toString()', foo.x().toString(), foo); | |
| 151 expectEval('name.substring(2)', foo.name.substring(2), foo); | |
| 152 expectEval('a()()', 1, null, {'a': () => () => 1}); | |
| 153 }); | |
| 154 | |
| 155 test('should invoke a top-level function', () { | |
| 156 expectEval('x()', 42, null, {'x': () => 42}); | |
| 157 expectEval('x(5)', 5, null, {'x': (i) => i}); | |
| 158 expectEval('y(5, 10)', 50, null, {'y': (i, j) => i * j}); | |
| 159 }); | |
| 160 | |
| 161 test('should give precedence to top-level functions over methods', () { | |
| 162 var foo = new Foo(name: 'foo', age: 2); | |
| 163 expectEval('x()', 42, foo, {'x': () => 42}); | |
| 164 }); | |
| 165 | |
| 166 test('should invoke the [] operator', () { | |
| 167 var map = {'a': 1, 'b': 2}; | |
| 168 expectEval('map["a"]', 1, null, {'map': map}); | |
| 169 expectEval('map["a"] + map["b"]', 3, null, {'map': map}); | |
| 170 }); | |
| 171 | |
| 172 test('should call a filter', () { | |
| 173 var topLevel = { | |
| 174 'a': 'foo', | |
| 175 'uppercase': (s) => s.toUpperCase(), | |
| 176 }; | |
| 177 expectEval('a | uppercase', 'FOO', null, topLevel); | |
| 178 }); | |
| 179 | |
| 180 test('should call a transformer', () { | |
| 181 var topLevel = { | |
| 182 'a': '42', | |
| 183 'parseInt': parseInt, | |
| 184 'add': add, | |
| 185 }; | |
| 186 expectEval('a | parseInt()', 42, null, topLevel); | |
| 187 expectEval('a | parseInt(8)', 34, null, topLevel); | |
| 188 expectEval('a | parseInt() | add(10)', 52, null, topLevel); | |
| 189 }); | |
| 190 | |
| 191 test('should filter a list', () { | |
| 192 expectEval('chars1 | filteredList', ['a', 'b'], new WordElement()); | |
| 193 }); | |
| 194 | |
| 195 test('should return null if the receiver of a method is null', () { | |
| 196 expectEval('a.b', null, null, {'a': null}); | |
| 197 expectEval('a.b()', null, null, {'a': null}); | |
| 198 }); | |
| 199 | |
| 200 test('should return null if null is invoked', () { | |
| 201 expectEval('a()', null, null, {'a': null}); | |
| 202 }); | |
| 203 | |
| 204 test('should return null if an operand is null', () { | |
| 205 expectEval('a + b', null, null, {'a': null, 'b': null}); | |
| 206 expectEval('+a', null, null, {'a': null}); | |
| 207 }); | |
| 208 | |
| 209 test('should treat null as false', () { | |
| 210 expectEval('!null', true); | |
| 211 expectEval('true && null', false); | |
| 212 expectEval('null || false', false); | |
| 213 | |
| 214 expectEval('!a', true, null, {'a': null}); | |
| 215 | |
| 216 expectEval('a && b', false, null, {'a': null, 'b': true}); | |
| 217 expectEval('a && b', false, null, {'a': true, 'b': null}); | |
| 218 expectEval('a && b', false, null, {'a': null, 'b': false}); | |
| 219 expectEval('a && b', false, null, {'a': false, 'b': null}); | |
| 220 expectEval('a && b', false, null, {'a': null, 'b': null}); | |
| 221 | |
| 222 expectEval('a || b', true, null, {'a': null, 'b': true}); | |
| 223 expectEval('a || b', true, null, {'a': true, 'b': null}); | |
| 224 expectEval('a || b', false, null, {'a': null, 'b': false}); | |
| 225 expectEval('a || b', false, null, {'a': false, 'b': null}); | |
| 226 expectEval('a || b', false, null, {'a': null, 'b': null}); | |
| 227 }); | |
| 228 | |
| 229 test('should not evaluate "in" expressions', () { | |
| 230 expect(() => eval(parse('item in items'), null), throws); | |
| 231 }); | |
| 232 | |
| 233 }); | |
| 234 | |
| 235 group('assign', () { | |
| 236 | |
| 237 test('should assign a single identifier', () { | |
| 238 var foo = new Foo(name: 'a'); | |
| 239 assign(parse('name'), 'b', new Scope(model: foo)); | |
| 240 expect(foo.name, 'b'); | |
| 241 }); | |
| 242 | |
| 243 test('should assign a sub-property', () { | |
| 244 var child = new Foo(name: 'child'); | |
| 245 var parent = new Foo(child: child); | |
| 246 assign(parse('child.name'), 'Joe', new Scope(model: parent)); | |
| 247 expect(parent.child.name, 'Joe'); | |
| 248 }); | |
| 249 | |
| 250 test('should assign an index', () { | |
| 251 var foo = new Foo(items: [1, 2, 3]); | |
| 252 assign(parse('items[0]'), 4, new Scope(model: foo)); | |
| 253 expect(foo.items[0], 4); | |
| 254 assign(parse('items[a]'), 5, new Scope(model: foo, variables: {'a': 0})); | |
| 255 expect(foo.items[0], 5); | |
| 256 }); | |
| 257 | |
| 258 test('should assign with a function call subexpression', () { | |
| 259 var child = new Foo(); | |
| 260 var foo = new Foo(items: [1, 2, 3], child: child); | |
| 261 assign(parse('getChild().name'), 'child', new Scope(model: foo)); | |
| 262 expect(child.name, 'child'); | |
| 263 }); | |
| 264 | |
| 265 test('should assign through transformers', () { | |
| 266 var foo = new Foo(name: '42', age: 32); | |
| 267 var globals = { | |
| 268 'a': '42', | |
| 269 'parseInt': parseInt, | |
| 270 'add': add, | |
| 271 }; | |
| 272 var scope = new Scope(model: foo, variables: globals); | |
| 273 assign(parse('age | add(7)'), 29, scope); | |
| 274 expect(foo.age, 22); | |
| 275 assign(parse('name | parseInt() | add(10)'), 29, scope); | |
| 276 expect(foo.name, '19'); | |
| 277 }); | |
| 278 | |
| 279 test('should not throw on assignments to properties on null', () { | |
| 280 assign(parse('name'), 'b', new Scope(model: null)); | |
| 281 }); | |
| 282 | |
| 283 test('should throw on assignments to non-assignable expressions', () { | |
| 284 var foo = new Foo(name: 'a'); | |
| 285 var scope = new Scope(model: foo); | |
| 286 expect(() => assign(parse('name + 1'), 1, scope), | |
| 287 throwsA(new isInstanceOf<EvalException>())); | |
| 288 expect(() => assign(parse('toString()'), 1, scope), | |
| 289 throwsA(new isInstanceOf<EvalException>())); | |
| 290 expect(() => assign(parse('name | filter'), 1, scope), | |
| 291 throwsA(new isInstanceOf<EvalException>())); | |
| 292 }); | |
| 293 | |
| 294 test('should not throw on assignments to non-assignable expressions if ' | |
| 295 'checkAssignability is false', () { | |
| 296 var foo = new Foo(name: 'a'); | |
| 297 var scope = new Scope(model: foo); | |
| 298 expect( | |
| 299 assign(parse('name + 1'), 1, scope, checkAssignability: false), | |
| 300 null); | |
| 301 expect( | |
| 302 assign(parse('toString()'), 1, scope, checkAssignability: false), | |
| 303 null); | |
| 304 expect( | |
| 305 assign(parse('name | filter'), 1, scope, checkAssignability: false), | |
| 306 null); | |
| 307 }); | |
| 308 | |
| 309 }); | |
| 310 | |
| 311 group('scope', () { | |
| 312 test('should return fields on the model', () { | |
| 313 var foo = new Foo(name: 'a', age: 1); | |
| 314 var scope = new Scope(model: foo); | |
| 315 expect(scope['name'], 'a'); | |
| 316 expect(scope['age'], 1); | |
| 317 }); | |
| 318 | |
| 319 test('should throw for undefined names', () { | |
| 320 var scope = new Scope(); | |
| 321 expect(() => scope['a'], throwsException); | |
| 322 }); | |
| 323 | |
| 324 test('should return variables', () { | |
| 325 var scope = new Scope(variables: {'a': 'A'}); | |
| 326 expect(scope['a'], 'A'); | |
| 327 }); | |
| 328 | |
| 329 test("should a field from the parent's model", () { | |
| 330 var parent = new Scope(variables: {'a': 'A', 'b': 'B'}); | |
| 331 var child = parent.childScope('a', 'a'); | |
| 332 expect(child['a'], 'a'); | |
| 333 expect(parent['a'], 'A'); | |
| 334 expect(child['b'], 'B'); | |
| 335 }); | |
| 336 | |
| 337 }); | |
| 338 | |
| 339 group('observe', () { | |
| 340 test('should observe an identifier', () { | |
| 341 var foo = new Foo(name: 'foo'); | |
| 342 return expectObserve('name', | |
| 343 model: foo, | |
| 344 beforeMatcher: 'foo', | |
| 345 mutate: () { | |
| 346 foo.name = 'fooz'; | |
| 347 }, | |
| 348 afterMatcher: 'fooz' | |
| 349 ); | |
| 350 }); | |
| 351 | |
| 352 test('should observe an invocation', () { | |
| 353 var foo = new Foo(name: 'foo'); | |
| 354 return expectObserve('foo.name', | |
| 355 variables: {'foo': foo}, | |
| 356 beforeMatcher: 'foo', | |
| 357 mutate: () { | |
| 358 foo.name = 'fooz'; | |
| 359 }, | |
| 360 afterMatcher: 'fooz' | |
| 361 ); | |
| 362 }); | |
| 363 | |
| 364 test('should observe map access', () { | |
| 365 var foo = toObservable({'one': 'one', 'two': 'two'}); | |
| 366 return expectObserve('foo["one"]', | |
| 367 variables: {'foo': foo}, | |
| 368 beforeMatcher: 'one', | |
| 369 mutate: () { | |
| 370 foo['one'] = '1'; | |
| 371 }, | |
| 372 afterMatcher: '1' | |
| 373 ); | |
| 374 }); | |
| 375 | |
| 376 }); | |
| 377 | |
| 378 } | |
| 379 | |
| 380 @reflectable | |
| 381 class Foo extends ChangeNotifier { | |
| 382 String _name; | |
| 383 String get name => _name; | |
| 384 void set name(String n) { | |
| 385 _name = notifyPropertyChange(#name, _name, n); | |
| 386 } | |
| 387 | |
| 388 int age; | |
| 389 Foo child; | |
| 390 List<int> items; | |
| 391 | |
| 392 Foo({name, this.age, this.child, this.items}) : _name = name; | |
| 393 | |
| 394 int x() => age * age; | |
| 395 | |
| 396 getChild() => child; | |
| 397 | |
| 398 filter(i) => i; | |
| 399 } | |
| 400 | |
| 401 @reflectable | |
| 402 class ListHolder { | |
| 403 List items; | |
| 404 ListHolder(this.items); | |
| 405 } | |
| 406 | |
| 407 parseInt([int radix = 10]) => new IntToString(radix: radix); | |
| 408 | |
| 409 class IntToString extends Transformer<int, String> { | |
| 410 final int radix; | |
| 411 IntToString({this.radix: 10}); | |
| 412 int forward(String s) => int.parse(s, radix: radix); | |
| 413 String reverse(int i) => '$i'; | |
| 414 } | |
| 415 | |
| 416 add(int i) => new Add(i); | |
| 417 | |
| 418 class Add extends Transformer<int, int> { | |
| 419 final int i; | |
| 420 Add(this.i); | |
| 421 int forward(int x) => x + i; | |
| 422 int reverse(int x) => x - i; | |
| 423 } | |
| 424 | |
| 425 Object evalString(String s, [Object model, Map vars]) => | |
| 426 eval(new Parser(s).parse(), new Scope(model: model, variables: vars)); | |
| 427 | |
| 428 expectEval(String s, dynamic matcher, [Object model, Map vars = const {}]) { | |
| 429 var expr = new Parser(s).parse(); | |
| 430 var scope = new Scope(model: model, variables: vars); | |
| 431 expect(eval(expr, scope), matcher, reason: s); | |
| 432 | |
| 433 var observer = observe(expr, scope); | |
| 434 new Updater(scope).visit(observer); | |
| 435 expect(observer.currentValue, matcher, reason: s); | |
| 436 } | |
| 437 | |
| 438 expectObserve(String s, { | |
| 439 Object model, | |
| 440 Map variables: const {}, | |
| 441 dynamic beforeMatcher, | |
| 442 mutate(), | |
| 443 dynamic afterMatcher}) { | |
| 444 | |
| 445 var scope = new Scope(model: model, variables: variables); | |
| 446 var observer = observe(new Parser(s).parse(), scope); | |
| 447 update(observer, scope); | |
| 448 expect(observer.currentValue, beforeMatcher); | |
| 449 var passed = false; | |
| 450 var future = observer.onUpdate.first.then((value) { | |
| 451 expect(value, afterMatcher); | |
| 452 expect(observer.currentValue, afterMatcher); | |
| 453 passed = true; | |
| 454 }); | |
| 455 mutate(); | |
| 456 // fail if we don't receive an update by the next event loop | |
| 457 return Future.wait([future, new Future(() { | |
| 458 expect(passed, true, reason: "Didn't receive a change notification on $s"); | |
| 459 })]); | |
| 460 } | |
| 461 | |
| 462 // Regression test from https://code.google.com/p/dart/issues/detail?id=13459 | |
| 463 class WordElement extends Observable { | |
| 464 @observable List chars1 = 'abcdefg'.split(''); | |
| 465 @reflectable List filteredList(List original) => [original[0], original[1]]; | |
| 466 } | |
| OLD | NEW |