diff options
| author | Igor Minar | 2013-10-07 09:58:37 -0700 | 
|---|---|---|
| committer | Igor Minar | 2013-10-09 15:15:43 -0700 | 
| commit | 5dc35b527b3c99f6544b8cb52e93c6510d3ac577 (patch) | |
| tree | e6a0a6c61084b2658ccff50f6f2e599550784c74 /test/ng/parseSpec.js | |
| parent | 2fe5a2def026ac1aa63a98be7135b93784677129 (diff) | |
| download | angular.js-5dc35b527b3c99f6544b8cb52e93c6510d3ac577.tar.bz2 | |
fix($parse): deprecate promise unwrapping and make it an opt-in
This commit disables promise unwrapping and adds
$parseProvider.unwrapPromises() getter/setter api that allows developers
to turn the feature back on if needed. Promise unwrapping support will
be removed from Angular in the future and this setting only allows for
enabling it during transitional period.
If the unwrapping is enabled, Angular will log a warning about each
expression that unwraps a promise (to reduce the noise, each expression
is logged only onces). To disable this logging use
`$parseProvider.logPromiseWarnings(false)`.
Previously promises found anywhere in the expression during expression
evaluation would evaluate to undefined while unresolved and to the
fulfillment value if fulfilled.
This is a feature that didn't prove to be wildly useful or popular,
primarily because of the dichotomy between data access in templates
(accessed as raw values) and controller code (accessed as promises).
In most code we ended up resolving promises manually in controllers
or automatically via routing and unifying the model access in this way.
Other downsides of automatic promise unwrapping:
- when building components it's often desirable to receive the
  raw promises
- adds complexity and slows down expression evaluation
- makes expression code pre-generation unattractive due to the
  amount of code that needs to be generated
