diff options
| author | Misko Hevery | 2011-09-08 13:56:29 -0700 | 
|---|---|---|
| committer | Igor Minar | 2011-10-11 11:01:45 -0700 | 
| commit | 4f78fd692c0ec51241476e6be9a4df06cd62fdd6 (patch) | |
| tree | 91f70bb89b9c095126fbc093f51cedbac5cb0c78 /test/widget | |
| parent | df6d2ba3266de405ad6c2f270f24569355706e76 (diff) | |
| download | angular.js-4f78fd692c0ec51241476e6be9a4df06cd62fdd6.tar.bz2 | |
feat(forms): new and improved forms
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]]); +      }); +    }); + +  }); + +}); | 
