diff options
Diffstat (limited to 'test/widget')
| -rw-r--r-- | test/widget/formSpec.js | 97 | ||||
| -rw-r--r-- | test/widget/inputSpec.js | 547 | ||||
| -rw-r--r-- | test/widget/selectSpec.js | 510 |
3 files changed, 1154 insertions, 0 deletions
diff --git a/test/widget/formSpec.js b/test/widget/formSpec.js new file mode 100644 index 00000000..7c575c33 --- /dev/null +++ b/test/widget/formSpec.js @@ -0,0 +1,97 @@ +'use strict'; + +describe('form', function(){ + var doc; + + afterEach(function(){ + dealoc(doc); + }); + + + it('should attach form to DOM', function(){ + doc = angular.element('<form>'); + var scope = angular.compile(doc)(); + expect(doc.data('$form')).toBeTruthy(); + }); + + + it('should prevent form submission', function(){ + var startingUrl = '' + window.location; + doc = angular.element('<form name="myForm"><input type=submit val=submit>'); + var scope = angular.compile(doc)(); + browserTrigger(doc.find('input')); + waitsFor( + function(){ return true; }, + 'let browser breath, so that the form submision can manifest itself', 10); + runs(function(){ + expect('' + window.location).toEqual(startingUrl); + }); + }); + + + it('should publish form to scope', function(){ + doc = angular.element('<form name="myForm">'); + var scope = angular.compile(doc)(); + expect(scope.myForm).toBeTruthy(); + expect(doc.data('$form')).toBeTruthy(); + expect(doc.data('$form')).toEqual(scope.myForm); + }); + + + it('should have ng-valide/ng-invalid style', function(){ + doc = angular.element('<form name="myForm"><input type=text ng:model=text required>'); + var scope = angular.compile(doc)(); + scope.text = 'misko'; + scope.$digest(); + + expect(doc.hasClass('ng-valid')).toBe(true); + expect(doc.hasClass('ng-invalid')).toBe(false); + + scope.text = ''; + scope.$digest(); + expect(doc.hasClass('ng-valid')).toBe(false); + expect(doc.hasClass('ng-invalid')).toBe(true); + }); + + + it('should chain nested forms', function(){ + doc = angular.element('<ng:form name=parent><ng:form name=child><input type=text ng:model=text name=text>'); + var scope = angular.compile(doc)(); + var parent = scope.parent; + var child = scope.child; + var input = child.text; + + input.$emit('$invalid', 'MyError'); + expect(parent.$error.MyError).toEqual([input]); + expect(child.$error.MyError).toEqual([input]); + + input.$emit('$valid', 'MyError'); + expect(parent.$error.MyError).toBeUndefined(); + expect(child.$error.MyError).toBeUndefined(); + }); + + + it('should chain nested forms in repeater', function(){ + doc = angular.element('<ng:form name=parent>' + + '<ng:form ng:repeat="f in forms" name=child><input type=text ng:model=text name=text>'); + var scope = angular.compile(doc)(); + scope.forms = [1]; + scope.$digest(); + + var parent = scope.parent; + var child = doc.find('input').scope().child; + var input = child.text; + expect(parent).toBeDefined(); + expect(child).toBeDefined(); + expect(input).toBeDefined(); + + input.$emit('$invalid', 'myRule'); + expect(input.$error.myRule).toEqual(true); + expect(child.$error.myRule).toEqual([input]); + expect(parent.$error.myRule).toEqual([input]); + + input.$emit('$valid', 'myRule'); + expect(parent.$error.myRule).toBeUndefined(); + expect(child.$error.myRule).toBeUndefined(); + }); +}); 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('<form name="form"><input ' + prefix + + 'type="text" ng:model="name" name="name" ng:change="change()"></form>'); + 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('<form name="form"><input type="abc" ng:model="name"></form>'); + scope = angular.compile(doc)(); + expect(doc.find('input').attr('type')).toEqual('text'); + }); + + + it('should not change html5 types to text', function(){ + doc = angular.element('<form name="form"><input type="number" ng:model="name"></form>'); + 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('<input type="text" ng:model="name"/>'); + + 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('<input type="text" ng:model="name" ng:change="count = count + 1" ng:init="count=0"/>'); + 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('<div>'+ + '<input type="text" ng:model="obj[\'abc\'].name"/>'+ + '</div>'); + 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('<input type="list" ng:model="list"/>'); + + 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('<input type="text" ng:model="age" ng:format="number" ng:init="age=null"/>'); + expect(scope.age).toBeNull(); + expect(scope.$element[0].value).toEqual(''); + }); + + + it("should show incorrect text while number does not parse", function(){ + compile('<input type="number" ng:model="age"/>'); + 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('<input type="list" ng:model="list"/>'); + + 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('<input type="number" ng:model="age"/>'); + scope.$digest(); + expect(scope.$element.val()).toEqual(''); + expect(scope.age).toEqual(null); + }); + }); + + + describe("checkbox", function(){ + it("should format booleans", function(){ + compile('<input type="checkbox" ng:model="name" ng:init="name=false"/>'); + expect(scope.name).toBe(false); + expect(scope.$element[0].checked).toBe(false); + }); + + + it('should support type="checkbox" with non-standard capitalization', function(){ + compile('<input type="checkBox" ng:model="checkbox"/>'); + + browserTrigger(element); + expect(scope.checkbox).toBe(true); + + browserTrigger(element); + expect(scope.checkbox).toBe(false); + }); + + + it('should allow custom enumeration', function(){ + compile('<input type="checkbox" ng:model="name" true-value="ano" false-value="nie"/>'); + + 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('<input type="text" ng:model="price" name="p" required/>', 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('<input type="text" ng:model="price" ng:required="{{required}}"/>', + 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('<textarea ng:model="name"></textarea>'); + + 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('<div>' + + '<input type="radio" name="r" ng:model="chose" value="A"/>' + + '<input type="radio" name="r" ng:model="chose" value="B"/>' + + '<input type="radio" name="r" ng:model="chose" value="C"/>' + + '</div>'); + 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('<div ng:init="choose=\'C\'">' + + '<input type="radio" ng:model="choose" value="A""/>' + + '<input type="radio" ng:model="choose" value="B" checked/>' + + '<input type="radio" ng:model="choose" value="C"/>' + + '</div>'); + + 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('<div ng:init="choose=\'A\'">' + + '<input type="radio" ng:model="choose" value="A""/>' + + '<input type="radio" ng:model="choose" value="B" checked/>' + + '<input type="radio" ng:model="choose" value="C"/>' + + '</div>'); + + 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('<input type="text"/>'); + 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('<input type="checkbox"/>'); + 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('<input type="text" ng:model="throw \'\'">'); + }).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('<input type="@MyType" ng:model="abc">'); + 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('<input type="@DontExist" ng:model="abc">'); + 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 = ['<input type="', type.split(' ')[0], '" ']; + forEach(params||{}, function(value, key){ + html.push(key + '="' + value + '" '); + }); + html.push('ng:model="value">'); + 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('<input ng:model="foo" ng:pattern="fooRegexp">'), + 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); + }); + }); +}); diff --git a/test/widget/selectSpec.js b/test/widget/selectSpec.js new file mode 100644 index 00000000..6adf8b93 --- /dev/null +++ b/test/widget/selectSpec.js @@ -0,0 +1,510 @@ +'use strict'; + +describe('select', function(){ + var compile = null, element = null, scope = null, $formFactory = 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(); + $formFactory = scope.$service('$formFactory'); + return scope; + }; + }); + + afterEach(function(){ + dealoc(element); + }); + + + describe('select-one', function(){ + + it('should compile children of a select without a name, but not create a model for it', + function() { + compile('<select>' + + '<option selected="true">{{a}}</option>' + + '<option value="">{{b}}</option>' + + '<option>C</option>' + + '</select>'); + scope.a = 'foo'; + scope.b = 'bar'; + scope.$digest(); + + expect(scope.$element.text()).toBe('foobarC'); + }); + + it('should require', function(){ + compile('<select name="select" ng:model="selection" required ng:change="log=log+\'change;\'">' + + '<option value=""></option>' + + '<option value="c">C</option>' + + '</select>'); + scope.log = ''; + scope.selection = 'c'; + scope.$digest(); + expect($formFactory.forElement(element).select.$error.REQUIRED).toEqual(undefined); + expect(element).toBeValid(); + expect(element).toBePristine(); + + scope.selection = ''; + scope.$digest(); + expect($formFactory.forElement(element).select.$error.REQUIRED).toEqual(true); + expect(element).toBeInvalid(); + expect(element).toBePristine(); + expect(scope.log).toEqual(''); + + element[0].value = 'c'; + browserTrigger(element, 'change'); + expect(element).toBeValid(); + expect(element).toBeDirty(); + expect(scope.log).toEqual('change;'); + }); + + it('should not be invalid if no require', function(){ + compile('<select name="select" ng:model="selection">' + + '<option value=""></option>' + + '<option value="c">C</option>' + + '</select>'); + + expect(element).toBeValid(); + expect(element).toBePristine(); + }); + + }); + + + describe('select-multiple', function(){ + it('should support type="select-multiple"', function(){ + compile('<select ng:model="selection" multiple>' + + '<option>A</option>' + + '<option>B</option>' + + '</select>'); + scope.selection = ['A']; + scope.$digest(); + expect(element[0].childNodes[0].selected).toEqual(true); + }); + + it('should require', function(){ + compile('<select name="select" ng:model="selection" multiple required>' + + '<option>A</option>' + + '<option>B</option>' + + '</select>'); + + scope.selection = []; + scope.$digest(); + expect($formFactory.forElement(element).select.$error.REQUIRED).toEqual(true); + expect(element).toBeInvalid(); + expect(element).toBePristine(); + + scope.selection = ['A']; + scope.$digest(); + expect(element).toBeValid(); + expect(element).toBePristine(); + + element[0].value = 'B'; + browserTrigger(element, 'change'); + expect(element).toBeValid(); + expect(element).toBeDirty(); + }); + + }); + + + describe('ng:options', function(){ + var select, scope; + + function createSelect(attrs, blank, unknown){ + var html = '<select'; + forEach(attrs, function(value, key){ + if (isBoolean(value)) { + if (value) html += ' ' + key; + } else { + html += ' ' + key + '="' + value + '"'; + } + }); + html += '>' + + (blank ? '<option value="">blank</option>' : '') + + (unknown ? '<option value="?">unknown</option>' : '') + + '</select>'; + select = jqLite(html); + scope = compile(select); + } + + function createSingleSelect(blank, unknown){ + createSelect({ + 'ng:model':'selected', + 'ng:options':'value.name for value in values' + }, blank, unknown); + } + + function createMultiSelect(blank, unknown){ + createSelect({ + 'ng:model':'selected', + 'multiple':true, + 'ng:options':'value.name for value in values' + }, blank, unknown); + } + + afterEach(function(){ + dealoc(select); + dealoc(scope); + }); + + it('should throw when not formated "? for ? in ?"', function(){ + expect(function(){ + compile('<select ng:model="selected" ng:options="i dont parse"></select>'); + }).toThrow("Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in" + + " _collection_' but got 'i dont parse'."); + }); + + it('should render a list', function(){ + createSingleSelect(); + scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; + scope.selected = scope.values[0]; + scope.$digest(); + var options = select.find('option'); + expect(options.length).toEqual(3); + expect(sortedHtml(options[0])).toEqual('<option value="0">A</option>'); + expect(sortedHtml(options[1])).toEqual('<option value="1">B</option>'); + expect(sortedHtml(options[2])).toEqual('<option value="2">C</option>'); + }); + + it('should render an object', function(){ + createSelect({ + 'ng:model':'selected', + 'ng:options': 'value as key for (key, value) in object' + }); + scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'}; + scope.selected = scope.object.red; + scope.$digest(); + var options = select.find('option'); + expect(options.length).toEqual(3); + expect(sortedHtml(options[0])).toEqual('<option value="blue">blue</option>'); + expect(sortedHtml(options[1])).toEqual('<option value="green">green</option>'); + expect(sortedHtml(options[2])).toEqual('<option value="red">red</option>'); + expect(options[2].selected).toEqual(true); + + scope.object.azur = '8888FF'; + scope.$digest(); + options = select.find('option'); + expect(options[3].selected).toEqual(true); + }); + + it('should grow list', function(){ + createSingleSelect(); + scope.values = []; + scope.$digest(); + expect(select.find('option').length).toEqual(1); // because we add special empty option + expect(sortedHtml(select.find('option')[0])).toEqual('<option value="?"></option>'); + + scope.values.push({name:'A'}); + scope.selected = scope.values[0]; + scope.$digest(); + expect(select.find('option').length).toEqual(1); + expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); + + scope.values.push({name:'B'}); + scope.$digest(); + expect(select.find('option').length).toEqual(2); + expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); + expect(sortedHtml(select.find('option')[1])).toEqual('<option value="1">B</option>'); + }); + + it('should shrink list', function(){ + createSingleSelect(); + scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; + scope.selected = scope.values[0]; + scope.$digest(); + expect(select.find('option').length).toEqual(3); + + scope.values.pop(); + scope.$digest(); + expect(select.find('option').length).toEqual(2); + expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); + expect(sortedHtml(select.find('option')[1])).toEqual('<option value="1">B</option>'); + + scope.values.pop(); + scope.$digest(); + expect(select.find('option').length).toEqual(1); + expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); + + scope.values.pop(); + scope.selected = null; + scope.$digest(); + expect(select.find('option').length).toEqual(1); // we add back the special empty option + }); + + it('should shrink and then grow list', function(){ + createSingleSelect(); + scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; + scope.selected = scope.values[0]; + scope.$digest(); + expect(select.find('option').length).toEqual(3); + + scope.values = [{name:'1'}, {name:'2'}]; + scope.selected = scope.values[0]; + scope.$digest(); + expect(select.find('option').length).toEqual(2); + + scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; + scope.selected = scope.values[0]; + scope.$digest(); + expect(select.find('option').length).toEqual(3); + }); + + it('should update list', function(){ + createSingleSelect(); + scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; + scope.selected = scope.values[0]; + scope.$digest(); + + scope.values = [{name:'B'}, {name:'C'}, {name:'D'}]; + scope.selected = scope.values[0]; + scope.$digest(); + var options = select.find('option'); + expect(options.length).toEqual(3); + expect(sortedHtml(options[0])).toEqual('<option value="0">B</option>'); + expect(sortedHtml(options[1])).toEqual('<option value="1">C</option>'); + expect(sortedHtml(options[2])).toEqual('<option value="2">D</option>'); + }); + + it('should preserve existing options', function(){ + createSingleSelect(true); + + scope.values = []; + scope.$digest(); + expect(select.find('option').length).toEqual(1); + + scope.values = [{name:'A'}]; + scope.selected = scope.values[0]; + scope.$digest(); + expect(select.find('option').length).toEqual(2); + expect(jqLite(select.find('option')[0]).text()).toEqual('blank'); + expect(jqLite(select.find('option')[1]).text()).toEqual('A'); + + scope.values = []; + scope.selected = null; + scope.$digest(); + expect(select.find('option').length).toEqual(1); + expect(jqLite(select.find('option')[0]).text()).toEqual('blank'); + }); + + describe('binding', function(){ + it('should bind to scope value', function(){ + createSingleSelect(); + scope.values = [{name:'A'}, {name:'B'}]; + scope.selected = scope.values[0]; + scope.$digest(); + expect(select.val()).toEqual('0'); + + scope.selected = scope.values[1]; + scope.$digest(); + expect(select.val()).toEqual('1'); + }); + + it('should bind to scope value and group', function(){ + createSelect({ + 'ng:model':'selected', + 'ng:options':'item.name group by item.group for item in values' + }); + scope.values = [{name:'A'}, + {name:'B', group:'first'}, + {name:'C', group:'second'}, + {name:'D', group:'first'}, + {name:'E', group:'second'}]; + scope.selected = scope.values[3]; + scope.$digest(); + expect(select.val()).toEqual('3'); + + var first = jqLite(select.find('optgroup')[0]); + var b = jqLite(first.find('option')[0]); + var d = jqLite(first.find('option')[1]); + expect(first.attr('label')).toEqual('first'); + expect(b.text()).toEqual('B'); + expect(d.text()).toEqual('D'); + + var second = jqLite(select.find('optgroup')[1]); + var c = jqLite(second.find('option')[0]); + var e = jqLite(second.find('option')[1]); + expect(second.attr('label')).toEqual('second'); + expect(c.text()).toEqual('C'); + expect(e.text()).toEqual('E'); + + scope.selected = scope.values[0]; + scope.$digest(); + expect(select.val()).toEqual('0'); + }); + + it('should bind to scope value through experession', function(){ + createSelect({'ng:model':'selected', 'ng:options':'item.id as item.name for item in values'}); + scope.values = [{id:10, name:'A'}, {id:20, name:'B'}]; + scope.selected = scope.values[0].id; + scope.$digest(); + expect(select.val()).toEqual('0'); + + scope.selected = scope.values[1].id; + scope.$digest(); + expect(select.val()).toEqual('1'); + }); + + it('should bind to object key', function(){ + createSelect({ + 'ng:model':'selected', + 'ng:options':'key as value for (key, value) in object' + }); + scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'}; + scope.selected = 'green'; + scope.$digest(); + expect(select.val()).toEqual('green'); + + scope.selected = 'blue'; + scope.$digest(); + expect(select.val()).toEqual('blue'); + }); + + it('should bind to object value', function(){ + createSelect({ + 'ng:model':'selected', + 'ng:options':'value as key for (key, value) in object' + }); + scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'}; + scope.selected = '00FF00'; + scope.$digest(); + expect(select.val()).toEqual('green'); + + scope.selected = '0000FF'; + scope.$digest(); + expect(select.val()).toEqual('blue'); + }); + + it('should insert a blank option if bound to null', function(){ + createSingleSelect(); + scope.values = [{name:'A'}]; + scope.selected = null; + scope.$digest(); + expect(select.find('option').length).toEqual(2); + expect(select.val()).toEqual(''); + expect(jqLite(select.find('option')[0]).val()).toEqual(''); + + scope.selected = scope.values[0]; + scope.$digest(); + expect(select.val()).toEqual('0'); + expect(select.find('option').length).toEqual(1); + }); + + it('should reuse blank option if bound to null', function(){ + createSingleSelect(true); + scope.values = [{name:'A'}]; + scope.selected = null; + scope.$digest(); + expect(select.find('option').length).toEqual(2); + expect(select.val()).toEqual(''); + expect(jqLite(select.find('option')[0]).val()).toEqual(''); + + scope.selected = scope.values[0]; + scope.$digest(); + expect(select.val()).toEqual('0'); + expect(select.find('option').length).toEqual(2); + }); + + it('should insert a unknown option if bound to something not in the list', function(){ + createSingleSelect(); + scope.values = [{name:'A'}]; + scope.selected = {}; + scope.$digest(); + expect(select.find('option').length).toEqual(2); + expect(select.val()).toEqual('?'); + expect(jqLite(select.find('option')[0]).val()).toEqual('?'); + + scope.selected = scope.values[0]; + scope.$digest(); + expect(select.val()).toEqual('0'); + expect(select.find('option').length).toEqual(1); + }); + }); + + describe('on change', function(){ + it('should update model on change', function(){ + createSingleSelect(); + scope.values = [{name:'A'}, {name:'B'}]; + scope.selected = scope.values[0]; + scope.$digest(); + expect(select.val()).toEqual('0'); + + select.val('1'); + browserTrigger(select, 'change'); + expect(scope.selected).toEqual(scope.values[1]); + }); + + it('should update model on change through expression', function(){ + createSelect({'ng:model':'selected', 'ng:options':'item.id as item.name for item in values'}); + scope.values = [{id:10, name:'A'}, {id:20, name:'B'}]; + scope.selected = scope.values[0].id; + scope.$digest(); + expect(select.val()).toEqual('0'); + + select.val('1'); + browserTrigger(select, 'change'); + expect(scope.selected).toEqual(scope.values[1].id); + }); + + it('should update model to null on change', function(){ + createSingleSelect(true); + scope.values = [{name:'A'}, {name:'B'}]; + scope.selected = scope.values[0]; + select.val('0'); + scope.$digest(); + + select.val(''); + browserTrigger(select, 'change'); + expect(scope.selected).toEqual(null); + }); + }); + + describe('select-many', function(){ + it('should read multiple selection', function(){ + createMultiSelect(); + scope.values = [{name:'A'}, {name:'B'}]; + + scope.selected = []; + scope.$digest(); + expect(select.find('option').length).toEqual(2); + expect(jqLite(select.find('option')[0]).attr('selected')).toBeFalsy(); + expect(jqLite(select.find('option')[1]).attr('selected')).toBeFalsy(); + + scope.selected.push(scope.values[1]); + scope.$digest(); + expect(select.find('option').length).toEqual(2); + expect(select.find('option')[0].selected).toEqual(false); + expect(select.find('option')[1].selected).toEqual(true); + + scope.selected.push(scope.values[0]); + scope.$digest(); + expect(select.find('option').length).toEqual(2); + expect(select.find('option')[0].selected).toEqual(true); + expect(select.find('option')[1].selected).toEqual(true); + }); + + it('should update model on change', function(){ + createMultiSelect(); + scope.values = [{name:'A'}, {name:'B'}]; + + scope.selected = []; + scope.$digest(); + select.find('option')[0].selected = true; + + browserTrigger(select, 'change'); + expect(scope.selected).toEqual([scope.values[0]]); + }); + }); + + }); + +}); |