- makes IDE auto-completion and tool support hard
- adds too much magic
BREAKING CHANGE: $parse and templates in general will no longer
automatically unwrap promises. This feature has been deprecated and
if absolutely needed, it can be reenabled during transitional period
via `$parseProvider.unwrapPromises(true)` api.
Closes #4158
Closes #4270
Diffstat (limited to 'test/ng/parseSpec.js')
| -rw-r--r-- | test/ng/parseSpec.js | 363 | 
1 files changed, 245 insertions, 118 deletions
| diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 19182332..277178a1 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -1,12 +1,20 @@  'use strict';  describe('parser', function() { + +  beforeEach(function() { +    // clear caches +    getterFnCache = {}; +    promiseWarningCache = {}; +  }); + +    describe('lexer', function() {      var lex;      beforeEach(function () {        lex = function () { -        var lexer = new Lexer(); +        var lexer = new Lexer({csp: false, unwrapPromises: false});          return lexer.lex.apply(lexer, arguments);        };      }); @@ -198,7 +206,6 @@ describe('parser', function() {        beforeEach(inject(function ($rootScope, $sniffer) {          scope = $rootScope;          $sniffer.csp = cspEnabled; -        getterFnCache = {}; // clear cache        })); @@ -854,15 +861,228 @@ describe('parser', function() {        }); -      describe('promises', function() { -        var deferred, promise, q; +      describe('assignable', function() { +        it('should expose assignment function', inject(function($parse) { +          var fn = $parse('a'); +          expect(fn.assign).toBeTruthy(); +          var scope = {}; +          fn.assign(scope, 123); +          expect(scope).toEqual({a:123}); +        })); +      }); + + +      describe('locals', function() { +        it('should expose local variables', inject(function($parse) { +          expect($parse('a')({a: 0}, {a: 1})).toEqual(1); +          expect($parse('add(a,b)')({b: 1, add: function(a, b) { return a + b; }}, {a: 2})).toEqual(3); +        })); + +        it('should expose traverse locals', inject(function($parse) { +          expect($parse('a.b')({a: {b: 0}}, {a: {b:1}})).toEqual(1); +          expect($parse('a.b')({a: null}, {a: {b:1}})).toEqual(1); +          expect($parse('a.b')({a: {b: 0}}, {a: null})).toEqual(undefined); +        })); +      }); + +      describe('literal', function() { +        it('should mark scalar value expressions as literal', inject(function($parse) { +          expect($parse('0').literal).toBe(true); +          expect($parse('"hello"').literal).toBe(true); +          expect($parse('true').literal).toBe(true); +          expect($parse('false').literal).toBe(true); +          expect($parse('null').literal).toBe(true); +          expect($parse('undefined').literal).toBe(true); +        })); + +        it('should mark array expressions as literal', inject(function($parse) { +          expect($parse('[]').literal).toBe(true); +          expect($parse('[1, 2, 3]').literal).toBe(true); +          expect($parse('[1, identifier]').literal).toBe(true); +        })); + +        it('should mark object expressions as literal', inject(function($parse) { +          expect($parse('{}').literal).toBe(true); +          expect($parse('{x: 1}').literal).toBe(true); +          expect($parse('{foo: bar}').literal).toBe(true); +        })); + +        it('should not mark function calls or operator expressions as literal', inject(function($parse) { +          expect($parse('1 + 1').literal).toBe(false); +          expect($parse('call()').literal).toBe(false); +          expect($parse('[].length').literal).toBe(false); +        })); +      }); + +      describe('constant', function() { +        it('should mark scalar value expressions as constant', inject(function($parse) { +          expect($parse('12.3').constant).toBe(true); +          expect($parse('"string"').constant).toBe(true); +          expect($parse('true').constant).toBe(true); +          expect($parse('false').constant).toBe(true); +          expect($parse('null').constant).toBe(true); +          expect($parse('undefined').constant).toBe(true); +        })); + +        it('should mark arrays as constant if they only contain constant elements', inject(function($parse) { +          expect($parse('[]').constant).toBe(true); +          expect($parse('[1, 2, 3]').constant).toBe(true); +          expect($parse('["string", null]').constant).toBe(true); +          expect($parse('[[]]').constant).toBe(true); +          expect($parse('[1, [2, 3], {4: 5}]').constant).toBe(true); +        })); + +        it('should not mark arrays as constant if they contain any non-constant elements', inject(function($parse) { +          expect($parse('[foo]').constant).toBe(false); +          expect($parse('[x + 1]').constant).toBe(false); +          expect($parse('[bar[0]]').constant).toBe(false); +        })); + +        it('should mark complex expressions involving constant values as constant', inject(function($parse) { +          expect($parse('!true').constant).toBe(true); +          expect($parse('1 - 1').constant).toBe(true); +          expect($parse('"foo" + "bar"').constant).toBe(true); +          expect($parse('5 != null').constant).toBe(true); +          expect($parse('{standard: 4/3, wide: 16/9}').constant).toBe(true); +        })); + +        it('should not mark any expression involving variables or function calls as constant', inject(function($parse) { +          expect($parse('true.toString()').constant).toBe(false); +          expect($parse('foo(1, 2, 3)').constant).toBe(false); +          expect($parse('"name" + id').constant).toBe(false); +        })); +      }); +    }); +  }); + + +  describe('promises', function() { + +    var deferred, promise, q; + +    describe('unwrapPromises setting', function () { + +      beforeEach(inject(function($rootScope, $q) { +        scope = $rootScope; + +        $rootScope.$apply(function() { +          deferred = $q.defer(); +          deferred.resolve('Bobo'); +          promise = deferred.promise; +        }); +      })); + +      it('should not unwrap promises by default', inject(function ($parse) { +        scope.person = promise; +        scope.things = {person: promise}; +        scope.getPerson = function () { return promise; }; + +        var getter = $parse('person'); +        var propGetter = $parse('things.person'); +        var fnGetter = $parse('getPerson()'); + +        expect(getter(scope)).toBe(promise); +        expect(propGetter(scope)).toBe(promise); +        expect(fnGetter(scope)).toBe(promise); +      })); +    }); + + +    forEach([true, false], function(cspEnabled) { + +      describe('promise logging (csp:' + cspEnabled + ')', function() { + +        var $log; +        var PROMISE_WARNING_REGEXP = /\[\$parse\] Promise found in the expression `[^`]+`. Automatic unwrapping of promises in Angular expressions is deprecated\./; + +        beforeEach(module(function($parseProvider) { +          $parseProvider.unwrapPromises(true); +        })); + +        beforeEach(inject(function($rootScope, $q, _$log_) { +          scope = $rootScope; + +          $rootScope.$apply(function() { +            deferred = $q.defer(); +            deferred.resolve('Bobo'); +            promise = deferred.promise; +          }); + +          $log = _$log_; +        })); + +        it('should log warnings by default', function() { +          scope.person = promise; +          scope.$eval('person'); +          expect($log.warn.logs.pop()).toEqual(['[$parse] Promise found in the expression `person`. ' + +              'Automatic unwrapping of promises in Angular expressions is deprecated.']); +        }); + + +        it('should log warnings for deep promises', function() { +          scope.car = {wheel: {disc: promise}}; +          scope.$eval('car.wheel.disc.pad'); +          expect($log.warn.logs.pop()).toMatch(PROMISE_WARNING_REGEXP); +        }); + + +        it('should log warnings for setters', function() { +          scope.person = promise; +          scope.$eval('person.name = "Bubu"'); +          expect($log.warn.logs.pop()).toMatch(PROMISE_WARNING_REGEXP); +        }); + + +        it('should log only a single warning for each expression', function() { +          scope.person1 = promise; +          scope.person2 = promise; + +          scope.$eval('person1'); +          scope.$eval('person1'); +          expect($log.warn.logs.pop()).toMatch(/`person1`/); +          expect($log.warn.logs).toEqual([]); + +          scope.$eval('person1'); +          scope.$eval('person2'); +          scope.$eval('person1'); +          scope.$eval('person2'); +          expect($log.warn.logs.pop()).toMatch(/`person2`/); +          expect($log.warn.logs).toEqual([]); +        }); + + +        it('should log warning for complex expressions', function() { +          scope.person1 = promise; +          scope.person2 = promise; + +          scope.$eval('person1 + person2'); +          expect($log.warn.logs.pop()).toMatch(/`person1 \+ person2`/); +          expect($log.warn.logs).toEqual([]); +        }); +      }); +    }); + + +    forEach([true, false], function(cspEnabled) { + +      describe('csp ' + cspEnabled, function() { + +        beforeEach(module(function($parseProvider) { +          $parseProvider.unwrapPromises(true); +          $parseProvider.logPromiseWarnings(false); +        })); + + +        beforeEach(inject(function($rootScope, $sniffer, $q) { +          scope = $rootScope; +          $sniffer.csp = cspEnabled; -        beforeEach(inject(function($q) {            q = $q;            deferred = q.defer();            promise = deferred.promise;          })); +          describe('{{promise}}', function() {            it('should evaluated resolved promise and get its value', function() {              deferred.resolve('hello!'); @@ -876,7 +1096,7 @@ describe('parser', function() {            it('should evaluated rejected promise and ignore the rejection reason', function() {              deferred.reject('sorry');              scope.greeting = promise; -            expect(scope.$eval('gretting')).toBe(undefined); +            expect(scope.$eval('greeting')).toBe(undefined);              scope.$digest();              expect(scope.$eval('greeting')).toBe(undefined);            }); @@ -929,7 +1149,7 @@ describe('parser', function() {                scope.person = promise;                deferred.resolve({'name': 'Bill Gates'}); -              var getter = $parse('person.name'); +              var getter = $parse('person.name', { unwrapPromises: true });                expect(getter(scope)).toBe(undefined);                scope.$digest(); @@ -943,7 +1163,7 @@ describe('parser', function() {                scope.greeting = promise;                deferred.resolve('Salut!'); -              var getter = $parse('greeting'); +              var getter = $parse('greeting', { unwrapPromises: true });                expect(getter(scope)).toBe(undefined);                scope.$digest(); @@ -957,7 +1177,7 @@ describe('parser', function() {              it('should evaluate an unresolved promise and set and remember its value', inject(function($parse) {                scope.person = promise; -              var getter = $parse('person.name'); +              var getter = $parse('person.name', { unwrapPromises: true });                expect(getter(scope)).toBe(undefined);                scope.$digest(); @@ -968,7 +1188,7 @@ describe('parser', function() {                expect(getter(scope)).toBe('Bonjour'); -              var c1Getter = $parse('person.A.B.C1'); +              var c1Getter = $parse('person.A.B.C1', { unwrapPromises: true });                scope.$digest();                expect(c1Getter(scope)).toBe(undefined);                c1Getter.assign(scope, 'c1_value'); @@ -976,7 +1196,7 @@ describe('parser', function() {                expect(c1Getter(scope)).toBe('c1_value');                // Set another property on the person.A.B -              var c2Getter = $parse('person.A.B.C2'); +              var c2Getter = $parse('person.A.B.C2', { unwrapPromises: true });                scope.$digest();                expect(c2Getter(scope)).toBe(undefined);                c2Getter.assign(scope, 'c2_value'); @@ -984,15 +1204,15 @@ describe('parser', function() {                expect(c2Getter(scope)).toBe('c2_value');                // c1 should be unchanged. -              expect($parse('person.A')(scope)).toEqual( +              expect($parse('person.A', { unwrapPromises: true })(scope)).toEqual(                    {B: {C1: 'c1_value', C2: 'c2_value'}});              }));              it('should evaluate a resolved promise and overwrite the previous set value in the absense of the getter', -               inject(function($parse) { +                inject(function($parse) {                scope.person = promise; -              var c1Getter = $parse('person.A.B.C1'); +              var c1Getter = $parse('person.A.B.C1', { unwrapPromises: true });                c1Getter.assign(scope, 'c1_value');                // resolving the promise should update the tree.                deferred.resolve({A: {B: {C1: 'resolved_c1'}}}); @@ -1037,19 +1257,19 @@ describe('parser', function() {            it('should evaluate and dereference array references leading to and from a promise',                function() { -            scope.greetings = [promise]; -            expect(scope.$eval('greetings[0]')).toBe(undefined); -            expect(scope.$eval('greetings[0][0]')).toBe(undefined); +                scope.greetings = [promise]; +                expect(scope.$eval('greetings[0]')).toBe(undefined); +                expect(scope.$eval('greetings[0][0]')).toBe(undefined); -            scope.$digest(); -            expect(scope.$eval('greetings[0]')).toBe(undefined); -            expect(scope.$eval('greetings[0][0]')).toBe(undefined); +                scope.$digest(); +                expect(scope.$eval('greetings[0]')).toBe(undefined); +                expect(scope.$eval('greetings[0][0]')).toBe(undefined); -            deferred.resolve(['Hi!', 'Cau!']); -            scope.$digest(); -            expect(scope.$eval('greetings[0]')).toEqual(['Hi!', 'Cau!']); -            expect(scope.$eval('greetings[0][0]')).toBe('Hi!'); -          }); +                deferred.resolve(['Hi!', 'Cau!']); +                scope.$digest(); +                expect(scope.$eval('greetings[0]')).toEqual(['Hi!', 'Cau!']); +                expect(scope.$eval('greetings[0][0]')).toBe('Hi!'); +              });            it('should evaluate and dereference promises used as function arguments', function() { @@ -1109,99 +1329,6 @@ describe('parser', function() {            });          });        }); - - -      describe('assignable', function() { -        it('should expose assignment function', inject(function($parse) { -          var fn = $parse('a'); -          expect(fn.assign).toBeTruthy(); -          var scope = {}; -          fn.assign(scope, 123); -          expect(scope).toEqual({a:123}); -        })); -      }); - - -      describe('locals', function() { -        it('should expose local variables', inject(function($parse) { -          expect($parse('a')({a: 0}, {a: 1})).toEqual(1); -          expect($parse('add(a,b)')({b: 1, add: function(a, b) { return a + b; }}, {a: 2})).toEqual(3); -        })); - -        it('should expose traverse locals', inject(function($parse) { -          expect($parse('a.b')({a: {b: 0}}, {a: {b:1}})).toEqual(1); -          expect($parse('a.b')({a: null}, {a: {b:1}})).toEqual(1); -          expect($parse('a.b')({a: {b: 0}}, {a: null})).toEqual(undefined); -        })); -      }); - -      describe('literal', function() { -        it('should mark scalar value expressions as literal', inject(function($parse) { -          expect($parse('0').literal).toBe(true); -          expect($parse('"hello"').literal).toBe(true); -          expect($parse('true').literal).toBe(true); -          expect($parse('false').literal).toBe(true); -          expect($parse('null').literal).toBe(true); -          expect($parse('undefined').literal).toBe(true); -        })); - -        it('should mark array expressions as literal', inject(function($parse) { -          expect($parse('[]').literal).toBe(true); -          expect($parse('[1, 2, 3]').literal).toBe(true); -          expect($parse('[1, identifier]').literal).toBe(true); -        })); - -        it('should mark object expressions as literal', inject(function($parse) { -          expect($parse('{}').literal).toBe(true); -          expect($parse('{x: 1}').literal).toBe(true); -          expect($parse('{foo: bar}').literal).toBe(true); -        })); - -        it('should not mark function calls or operator expressions as literal', inject(function($parse) { -          expect($parse('1 + 1').literal).toBe(false); -          expect($parse('call()').literal).toBe(false); -          expect($parse('[].length').literal).toBe(false); -        })); -      }); - -      describe('constant', function() { -        it('should mark scalar value expressions as constant', inject(function($parse) { -          expect($parse('12.3').constant).toBe(true); -          expect($parse('"string"').constant).toBe(true); -          expect($parse('true').constant).toBe(true); -          expect($parse('false').constant).toBe(true); -          expect($parse('null').constant).toBe(true); -          expect($parse('undefined').constant).toBe(true); -        })); - -        it('should mark arrays as constant if they only contain constant elements', inject(function($parse) { -          expect($parse('[]').constant).toBe(true); -          expect($parse('[1, 2, 3]').constant).toBe(true); -          expect($parse('["string", null]').constant).toBe(true); -          expect($parse('[[]]').constant).toBe(true); -          expect($parse('[1, [2, 3], {4: 5}]').constant).toBe(true); -        })); - -        it('should not mark arrays as constant if they contain any non-constant elements', inject(function($parse) { -          expect($parse('[foo]').constant).toBe(false); -          expect($parse('[x + 1]').constant).toBe(false); -          expect($parse('[bar[0]]').constant).toBe(false); -        })); - -        it('should mark complex expressions involving constant values as constant', inject(function($parse) { -          expect($parse('!true').constant).toBe(true); -          expect($parse('1 - 1').constant).toBe(true); -          expect($parse('"foo" + "bar"').constant).toBe(true); -          expect($parse('5 != null').constant).toBe(true); -          expect($parse('{standard: 4/3, wide: 16/9}').constant).toBe(true); -        })); - -        it('should not mark any expression involving variables or function calls as constant', inject(function($parse) { -          expect($parse('true.toString()').constant).toBe(false); -          expect($parse('foo(1, 2, 3)').constant).toBe(false); -          expect($parse('"name" + id').constant).toBe(false); -        })); -      });      });    });  }); | 
