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 |