From 4f78fd692c0ec51241476e6be9a4df06cd62fdd6 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 8 Sep 2011 13:56:29 -0700 Subject: feat(forms): new and improved forms --- test/widget/inputSpec.js | 547 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 547 insertions(+) create mode 100644 test/widget/inputSpec.js (limited to 'test/widget/inputSpec.js') diff --git a/test/widget/inputSpec.js b/test/widget/inputSpec.js new file mode 100644 index 00000000..31f8c59c --- /dev/null +++ b/test/widget/inputSpec.js @@ -0,0 +1,547 @@ +'use strict'; + +describe('widget: input', function(){ + var compile = null, element = null, scope = null, defer = null; + var doc = null; + + beforeEach(function() { + scope = null; + element = null; + compile = function(html, parent) { + if (parent) { + parent.html(html); + element = parent.children(); + } else { + element = jqLite(html); + } + scope = angular.compile(element)(); + scope.$apply(); + defer = scope.$service('$browser').defer; + return scope; + }; + }); + + afterEach(function(){ + dealoc(element); + dealoc(doc); + }); + + + describe('text', function(){ + var scope = null, + form = null, + formElement = null, + inputElement = null; + + function createInput(flags){ + var prefix = ''; + forEach(flags, function(value, key){ + prefix += key + '="' + value + '" '; + }); + formElement = doc = angular.element('
'); + inputElement = formElement.find('input'); + scope = angular.compile(doc)(); + form = formElement.inheritedData('$form'); + }; + + + it('should bind update scope from model', function(){ + createInput(); + expect(scope.form.name.$required).toBe(false); + scope.name = 'misko'; + scope.$digest(); + expect(inputElement.val()).toEqual('misko'); + }); + + + it('should require', function(){ + createInput({required:''}); + expect(scope.form.name.$required).toBe(true); + scope.$digest(); + expect(scope.form.name.$valid).toBe(false); + scope.name = 'misko'; + scope.$digest(); + expect(scope.form.name.$valid).toBe(true); + }); + + + it('should call $destroy on element remove', function(){ + createInput(); + var log = ''; + form.$on('$destroy', function(){ + log += 'destroy;'; + }); + inputElement.remove(); + expect(log).toEqual('destroy;'); + }); + + + it('should update the model and trim input', function(){ + createInput(); + var log = ''; + scope.change = function(){ + log += 'change();'; + }; + inputElement.val(' a '); + browserTrigger(inputElement); + scope.$service('$browser').defer.flush(); + expect(scope.name).toEqual('a'); + expect(log).toEqual('change();'); + }); + + + it('should change non-html5 types to text', function(){ + doc = angular.element('
'); + scope = angular.compile(doc)(); + expect(doc.find('input').attr('type')).toEqual('text'); + }); + + + it('should not change html5 types to text', function(){ + doc = angular.element('
'); + scope = angular.compile(doc)(); + expect(doc.find('input')[0].getAttribute('type')).toEqual('number'); + }); + }); + + + describe("input", function(){ + + describe("text", function(){ + it('should input-text auto init and handle keydown/change events', function(){ + compile(''); + + scope.name = 'Adam'; + scope.$digest(); + expect(element.val()).toEqual("Adam"); + + element.val('Shyam'); + browserTrigger(element, 'keydown'); + // keydown event must be deferred + expect(scope.name).toEqual('Adam'); + defer.flush(); + expect(scope.name).toEqual('Shyam'); + + element.val('Kai'); + browserTrigger(element, 'change'); + scope.$service('$browser').defer.flush(); + expect(scope.name).toEqual('Kai'); + }); + + + it('should not trigger eval if value does not change', function(){ + compile(''); + scope.name = 'Misko'; + scope.$digest(); + expect(scope.name).toEqual("Misko"); + expect(scope.count).toEqual(0); + browserTrigger(element, 'keydown'); + scope.$service('$browser').defer.flush(); + expect(scope.name).toEqual("Misko"); + expect(scope.count).toEqual(0); + }); + + + it('should allow complex reference binding', function(){ + compile('
'+ + ''+ + '
'); + scope.obj = { abc: { name: 'Misko'} }; + scope.$digest(); + expect(scope.$element.find('input').val()).toEqual('Misko'); + }); + + + describe("ng:format", function(){ + it("should format text", function(){ + compile(''); + + scope.list = ['x', 'y', 'z']; + scope.$digest(); + expect(element.val()).toEqual("x, y, z"); + + element.val('1, 2, 3'); + browserTrigger(element); + scope.$service('$browser').defer.flush(); + expect(scope.list).toEqual(['1', '2', '3']); + }); + + + it("should render as blank if null", function(){ + compile(''); + expect(scope.age).toBeNull(); + expect(scope.$element[0].value).toEqual(''); + }); + + + it("should show incorrect text while number does not parse", function(){ + compile(''); + scope.age = 123; + scope.$digest(); + expect(scope.$element.val()).toEqual('123'); + try { + // to allow non-number values, we have to change type so that + // the browser which have number validation will not interfere with + // this test. IE8 won't allow it hence the catch. + scope.$element[0].setAttribute('type', 'text'); + } catch (e){} + scope.$element.val('123X'); + browserTrigger(scope.$element, 'change'); + scope.$service('$browser').defer.flush(); + expect(scope.$element.val()).toEqual('123X'); + expect(scope.age).toEqual(123); + expect(scope.$element).toBeInvalid(); + }); + + + it("should not clobber text if model changes due to itself", function(){ + // When the user types 'a,b' the 'a,' stage parses to ['a'] but if the + // $parseModel function runs it will change to 'a', in essence preventing + // the user from ever typying ','. + compile(''); + + scope.$element.val('a '); + browserTrigger(scope.$element, 'change'); + scope.$service('$browser').defer.flush(); + expect(scope.$element.val()).toEqual('a '); + expect(scope.list).toEqual(['a']); + + scope.$element.val('a ,'); + browserTrigger(scope.$element, 'change'); + scope.$service('$browser').defer.flush(); + expect(scope.$element.val()).toEqual('a ,'); + expect(scope.list).toEqual(['a']); + + scope.$element.val('a , '); + browserTrigger(scope.$element, 'change'); + scope.$service('$browser').defer.flush(); + expect(scope.$element.val()).toEqual('a , '); + expect(scope.list).toEqual(['a']); + + scope.$element.val('a , b'); + browserTrigger(scope.$element, 'change'); + scope.$service('$browser').defer.flush(); + expect(scope.$element.val()).toEqual('a , b'); + expect(scope.list).toEqual(['a', 'b']); + }); + + + it("should come up blank when no value specified", function(){ + compile(''); + scope.$digest(); + expect(scope.$element.val()).toEqual(''); + expect(scope.age).toEqual(null); + }); + }); + + + describe("checkbox", function(){ + it("should format booleans", function(){ + compile(''); + expect(scope.name).toBe(false); + expect(scope.$element[0].checked).toBe(false); + }); + + + it('should support type="checkbox" with non-standard capitalization', function(){ + compile(''); + + browserTrigger(element); + expect(scope.checkbox).toBe(true); + + browserTrigger(element); + expect(scope.checkbox).toBe(false); + }); + + + it('should allow custom enumeration', function(){ + compile(''); + + scope.name='ano'; + scope.$digest(); + expect(scope.$element[0].checked).toBe(true); + + scope.name='nie'; + scope.$digest(); + expect(scope.$element[0].checked).toBe(false); + + scope.name='abc'; + scope.$digest(); + expect(scope.$element[0].checked).toBe(false); + + browserTrigger(element); + expect(scope.name).toEqual('ano'); + + browserTrigger(element); + expect(scope.name).toEqual('nie'); + }); + }); + }); + + + it("should process required", function(){ + compile('', jqLite(document.body)); + expect(scope.$service('$formFactory').rootForm.p.$required).toBe(true); + expect(element.hasClass('ng-invalid')).toBeTruthy(); + + scope.price = 'xxx'; + scope.$digest(); + expect(element.hasClass('ng-invalid')).toBeFalsy(); + + element.val(''); + browserTrigger(element); + scope.$service('$browser').defer.flush(); + expect(element.hasClass('ng-invalid')).toBeTruthy(); + }); + + + it('should allow bindings on ng:required', function() { + compile('', + jqLite(document.body)); + scope.price = ''; + scope.required = false; + scope.$digest(); + expect(element).toBeValid(); + + scope.price = 'xxx'; + scope.$digest(); + expect(element).toBeValid(); + + scope.price = ''; + scope.required = true; + scope.$digest(); + expect(element).toBeInvalid(); + + element.val('abc'); + browserTrigger(element); + scope.$service('$browser').defer.flush(); + expect(element).toBeValid(); + }); + + + describe('textarea', function(){ + it("should process textarea", function() { + compile(''); + + scope.name = 'Adam'; + scope.$digest(); + expect(element.val()).toEqual("Adam"); + + element.val('Shyam'); + browserTrigger(element); + defer.flush(); + expect(scope.name).toEqual('Shyam'); + + element.val('Kai'); + browserTrigger(element); + defer.flush(); + expect(scope.name).toEqual('Kai'); + }); + }); + + + describe('radio', function(){ + it('should support type="radio"', function(){ + compile('
' + + '' + + '' + + '' + + '
'); + var a = element[0].childNodes[0]; + var b = element[0].childNodes[1]; + expect(b.name.split('@')[1]).toEqual('r'); + scope.chose = 'A'; + scope.$digest(); + expect(a.checked).toBe(true); + + scope.chose = 'B'; + scope.$digest(); + expect(a.checked).toBe(false); + expect(b.checked).toBe(true); + expect(scope.clicked).not.toBeDefined(); + + browserTrigger(a); + expect(scope.chose).toEqual('A'); + }); + + + it('should honor model over html checked keyword after', function(){ + compile('
' + + '' + + '' + + '' + + '
'); + + expect(scope.choose).toEqual('C'); + var inputs = scope.$element.find('input'); + expect(inputs[1].checked).toBe(false); + expect(inputs[2].checked).toBe(true); + }); + + + it('should honor model over html checked keyword before', function(){ + compile('
' + + '' + + '' + + '' + + '
'); + + expect(scope.choose).toEqual('A'); + var inputs = scope.$element.find('input'); + expect(inputs[0].checked).toBe(true); + expect(inputs[1].checked).toBe(false); + }); + }); + + + it('should ignore text widget which have no name', function(){ + compile(''); + expect(scope.$element.attr('ng-exception')).toBeFalsy(); + expect(scope.$element.hasClass('ng-exception')).toBeFalsy(); + }); + + + it('should ignore checkbox widget which have no name', function(){ + compile(''); + expect(scope.$element.attr('ng-exception')).toBeFalsy(); + expect(scope.$element.hasClass('ng-exception')).toBeFalsy(); + }); + + + it('should report error on assignment error', function(){ + expect(function(){ + compile(''); + }).toThrow("Syntax Error: Token '''' is an unexpected token at column 7 of the expression [throw ''] starting at ['']."); + $logMock.error.logs.shift(); + }); + }); + + + describe('scope declaration', function(){ + it('should read the declaration from scope', function(){ + var input, $formFactory; + element = angular.element(''); + scope = angular.scope(); + scope.MyType = function($f, i) { + input = i; + $formFactory = $f; + }; + scope.MyType.$inject = ['$formFactory']; + + angular.compile(element)(scope); + + expect($formFactory).toBe(scope.$service('$formFactory')); + expect(input[0]).toBe(element[0]); + }); + + it('should throw an error of Cntoroller not declared in scope', function() { + var input, $formFactory; + element = angular.element(''); + var error; + try { + scope = angular.scope(); + angular.compile(element)(scope); + error = 'no error thrown'; + } catch (e) { + error = e; + } + expect(error.message).toEqual("Argument 'DontExist' is not a function, got undefined"); + }); + }); + + + describe('text subtypes', function(){ + + function itShouldVerify(type, validList, invalidList, params, fn) { + describe(type, function(){ + forEach(validList, function(value){ + it('should validate "' + value + '"', function(){ + setup(value); + expect(scope.$element).toBeValid(); + }); + }); + forEach(invalidList, function(value){ + it('should NOT validate "' + value + '"', function(){ + setup(value); + expect(scope.$element).toBeInvalid(); + }); + }); + + function setup(value){ + var html = [''); + compile(html.join('')); + (fn||noop)(scope); + scope.value = null; + try { + // to allow non-number values, we have to change type so that + // the browser which have number validation will not interfere with + // this test. IE8 won't allow it hence the catch. + scope.$element[0].setAttribute('type', 'text'); + } catch (e){} + if (value != undefined) { + scope.$element.val(value); + browserTrigger(element, 'keydown'); + scope.$service('$browser').defer.flush(); + } + scope.$digest(); + } + }); + } + + + itShouldVerify('email', ['a@b.com'], ['a@B.c']); + + + itShouldVerify('url', ['http://server:123/path'], ['a@b.c']); + + + itShouldVerify('number', + ['', '1', '12.34', '-4', '+13', '.1'], + ['x', '12b', '-6', '101'], + {min:-5, max:100}); + + + itShouldVerify('integer', + [null, '', '1', '12', '-4', '+13'], + ['x', '12b', '-6', '101', '1.', '1.2'], + {min:-5, max:100}); + + + itShouldVerify('integer', + [null, '', '0', '1'], + ['-1', '2'], + {min:0, max:1}); + + + itShouldVerify('text with inlined pattern contraint', + ['', '000-00-0000', '123-45-6789'], + ['x000-00-0000x', 'x'], + {'ng:pattern':'/^\\d\\d\\d-\\d\\d-\\d\\d\\d\\d$/'}); + + + itShouldVerify('text with pattern constraint on scope', + ['', '000-00-0000', '123-45-6789'], + ['x000-00-0000x', 'x'], + {'ng:pattern':'regexp'}, function(scope){ + scope.regexp = /^\d\d\d-\d\d-\d\d\d\d$/; + }); + + + it('should throw an error when scope pattern can\'t be found', function() { + var el = jqLite(''), + scope = angular.compile(el)(); + + el.val('xx'); + browserTrigger(el, 'keydown'); + expect(function() { scope.$service('$browser').defer.flush(); }). + toThrow('Expected fooRegexp to be a RegExp but was undefined'); + + dealoc(el); + }); + }); +}); -- cgit v1.2.3