')($rootScope);
+ $rootScope.$apply();
+
+ expect(attrs.title).toBe('12');
+ expect(attrs.$attr.title).toBe('title');
+ expect(attrs.ngAttrTitle).toBeUndefined();
+ expect(attrs.$attr.ngAttrTitle).toBeUndefined();
+
+ expect(attrs.superTitle).toBe('34');
+ expect(attrs.$attr.superTitle).toBe('super-title');
+ expect(attrs.ngAttrSuperTitle).toBeUndefined();
+ expect(attrs.$attr.ngAttrSuperTitle).toBeUndefined();
+
+ // Note the casing is incorrect: https://github.com/angular/angular.js/issues/16624
+ expect(attrs.myCameltitle).toBe('56');
+ expect(attrs.$attr.myCameltitle).toBe('my-camelTitle');
+ expect(attrs.ngAttrMyCameltitle).toBeUndefined();
+ expect(attrs.ngAttrMyCamelTitle).toBeUndefined();
+ expect(attrs.$attr.ngAttrMyCameltitle).toBeUndefined();
+ expect(attrs.$attr.ngAttrMyCamelTitle).toBeUndefined();
+ });
+ });
+
it('should work with the "href" attribute', inject(function() {
$rootScope.value = 'test';
element = $compile('
')($rootScope);
@@ -12112,6 +12146,112 @@ describe('$compile', function() {
});
+ describe('addPropertySecurityContext', function() {
+ function testProvider(provider) {
+ module(provider);
+ inject(function($compile) { /* done! */ });
+ }
+
+ it('should allow adding new properties', function() {
+ testProvider(function($compileProvider) {
+ $compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
+ $compileProvider.addPropertySecurityContext('*', 'my-prop', 'resourceUrl');
+ });
+ });
+
+ it('should allow different sce types of a property on different element types', function() {
+ testProvider(function($compileProvider) {
+ $compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
+ $compileProvider.addPropertySecurityContext('span', 'title', 'css');
+ $compileProvider.addPropertySecurityContext('*', 'title', 'resourceUrl');
+ $compileProvider.addPropertySecurityContext('article', 'title', 'html');
+ });
+ });
+
+ it('should throw \'ctxoverride\' when changing an existing context', function() {
+ testProvider(function($compileProvider) {
+ $compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
+
+ expect(function() {
+ $compileProvider.addPropertySecurityContext('div', 'title', 'resourceUrl');
+ })
+ .toThrowMinErr('$compile', 'ctxoverride', 'Property context \'div.title\' already set to \'mediaUrl\', cannot override to \'resourceUrl\'.');
+ });
+ });
+
+ it('should allow setting the same property/element to the same value', function() {
+ testProvider(function($compileProvider) {
+ $compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
+ $compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
+ });
+ });
+
+ it('should enforce the specified sce type for properties added for specific elements', function() {
+ module(function($compileProvider) {
+ $compileProvider.addPropertySecurityContext('div', 'foo', 'mediaUrl');
+ });
+ inject(function($compile, $rootScope, $sce) {
+ var element = $compile('
')($rootScope);
+
+ $rootScope.bar = 'untrusted:test1';
+ $rootScope.$apply();
+ expect(element.prop('foo')).toBe('unsafe:untrusted:test1');
+
+ $rootScope.bar = $sce.trustAsCss('untrusted:test2');
+ $rootScope.$apply();
+ expect(element.prop('foo')).toBe('unsafe:untrusted:test2');
+
+ $rootScope.bar = $sce.trustAsMediaUrl('untrusted:test3');
+ $rootScope.$apply();
+ expect(element.prop('foo')).toBe('untrusted:test3');
+ });
+ });
+
+ it('should enforce the specified sce type for properties added for all elements (*)', function() {
+ module(function($compileProvider) {
+ $compileProvider.addPropertySecurityContext('*', 'foo', 'mediaUrl');
+ });
+ inject(function($compile, $rootScope, $sce) {
+ var element = $compile('
')($rootScope);
+
+ $rootScope.bar = 'untrusted:test1';
+ $rootScope.$apply();
+ expect(element.prop('foo')).toBe('unsafe:untrusted:test1');
+
+ $rootScope.bar = $sce.trustAsCss('untrusted:test2');
+ $rootScope.$apply();
+ expect(element.prop('foo')).toBe('unsafe:untrusted:test2');
+
+ $rootScope.bar = $sce.trustAsMediaUrl('untrusted:test3');
+ $rootScope.$apply();
+ expect(element.prop('foo')).toBe('untrusted:test3');
+ });
+ });
+
+ it('should enforce the specific sce type when both an element specific and generic exist', function() {
+ module(function($compileProvider) {
+ $compileProvider.addPropertySecurityContext('*', 'foo', 'css');
+ $compileProvider.addPropertySecurityContext('div', 'foo', 'mediaUrl');
+ });
+ inject(function($compile, $rootScope, $sce) {
+ var element = $compile('
')($rootScope);
+
+ $rootScope.bar = 'untrusted:test1';
+ $rootScope.$apply();
+ expect(element.prop('foo')).toBe('unsafe:untrusted:test1');
+
+ $rootScope.bar = $sce.trustAsCss('untrusted:test2');
+ $rootScope.$apply();
+ expect(element.prop('foo')).toBe('unsafe:untrusted:test2');
+
+ $rootScope.bar = $sce.trustAsMediaUrl('untrusted:test3');
+ $rootScope.$apply();
+ expect(element.prop('foo')).toBe('untrusted:test3');
+ });
+ });
+ });
+
+
describe('when an attribute has an underscore-separated name', function() {
it('should work with different prefixes', inject(function($compile, $rootScope) {
diff --git a/test/ng/ngOnSpec.js b/test/ng/ngOnSpec.js
new file mode 100644
index 000000000000..9da918e881c2
--- /dev/null
+++ b/test/ng/ngOnSpec.js
@@ -0,0 +1,158 @@
+'use strict';
+
+describe('ngOn* event binding', function() {
+ it('should add event listener of specified name', inject(function($compile, $rootScope) {
+ $rootScope.name = 'Misko';
+ var element = $compile('
')($rootScope);
+ element.triggerHandler('foo');
+ expect($rootScope.name).toBe('Misko3');
+ }));
+
+ it('should use angular.element(x).on() API to add listener', inject(function($compile, $rootScope) {
+ spyOn(angular.element.prototype, 'on');
+
+ var element = $compile('
')($rootScope);
+
+ expect(angular.element.prototype.on).toHaveBeenCalledWith('foo', jasmine.any(Function));
+ }));
+
+ it('should allow access to the $event object', inject(function($rootScope, $compile) {
+ var element = $compile('
')($rootScope);
+ element.triggerHandler('foo');
+ expect($rootScope.e.target).toBe(element[0]);
+ }));
+
+ it('should call the listener synchronously', inject(function($compile, $rootScope) {
+ var element = $compile('
')($rootScope);
+ $rootScope.fooEvent = jasmine.createSpy('fooEvent');
+
+ element.triggerHandler('foo');
+
+ expect($rootScope.fooEvent).toHaveBeenCalledOnce();
+ }));
+
+ it('should support multiple events on a single element', inject(function($compile, $rootScope) {
+ var element = $compile('
')($rootScope);
+ $rootScope.fooEvent = jasmine.createSpy('fooEvent');
+ $rootScope.barEvent = jasmine.createSpy('barEvent');
+
+ element.triggerHandler('foo');
+ expect($rootScope.fooEvent).toHaveBeenCalled();
+ expect($rootScope.barEvent).not.toHaveBeenCalled();
+
+ $rootScope.fooEvent.calls.reset();
+ $rootScope.barEvent.calls.reset();
+
+ element.triggerHandler('bar');
+ expect($rootScope.fooEvent).not.toHaveBeenCalled();
+ expect($rootScope.barEvent).toHaveBeenCalled();
+ }));
+
+ it('should work with different prefixes', inject(function($rootScope, $compile) {
+ var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
+ var element = $compile('
')($rootScope);
+
+ element.triggerHandler('test');
+ expect(cb).toHaveBeenCalledWith(1);
+
+ element.triggerHandler('test2');
+ expect(cb).toHaveBeenCalledWith(2);
+
+ element.triggerHandler('test3');
+ expect(cb).toHaveBeenCalledWith(3);
+ }));
+
+ it('should work if they are prefixed with x- or data- and different prefixes', inject(function($rootScope, $compile) {
+ var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
+ var element = $compile('
')($rootScope);
+
+ element.triggerHandler('test2');
+ expect(cb).toHaveBeenCalledWith(2);
+
+ element.triggerHandler('test3');
+ expect(cb).toHaveBeenCalledWith(3);
+
+ element.triggerHandler('test4');
+ expect(cb).toHaveBeenCalledWith(4);
+
+ element.triggerHandler('test5');
+ expect(cb).toHaveBeenCalledWith(5);
+
+ element.triggerHandler('test6');
+ expect(cb).toHaveBeenCalledWith(6);
+ }));
+
+ it('should work independently of attributes with the same name', inject(function($rootScope, $compile) {
+ var element = $compile('
')($rootScope);
+ var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
+ $rootScope.$digest();
+ element.triggerHandler('asdf');
+ expect(cb).toHaveBeenCalled();
+ expect(element.attr('asdf')).toBe('foo');
+ }));
+
+ it('should work independently of (ng-)attributes with the same name', inject(function($rootScope, $compile) {
+ var element = $compile('
')($rootScope);
+ var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
+ $rootScope.$digest();
+ element.triggerHandler('asdf');
+ expect(cb).toHaveBeenCalled();
+ expect(element.attr('asdf')).toBe('foo');
+ }));
+
+ it('should work independently of properties with the same name', inject(function($rootScope, $compile) {
+ var element = $compile('
')($rootScope);
+ var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
+ $rootScope.$digest();
+ element.triggerHandler('asdf');
+ expect(cb).toHaveBeenCalled();
+ expect(element.prop('asdf')).toBe(123);
+ }));
+
+ it('should use the full ng-on-* attribute name in $attr mappings', function() {
+ var attrs;
+ module(function($compileProvider) {
+ $compileProvider.directive('attrExposer', valueFn({
+ link: function($scope, $element, $attrs) {
+ attrs = $attrs;
+ }
+ }));
+ });
+ inject(function($compile, $rootScope) {
+ $compile('
')($rootScope);
+
+ expect(attrs.title).toBeUndefined();
+ expect(attrs.$attr.title).toBeUndefined();
+ expect(attrs.ngOnTitle).toBe('cb(1)');
+ expect(attrs.$attr.ngOnTitle).toBe('ng-on-title');
+
+ expect(attrs.superTitle).toBeUndefined();
+ expect(attrs.$attr.superTitle).toBeUndefined();
+ expect(attrs.ngOnSuperTitle).toBe('cb(2)');
+ expect(attrs.$attr.ngOnSuperTitle).toBe('ng-on-super-title');
+
+ expect(attrs.myCamelTitle).toBeUndefined();
+ expect(attrs.$attr.myCamelTitle).toBeUndefined();
+ expect(attrs.ngOnMyCamelTitle).toBe('cb(3)');
+ expect(attrs.$attr.ngOnMyCamelTitle).toBe('ng-on-my-camel_title');
+ });
+ });
+
+ it('should not conflict with (ng-attr-)attribute mappings of the same name', function() {
+ var attrs;
+ module(function($compileProvider) {
+ $compileProvider.directive('attrExposer', valueFn({
+ link: function($scope, $element, $attrs) {
+ attrs = $attrs;
+ }
+ }));
+ });
+ inject(function($compile, $rootScope) {
+ $compile('
')($rootScope);
+ expect(attrs.title).toBe('foo');
+ expect(attrs.$attr.title).toBe('title');
+ expect(attrs.$attr.ngOnTitle).toBe('ng-on-title');
+ });
+ });
+});
diff --git a/test/ng/ngPropSpec.js b/test/ng/ngPropSpec.js
new file mode 100644
index 000000000000..71bf67a5a5fb
--- /dev/null
+++ b/test/ng/ngPropSpec.js
@@ -0,0 +1,836 @@
+'use strict';
+
+/* eslint-disable no-script-url */
+
+describe('ngProp*', function() {
+ it('should bind boolean properties (input disabled)', inject(function($rootScope, $compile) {
+ var element = $compile('
')($rootScope);
+ $rootScope.$digest();
+ expect(element.prop('disabled')).toBe(false);
+ $rootScope.isDisabled = true;
+ $rootScope.$digest();
+ expect(element.prop('disabled')).toBe(true);
+ $rootScope.isDisabled = false;
+ $rootScope.$digest();
+ expect(element.prop('disabled')).toBe(false);
+ }));
+
+ it('should bind boolean properties (input checked)', inject(function($rootScope, $compile) {
+ var element = $compile('
')($rootScope);
+ expect(element.prop('checked')).toBe(false);
+ $rootScope.isChecked = true;
+ $rootScope.$digest();
+ expect(element.prop('checked')).toBe(true);
+ $rootScope.isChecked = false;
+ $rootScope.$digest();
+ expect(element.prop('checked')).toBe(false);
+ }));
+
+ it('should bind string properties (title)', inject(function($rootScope, $compile) {
+ var element = $compile('
')($rootScope);
+ $rootScope.title = 123;
+ $rootScope.$digest();
+ expect(element.prop('title')).toBe('123');
+ $rootScope.title = 'foobar';
+ $rootScope.$digest();
+ expect(element.prop('title')).toBe('foobar');
+ }));
+
+ it('should bind variable type properties', inject(function($rootScope, $compile) {
+ var element = $compile('
')($rootScope);
+ $rootScope.asdf = 123;
+ $rootScope.$digest();
+ expect(element.prop('asdf')).toBe(123);
+ $rootScope.asdf = 'foobar';
+ $rootScope.$digest();
+ expect(element.prop('asdf')).toBe('foobar');
+ $rootScope.asdf = true;
+ $rootScope.$digest();
+ expect(element.prop('asdf')).toBe(true);
+ }));
+
+ it('should support mixed case using underscore-separated names', inject(function($rootScope, $compile) {
+ var element = $compile('
')($rootScope);
+ $rootScope.value = 123;
+ $rootScope.$digest();
+ expect(element.prop('aBcdE')).toBe(123);
+ }));
+
+ it('should work with different prefixes', inject(function($rootScope, $compile) {
+ $rootScope.name = 'Misko';
+ var element = $compile('
')($rootScope);
+ expect(element.prop('test')).toBe('Misko');
+ expect(element.prop('test2')).toBe('Misko');
+ expect(element.prop('test3')).toBe('Misko');
+ }));
+
+ it('should work with the "href" property', inject(function($rootScope, $compile) {
+ $rootScope.value = 'test';
+ var element = $compile('
')($rootScope);
+ $rootScope.$digest();
+ expect(element.prop('href')).toMatch(/\/test\/test$/);
+ }));
+
+ it('should work if they are prefixed with x- or data- and different prefixes', inject(function($rootScope, $compile) {
+ $rootScope.name = 'Misko';
+ var element = $compile('
')($rootScope);
+ expect(element.prop('test2')).toBe('Misko');
+ expect(element.prop('test3')).toBe('Misko');
+ expect(element.prop('test4')).toBe('Misko');
+ expect(element.prop('test5')).toBe('Misko');
+ expect(element.prop('test6')).toBe('Misko');
+ }));
+
+ it('should work independently of attributes with the same name', inject(function($rootScope, $compile) {
+ var element = $compile('
')($rootScope);
+ $rootScope.asdf = 123;
+ $rootScope.$digest();
+ expect(element.prop('asdf')).toBe(123);
+ expect(element.attr('asdf')).toBe('foo');
+ }));
+
+ it('should work independently of (ng-)attributes with the same name', inject(function($rootScope, $compile) {
+ var element = $compile('
')($rootScope);
+ $rootScope.asdf = 123;
+ $rootScope.$digest();
+ expect(element.prop('asdf')).toBe(123);
+ expect(element.attr('asdf')).toBe('foo');
+ }));
+
+ it('should use the full ng-prop-* attribute name in $attr mappings', function() {
+ var attrs;
+ module(function($compileProvider) {
+ $compileProvider.directive('attrExposer', valueFn({
+ link: function($scope, $element, $attrs) {
+ attrs = $attrs;
+ }
+ }));
+ });
+ inject(function($compile, $rootScope) {
+ $compile('
')($rootScope);
+
+ expect(attrs.title).toBeUndefined();
+ expect(attrs.$attr.title).toBeUndefined();
+ expect(attrs.ngPropTitle).toBe('12');
+ expect(attrs.$attr.ngPropTitle).toBe('ng-prop-title');
+
+ expect(attrs.superTitle).toBeUndefined();
+ expect(attrs.$attr.superTitle).toBeUndefined();
+ expect(attrs.ngPropSuperTitle).toBe('34');
+ expect(attrs.$attr.ngPropSuperTitle).toBe('ng-prop-super-title');
+
+ expect(attrs.myCamelTitle).toBeUndefined();
+ expect(attrs.$attr.myCamelTitle).toBeUndefined();
+ expect(attrs.ngPropMyCamelTitle).toBe('56');
+ expect(attrs.$attr.ngPropMyCamelTitle).toBe('ng-prop-my-camel_title');
+ });
+ });
+
+ it('should not conflict with (ng-attr-)attribute mappings of the same name', function() {
+ var attrs;
+ module(function($compileProvider) {
+ $compileProvider.directive('attrExposer', valueFn({
+ link: function($scope, $element, $attrs) {
+ attrs = $attrs;
+ }
+ }));
+ });
+ inject(function($compile, $rootScope) {
+ $compile('
')($rootScope);
+ expect(attrs.title).toBe('foo');
+ expect(attrs.$attr.title).toBe('title');
+ expect(attrs.$attr.ngPropTitle).toBe('ng-prop-title');
+ });
+ });
+
+ it('should disallow property binding to onclick', inject(function($compile, $rootScope) {
+ // All event prop bindings are disallowed.
+ expect(function() {
+ $compile('