/* global FormController: false */ 'use strict'; describe('form', function() { var doc, control, scope, $compile, changeInputValue; beforeEach(module(function($compileProvider) { $compileProvider.directive('storeModelCtrl', function() { return { require: 'ngModel', link: function(scope, elm, attr, ctrl) { control = ctrl; } }; }); })); beforeEach(inject(function($injector, $sniffer) { $compile = $injector.get('$compile'); scope = $injector.get('$rootScope'); changeInputValue = function(elm, value) { elm.val(value); browserTrigger(elm, $sniffer.hasEvent('input') ? 'input' : 'change'); }; })); afterEach(function() { dealoc(doc); }); it('should instantiate form and attach it to DOM', function() { doc = $compile('
')(scope); expect(doc.data('$formController')).toBeTruthy(); expect(doc.data('$formController') instanceof FormController).toBe(true); }); it('should remove form control references from the form when nested control is removed from the DOM', function() { doc = $compile( '' + '' + '
')(scope); scope.inputPresent = true; scope.$digest(); var form = scope.myForm; control.$setValidity('required', false); expect(form.alias).toBe(control); expect(form.$error.required).toEqual([control]); // remove nested control scope.inputPresent = false; scope.$apply(); expect(form.$error.required).toBeFalsy(); expect(form.alias).toBeUndefined(); }); it('should ignore changes in manually removed controls', function() { doc = $compile( '
' + '' + '
')(scope); var form = scope.myForm; var input = doc.find('input').eq(0); var inputController = input.controller('ngModel'); changeInputValue(input, 'ab'); scope.$apply(); expect(form.$error.maxlength).toBeTruthy(); expect(form.$dirty).toBe(true); expect(form.$error.maxlength[0].$name).toBe('control'); // remove control form.$removeControl(form.control); expect(form.control).toBeUndefined(); expect(form.$error.maxlength).toBeFalsy(); inputController.$setPristine(); expect(form.$dirty).toBe(true); form.$setPristine(); changeInputValue(input, 'abc'); scope.$apply(); expect(form.$error.maxlength).toBeFalsy(); expect(form.$dirty).toBe(false); }); it('should react to validation changes in manually added controls', function() { doc = $compile( '
' + '' + '
')(scope); scope.$digest(); var form = scope.myForm; var input = doc.find('input').eq(0); // remove control and invalidate it form.$removeControl(control); expect(form.control).toBeUndefined(); changeInputValue(input, 'abc'); expect(control.$error.maxlength).toBe(true); expect(control.$dirty).toBe(true); expect(form.$error.maxlength).toBeFalsy(); expect(form.$dirty).toBe(false); // re-add the control; its current validation state is not propagated form.$addControl(control); expect(form.control).toBe(control); expect(form.$error.maxlength).toBeFalsy(); expect(form.$dirty).toBe(false); // Only when the input changes again its validation state is propagated changeInputValue(input, 'abcd'); expect(form.$error.maxlength[0]).toBe(control); expect(form.$dirty).toBe(false); }); it('should use the correct parent when renaming and removing dynamically added controls', function() { scope.controlName = 'childControl'; scope.hasChildControl = true; doc = $compile( '
' + '
' + '' + '
' + '
' + '
')(scope); scope.$digest(); var form = scope.myForm; var otherForm = scope.otherForm; var childControl = form.childControl; // remove child form and add it to another form form.$removeControl(childControl); otherForm.$addControl(childControl); expect(form.childControl).toBeUndefined(); expect(otherForm.childControl).toBe(childControl); // rename the childControl scope.controlName = 'childControlMoved'; scope.$digest(); expect(form.childControlMoved).toBeUndefined(); expect(otherForm.childControl).toBeUndefined(); expect(otherForm.childControlMoved).toBe(childControl); scope.hasChildControl = false; scope.$digest(); expect(form.childControlMoved).toBeUndefined(); expect(otherForm.childControlMoved).toBeUndefined(); }); it('should remove scope reference when form with no parent form is removed from the DOM', function() { var formController; scope.ctrl = {}; doc = $compile( '
' + '' + '
')(scope); scope.$digest(); expect(scope.ctrl.myForm).toBeUndefined(); scope.$apply('formPresent = true'); expect(scope.ctrl.myForm).toBeDefined(); formController = doc.find('form').controller('form'); expect(scope.ctrl.myForm).toBe(formController); scope.$apply('formPresent = false'); expect(scope.ctrl.myForm).toBeUndefined(); }); it('should use ngForm value as form name', function() { doc = $compile( '
' + '' + '
')(scope); expect(scope.myForm).toBeDefined(); expect(scope.myForm.alias).toBeDefined(); }); it('should use ngForm value as form name when nested inside form', function() { doc = $compile( '
' + '
' + '
')(scope); expect(scope.myForm).toBeDefined(); expect(scope.myForm.nestedForm).toBeDefined(); expect(scope.myForm.nestedForm.alias).toBeDefined(); }); it('should publish form to scope when name attr is defined', function() { doc = $compile('
')(scope); expect(scope.myForm).toBeTruthy(); expect(doc.data('$formController')).toBeTruthy(); expect(doc.data('$formController')).toEqual(scope.myForm); }); it('should support expression in form name', function() { doc = $compile('
')(scope); expect(scope.obj).toBeDefined(); expect(scope.obj.myForm).toBeTruthy(); }); it('should support two forms on a single scope', function() { doc = $compile( '
' + '
' + '' + '
' + '
' + '' + '
' + '
' )(scope); scope.$apply(); expect(scope.formA.$error.required.length).toBe(1); expect(scope.formA.$error.required).toEqual([scope.formA.firstName]); expect(scope.formB.$error.required.length).toBe(1); expect(scope.formB.$error.required).toEqual([scope.formB.lastName]); var inputA = doc.find('input').eq(0), inputB = doc.find('input').eq(1); changeInputValue(inputA, 'val1'); changeInputValue(inputB, 'val2'); expect(scope.firstName).toBe('val1'); expect(scope.lastName).toBe('val2'); expect(scope.formA.$error.required).toBeFalsy(); expect(scope.formB.$error.required).toBeFalsy(); }); it('should publish widgets', function() { doc = jqLite('
'); $compile(doc)(scope); var widget = scope.form.w1; expect(widget).toBeDefined(); expect(widget.$pristine).toBe(true); expect(widget.$dirty).toBe(false); expect(widget.$valid).toBe(true); expect(widget.$invalid).toBe(false); }); it('should throw an exception if an input has name="hasOwnProperty"', function() { doc = jqLite( '
' + '' + '' + '
'); expect(function() { $compile(doc)(scope); }).toThrowMinErr('ng', 'badname'); }); describe('triggering commit value on submit', function() { it('should trigger update on form submit', function() { var form = $compile( '
' + '' + '
')(scope); scope.$digest(); var inputElm = form.find('input').eq(0); changeInputValue(inputElm, 'a'); expect(scope.name).toEqual(undefined); browserTrigger(form, 'submit'); expect(scope.name).toEqual('a'); dealoc(form); }); it('should trigger update on form submit with nested forms', function() { var form = $compile( '
' + '
' + '' + '
' + '
')(scope); scope.$digest(); var inputElm = form.find('input').eq(0); changeInputValue(inputElm, 'a'); expect(scope.name).toEqual(undefined); browserTrigger(form, 'submit'); expect(scope.name).toEqual('a'); dealoc(form); }); it('should trigger update before ng-submit is invoked', function() { var form = $compile( '
' + '' + '
')(scope); scope.$digest(); var inputElm = form.find('input').eq(0); changeInputValue(inputElm, 'a'); scope.submit = jasmine.createSpy('submit').and.callFake(function() { expect(scope.name).toEqual('a'); }); browserTrigger(form, 'submit'); expect(scope.submit).toHaveBeenCalled(); dealoc(form); }); }); describe('rollback view value', function() { it('should trigger rollback on form controls', function() { var form = $compile( '
' + '' + '
' + '' + ''); // Support: Chrome 60+ (on Windows) // We need to add the form to the DOM in order for `submit` events to be properly fired. window.document.body.appendChild(doc[0]); var form = doc.find('form'), destroyed = false, nextTurn = false, submitted = false, reloadPrevented = 'never called'; scope.destroy = function() { // yes, I know, scope methods should not do direct DOM manipulation, but I wanted to keep // this test small. Imagine that the destroy action will cause a model change (e.g. // $location change) that will cause some directive to destroy the dom (e.g. ngView+$route) doc.empty(); destroyed = true; }; scope.submitMe = function() { submitted = true; }; var assertPreventDefaultListener = function(e) { reloadPrevented = e.defaultPrevented || (e.returnValue === false); }; $compile(doc)(scope); form[0].addEventListener('submit', assertPreventDefaultListener); browserTrigger(doc.find('button'), 'click'); // let the browser process all events (and potentially reload the page) window.setTimeout(function() { nextTurn = true;}, 100); var job = createAsync(done); job.waitsFor(function() { return nextTurn; }) .runs(function() { expect(doc.html()).toBe(''); expect(destroyed).toBe(true); expect(submitted).toBe(false); // this is known corner-case that is not currently handled // the issue is that the submit listener is destroyed before // the event propagates there. we can fix this if we see // the issue in the wild, I'm not going to bother to do it // now. (i) // Support: Chrome 60+ (on Windows) // Chrome 60+ on Windows does not fire `submit` events when the form is not attached to // the DOM. Verify that the `submit` listener was either never fired or (if fired) the // reload was prevented. expect(reloadPrevented).not.toBe(false); // prevent mem leak in test form[0].removeEventListener('submit', assertPreventDefaultListener); }) .done(); job.start(); }); }); it('should NOT prevent form submission if action attribute present', function() { var callback = jasmine.createSpy('submit').and.callFake(function(event) { expect(event.isDefaultPrevented()).toBe(false); event.preventDefault(); }); doc = $compile('
')(scope); doc.on('submit', callback); browserTrigger(doc, 'submit'); expect(callback).toHaveBeenCalledOnce(); }); }); describe('nested forms', function() { it('should chain nested forms', function() { doc = jqLite( '' + '' + '' + '' + '' + ''); $compile(doc)(scope); var parent = scope.parent, child = scope.child, inputA = child.inputA, inputB = child.inputB; inputA.$setValidity('MyError', false); inputB.$setValidity('MyError', false); expect(parent.$error.MyError).toEqual([child]); expect(child.$error.MyError).toEqual([inputA, inputB]); inputA.$setValidity('MyError', true); expect(parent.$error.MyError).toEqual([child]); expect(child.$error.MyError).toEqual([inputB]); inputB.$setValidity('MyError', true); expect(parent.$error.MyError).toBeFalsy(); expect(child.$error.MyError).toBeFalsy(); child.$setDirty(); expect(parent.$dirty).toBeTruthy(); child.$setSubmitted(); expect(parent.$submitted).toBeTruthy(); }); it('should set $submitted to true on child forms when parent is submitted', function() { doc = jqLite( '' + '' + '' + '' + '' + ''); $compile(doc)(scope); var parent = scope.parent, child = scope.child; parent.$setSubmitted(); expect(parent.$submitted).toBeTruthy(); expect(child.$submitted).toBeTruthy(); }); it('should not propagate $submitted state on removed child forms when parent is submitted', function() { doc = jqLite( '' + '' + '' + '' + '' + '' + ''); $compile(doc)(scope); var parent = scope.parent, child = scope.child, grandchild = scope.grandchild, ggchild = scope.greatgrandchild; parent.$removeControl(child); parent.$setSubmitted(); expect(parent.$submitted).toBeTruthy(); expect(child.$submitted).not.toBeTruthy(); expect(grandchild.$submitted).not.toBeTruthy(); parent.$addControl(child); expect(parent.$submitted).toBeTruthy(); expect(child.$submitted).not.toBeTruthy(); expect(grandchild.$submitted).not.toBeTruthy(); parent.$setSubmitted(); expect(parent.$submitted).toBeTruthy(); expect(child.$submitted).toBeTruthy(); expect(grandchild.$submitted).toBeTruthy(); parent.$removeControl(child); expect(parent.$submitted).toBeTruthy(); expect(child.$submitted).toBeTruthy(); expect(grandchild.$submitted).toBeTruthy(); parent.$setPristine(); // sets $submitted to false expect(parent.$submitted).not.toBeTruthy(); expect(child.$submitted).toBeTruthy(); expect(grandchild.$submitted).toBeTruthy(); grandchild.$setPristine(); expect(grandchild.$submitted).not.toBeTruthy(); child.$setSubmitted(); expect(parent.$submitted).not.toBeTruthy(); expect(child.$submitted).toBeTruthy(); expect(grandchild.$submitted).toBeTruthy(); child.$setPristine(); expect(parent.$submitted).not.toBeTruthy(); expect(child.$submitted).not.toBeTruthy(); expect(grandchild.$submitted).not.toBeTruthy(); // Test upwards submission setting grandchild.$setSubmitted(); expect(parent.$submitted).not.toBeTruthy(); expect(child.$submitted).toBeTruthy(); expect(grandchild.$submitted).toBeTruthy(); }); it('should set $submitted to true on child and parent forms when form is submitted', function() { doc = jqLite( '' + '' + '' + '' + '' + '' + '' + ''); $compile(doc)(scope); var parent = scope.parent, child = scope.child, grandchild = scope.grandchild; child.$setSubmitted(); expect(parent.$submitted).toBeTruthy(); expect(child.$submitted).toBeTruthy(); expect(grandchild.$submitted).toBeTruthy(); }); it('should deregister a child form when its DOM is removed', function() { doc = jqLite( '
' + '
' + '' + '
' + '
'); $compile(doc)(scope); scope.$apply(); var parent = scope.parent, child = scope.child; expect(parent).toBeDefined(); expect(child).toBeDefined(); expect(parent.$error.required).toEqual([child]); doc.children().remove(); //remove child expect(parent.child).toBeUndefined(); expect(scope.child).toBeUndefined(); expect(parent.$error.required).toBeFalsy(); }); it('should deregister a child form whose name is an expression when its DOM is removed', function() { doc = jqLite( '
' + '
' + '' + '
' + '
'); $compile(doc)(scope); scope.$apply(); var parent = scope.parent, child = scope.child.form; expect(parent).toBeDefined(); expect(child).toBeDefined(); expect(parent.$error.required).toEqual([child]); doc.children().remove(); //remove child expect(parent.child).toBeUndefined(); expect(scope.child.form).toBeUndefined(); expect(parent.$error.required).toBeFalsy(); }); it('should deregister a input when it is removed from DOM', function() { doc = jqLite( '
' + '
' + '' + '
' + '
'); $compile(doc)(scope); scope.inputPresent = true; scope.$apply(); var parent = scope.parent, child = scope.child, input = child.inputA; expect(parent).toBeDefined(); expect(child).toBeDefined(); expect(parent.$error.required).toEqual([child]); expect(parent.$$success.maxlength).toEqual([child]); expect(child.$error.required).toEqual([input]); expect(child.$$success.maxlength).toEqual([input]); expect(doc.hasClass('ng-invalid')).toBe(true); expect(doc.hasClass('ng-invalid-required')).toBe(true); expect(doc.hasClass('ng-valid-maxlength')).toBe(true); expect(doc.find('div').hasClass('ng-invalid')).toBe(true); expect(doc.find('div').hasClass('ng-invalid-required')).toBe(true); expect(doc.find('div').hasClass('ng-valid-maxlength')).toBe(true); //remove child input scope.$apply('inputPresent = false'); expect(parent.$error.required).toBeFalsy(); expect(parent.$$success.maxlength).toBeFalsy(); expect(child.$error.required).toBeFalsy(); expect(child.$$success.maxlength).toBeFalsy(); expect(doc.hasClass('ng-valid')).toBe(true); expect(doc.hasClass('ng-valid-required')).toBe(false); expect(doc.hasClass('ng-invalid-required')).toBe(false); expect(doc.hasClass('ng-valid-maxlength')).toBe(false); expect(doc.hasClass('ng-invalid-maxlength')).toBe(false); expect(doc.find('div').hasClass('ng-valid')).toBe(true); expect(doc.find('div').hasClass('ng-valid-required')).toBe(false); expect(doc.find('div').hasClass('ng-invalid-required')).toBe(false); expect(doc.find('div').hasClass('ng-valid-maxlength')).toBe(false); expect(doc.find('div').hasClass('ng-invalid-maxlength')).toBe(false); }); it('should deregister a input that is $pending when it is removed from DOM', function() { doc = jqLite( '
' + '
' + '' + '
' + '
'); $compile(doc)(scope); scope.$apply('inputPresent = true'); var parent = scope.parent; var child = scope.child; var input = child.inputA; scope.$apply(child.inputA.$setValidity('fake', undefined)); expect(parent).toBeDefined(); expect(child).toBeDefined(); expect(parent.$pending.fake).toEqual([child]); expect(child.$pending.fake).toEqual([input]); expect(doc.hasClass('ng-pending')).toBe(true); expect(doc.find('div').hasClass('ng-pending')).toBe(true); //remove child input scope.$apply('inputPresent = false'); expect(parent.$pending).toBeUndefined(); expect(child.$pending).toBeUndefined(); expect(doc.hasClass('ng-pending')).toBe(false); expect(doc.find('div').hasClass('ng-pending')).toBe(false); }); it('should leave the parent form invalid when deregister a removed input', function() { doc = jqLite( '
' + '
' + '' + '' + '
' + '
'); $compile(doc)(scope); scope.inputPresent = true; scope.$apply(); var parent = scope.parent, child = scope.child, inputA = child.inputA, inputB = child.inputB; expect(parent).toBeDefined(); expect(child).toBeDefined(); expect(parent.$error.required).toEqual([child]); expect(child.$error.required).toEqual([inputB, inputA]); //remove child input scope.inputPresent = false; scope.$apply(); expect(parent.$error.required).toEqual([child]); expect(child.$error.required).toEqual([inputB]); }); it('should ignore changes in manually removed child forms', function() { doc = $compile( '
' + '' + '' + '' + '
')(scope); var form = scope.myForm; var childformController = doc.find('ng-form').eq(0).controller('form'); var input = doc.find('input').eq(0); var inputController = input.controller('ngModel'); changeInputValue(input, 'ab'); scope.$apply(); expect(form.$dirty).toBe(true); expect(form.$error.maxlength).toBeTruthy(); expect(form.$error.maxlength[0].$name).toBe('childform'); inputController.$setPristine(); expect(form.$dirty).toBe(true); form.$setPristine(); // remove child form form.$removeControl(childformController); expect(form.childform).toBeUndefined(); expect(form.$error.maxlength).toBeFalsy(); changeInputValue(input, 'abc'); scope.$apply(); expect(form.$error.maxlength).toBeFalsy(); expect(form.$dirty).toBe(false); }); it('should react to changes in manually added child forms', function() { doc = $compile( '
' + '' + '' + '' + '
')(scope); var form = scope.myForm; var childFormController = doc.find('ng-form').eq(0).controller('form'); var input = doc.find('input').eq(0); // remove child form so we can add it manually form.$removeControl(childFormController); changeInputValue(input, 'ab'); expect(form.childForm).toBeUndefined(); expect(form.$dirty).toBe(false); expect(form.$error.maxlength).toBeFalsy(); // re-add the child form; its current validation state is not propagated form.$addControl(childFormController); expect(form.childForm).toBe(childFormController); expect(form.$error.maxlength).toBeFalsy(); expect(form.$dirty).toBe(false); // Only when the input inside the child form changes, the validation state is propagated changeInputValue(input, 'abc'); expect(form.$error.maxlength[0]).toBe(childFormController); expect(form.$dirty).toBe(false); }); it('should use the correct parent when renaming and removing dynamically added forms', function() { scope.formName = 'childForm'; scope.hasChildForm = true; doc = $compile( '
' + '
' + '' + '' + '' + '
' + '
' + '
')(scope); scope.$digest(); var form = scope.myForm; var otherForm = scope.otherForm; var childForm = form.childForm; // remove child form and add it to another form form.$removeControl(childForm); otherForm.$addControl(childForm); expect(form.childForm).toBeUndefined(); expect(otherForm.childForm).toBe(childForm); // rename the childForm scope.formName = 'childFormMoved'; scope.$digest(); expect(form.childFormMoved).toBeUndefined(); expect(otherForm.childForm).toBeUndefined(); expect(otherForm.childFormMoved).toBe(childForm); scope.hasChildForm = false; scope.$digest(); expect(form.childFormMoved).toBeUndefined(); expect(otherForm.childFormMoved).toBeUndefined(); }); it('should chain nested forms in repeater', function() { doc = jqLite( '' + '' + '' + '' + ''); $compile(doc)(scope); scope.$apply(function() { scope.forms = [1]; }); 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.$setValidity('myRule', false); expect(input.$error.myRule).toEqual(true); expect(child.$error.myRule).toEqual([input]); expect(parent.$error.myRule).toEqual([child]); input.$setValidity('myRule', true); expect(parent.$error.myRule).toBeFalsy(); expect(child.$error.myRule).toBeFalsy(); }); }); describe('validation', function() { beforeEach(function() { doc = $compile( '
' + '' + '
')(scope); scope.$digest(); }); it('should have ng-valid/ng-invalid css class', function() { expect(doc).toBeValid(); control.$setValidity('error', false); scope.$digest(); expect(doc).toBeInvalid(); expect(doc.hasClass('ng-valid-error')).toBe(false); expect(doc.hasClass('ng-invalid-error')).toBe(true); control.$setValidity('another', false); scope.$digest(); expect(doc.hasClass('ng-valid-error')).toBe(false); expect(doc.hasClass('ng-invalid-error')).toBe(true); expect(doc.hasClass('ng-valid-another')).toBe(false); expect(doc.hasClass('ng-invalid-another')).toBe(true); control.$setValidity('error', true); scope.$digest(); expect(doc).toBeInvalid(); expect(doc.hasClass('ng-valid-error')).toBe(true); expect(doc.hasClass('ng-invalid-error')).toBe(false); expect(doc.hasClass('ng-valid-another')).toBe(false); expect(doc.hasClass('ng-invalid-another')).toBe(true); control.$setValidity('another', true); scope.$digest(); expect(doc).toBeValid(); expect(doc.hasClass('ng-valid-error')).toBe(true); expect(doc.hasClass('ng-invalid-error')).toBe(false); expect(doc.hasClass('ng-valid-another')).toBe(true); expect(doc.hasClass('ng-invalid-another')).toBe(false); // validators are skipped, e.g. because of a parser error control.$setValidity('error', null); control.$setValidity('another', null); scope.$digest(); expect(doc.hasClass('ng-valid-error')).toBe(false); expect(doc.hasClass('ng-invalid-error')).toBe(false); expect(doc.hasClass('ng-valid-another')).toBe(false); expect(doc.hasClass('ng-invalid-another')).toBe(false); }); it('should have ng-pristine/ng-dirty css class', function() { expect(doc).toBePristine(); control.$setViewValue(''); scope.$apply(); expect(doc).toBeDirty(); }); }); describe('$pending', function() { beforeEach(function() { doc = $compile('
')(scope); scope.$digest(); }); it('should set valid and invalid to undefined when a validation error state is set as pending', inject(function($q, $rootScope) { var defer, form = doc.data('$formController'); var ctrl = {}; form.$setValidity('matias', undefined, ctrl); expect(form.$valid).toBeUndefined(); expect(form.$invalid).toBeUndefined(); expect(form.$pending.matias).toEqual([ctrl]); form.$setValidity('matias', true, ctrl); expect(form.$valid).toBe(true); expect(form.$invalid).toBe(false); expect(form.$pending).toBeUndefined(); form.$setValidity('matias', false, ctrl); expect(form.$valid).toBe(false); expect(form.$invalid).toBe(true); expect(form.$pending).toBeUndefined(); })); }); describe('$setPristine', function() { it('should reset pristine state of form and controls', function() { doc = $compile( '
' + '' + '' + '
')(scope); scope.$digest(); var form = doc, formCtrl = scope.testForm, input1 = form.find('input').eq(0), input1Ctrl = input1.controller('ngModel'), input2 = form.find('input').eq(1), input2Ctrl = input2.controller('ngModel'); input1Ctrl.$setViewValue('xx'); input2Ctrl.$setViewValue('yy'); scope.$apply(); expect(form).toBeDirty(); expect(input1).toBeDirty(); expect(input2).toBeDirty(); formCtrl.$setPristine(); scope.$digest(); expect(form).toBePristine(); expect(formCtrl.$pristine).toBe(true); expect(formCtrl.$dirty).toBe(false); expect(input1).toBePristine(); expect(input1Ctrl.$pristine).toBe(true); expect(input1Ctrl.$dirty).toBe(false); expect(input2).toBePristine(); expect(input2Ctrl.$pristine).toBe(true); expect(input2Ctrl.$dirty).toBe(false); }); it('should reset pristine state of anonymous form controls', function() { doc = $compile( '
' + '' + '
')(scope); scope.$digest(); var form = doc, formCtrl = scope.testForm, input = form.find('input').eq(0), inputCtrl = input.controller('ngModel'); inputCtrl.$setViewValue('xx'); scope.$apply(); expect(form).toBeDirty(); expect(input).toBeDirty(); formCtrl.$setPristine(); scope.$digest(); expect(form).toBePristine(); expect(formCtrl.$pristine).toBe(true); expect(formCtrl.$dirty).toBe(false); expect(input).toBePristine(); expect(inputCtrl.$pristine).toBe(true); expect(inputCtrl.$dirty).toBe(false); }); it('should reset pristine state of nested forms', function() { doc = $compile( '
' + '
' + '' + '
' + '
')(scope); scope.$digest(); var form = doc, formCtrl = scope.testForm, nestedForm = form.find('div'), nestedFormCtrl = nestedForm.controller('form'), nestedInput = form.find('input').eq(0), nestedInputCtrl = nestedInput.controller('ngModel'); nestedInputCtrl.$setViewValue('xx'); scope.$apply(); expect(form).toBeDirty(); expect(nestedForm).toBeDirty(); expect(nestedInput).toBeDirty(); formCtrl.$setPristine(); scope.$digest(); expect(form).toBePristine(); scope.$digest(); expect(formCtrl.$pristine).toBe(true); expect(formCtrl.$dirty).toBe(false); expect(nestedForm).toBePristine(); expect(nestedFormCtrl.$pristine).toBe(true); expect(nestedFormCtrl.$dirty).toBe(false); expect(nestedInput).toBePristine(); expect(nestedInputCtrl.$pristine).toBe(true); expect(nestedInputCtrl.$dirty).toBe(false); }); }); describe('$setUntouched', function() { it('should trigger setUntouched on form controls', function() { var form = $compile( '
' + '' + '
')(scope); scope.$digest(); scope.myForm.alias.$setTouched(); expect(scope.myForm.alias.$touched).toBe(true); scope.myForm.$setUntouched(); expect(scope.myForm.alias.$touched).toBe(false); dealoc(form); }); it('should trigger setUntouched on form controls with nested forms', function() { var form = $compile( '
' + '
' + '' + '
' + '
')(scope); scope.$digest(); scope.myForm.childForm.alias.$setTouched(); expect(scope.myForm.childForm.alias.$touched).toBe(true); scope.myForm.$setUntouched(); expect(scope.myForm.childForm.alias.$touched).toBe(false); dealoc(form); }); }); describe('$getControls', function() { it('should return an empty array if the controller has no controls', function() { doc = $compile('
')(scope); scope.$digest(); var formCtrl = scope.testForm; expect(formCtrl.$getControls()).toEqual([]); }); it('should return a shallow copy of the form controls', function() { doc = $compile( '
' + '' + '
' + '' + '
' + '
')(scope); scope.$digest(); var form = doc, formCtrl = scope.testForm, formInput = form.children('input').eq(0), formInputCtrl = formInput.controller('ngModel'), nestedForm = form.find('div'), nestedFormCtrl = nestedForm.controller('form'), nestedInput = nestedForm.children('input').eq(0), nestedInputCtrl = nestedInput.controller('ngModel'); var controls = formCtrl.$getControls(); expect(controls).not.toBe(formCtrl.$$controls); controls.push('something'); expect(formCtrl.$$controls).not.toContain('something'); expect(controls[0]).toBe(formInputCtrl); expect(controls[1]).toBe(nestedFormCtrl); var nestedControls = controls[1].$getControls(); expect(nestedControls[0]).toBe(nestedInputCtrl); }); }); it('should rename nested form controls when interpolated name changes', function() { scope.idA = 'A'; scope.idB = 'X'; doc = $compile( '
' + '
' + '
' + '
' + '' )(scope); scope.$digest(); var formA = scope.form.nestedA; expect(formA).toBeDefined(); expect(formA.$name).toBe('nestedA'); var formX = formA.nestedX; expect(formX).toBeDefined(); expect(formX.$name).toBe('nestedX'); scope.idA = 'B'; scope.idB = 'Y'; scope.$digest(); expect(scope.form.nestedA).toBeUndefined(); expect(scope.form.nestedB).toBe(formA); expect(formA.nestedX).toBeUndefined(); expect(formA.nestedY).toBe(formX); }); it('should rename forms with no parent when interpolated name changes', function() { var element = $compile('
')(scope); var element2 = $compile('
')(scope); scope.nameID = 'A'; scope.$digest(); var form = element.controller('form'); var form2 = element2.controller('form'); expect(scope.nameA).toBe(form); expect(scope.ngformA).toBe(form2); expect(form.$name).toBe('nameA'); expect(form2.$name).toBe('ngformA'); scope.nameID = 'B'; scope.$digest(); expect(scope.nameA).toBeUndefined(); expect(scope.ngformA).toBeUndefined(); expect(scope.nameB).toBe(form); expect(scope.ngformB).toBe(form2); expect(form.$name).toBe('nameB'); expect(form2.$name).toBe('ngformB'); }); it('should rename forms with an initially blank name', function() { var element = $compile('
')(scope); scope.$digest(); var form = element.controller('form'); expect(scope['']).toBe(form); expect(form.$name).toBe(''); scope.name = 'foo'; scope.$digest(); expect(scope.foo).toBe(form); expect(form.$name).toBe('foo'); expect(scope.foo).toBe(form); }); describe('$setSubmitted', function() { beforeEach(function() { doc = $compile( '
' + '' + '' + '
')(scope); scope.$digest(); }); it('should not init in submitted state', function() { expect(scope.form.$submitted).toBe(false); }); it('should be in submitted state when submitted', function() { browserTrigger(doc, 'submit'); expect(scope.form.$submitted).toBe(true); }); it('should revert submitted back to false when $setPristine is called on the form', function() { scope.form.$submitted = true; scope.form.$setPristine(); expect(scope.form.$submitted).toBe(false); }); }); }); describe('form animations', function() { beforeEach(module('ngAnimateMock')); function assertValidAnimation(animation, event, classNameAdded, classNameRemoved) { expect(animation.event).toBe(event); expect(animation.args[1]).toBe(classNameAdded); expect(animation.args[2]).toBe(classNameRemoved); } var doc, scope, form; beforeEach(inject(function($rootScope, $compile, $rootElement, $animate) { scope = $rootScope.$new(); doc = jqLite('
'); $rootElement.append(doc); $compile(doc)(scope); $animate.queue = []; form = scope.myForm; })); afterEach(function() { dealoc(doc); }); it('should trigger an animation when invalid', inject(function($animate) { form.$setValidity('required', false); assertValidAnimation($animate.queue[0], 'removeClass', 'ng-valid'); assertValidAnimation($animate.queue[1], 'addClass', 'ng-invalid'); assertValidAnimation($animate.queue[2], 'addClass', 'ng-invalid-required'); })); it('should trigger an animation when valid', inject(function($animate) { form.$setValidity('required', false); $animate.queue = []; form.$setValidity('required', true); assertValidAnimation($animate.queue[0], 'addClass', 'ng-valid'); assertValidAnimation($animate.queue[1], 'removeClass', 'ng-invalid'); assertValidAnimation($animate.queue[2], 'addClass', 'ng-valid-required'); })); it('should trigger an animation when dirty', inject(function($animate) { form.$setDirty(); assertValidAnimation($animate.queue[0], 'removeClass', 'ng-pristine'); assertValidAnimation($animate.queue[1], 'addClass', 'ng-dirty'); })); it('should trigger an animation when pristine', inject(function($animate) { form.$setDirty(); $animate.queue = []; form.$setPristine(); assertValidAnimation($animate.queue[0], 'setClass', 'ng-pristine', 'ng-dirty ng-submitted'); })); it('should trigger custom errors as addClass/removeClass when invalid/valid', inject(function($animate) { form.$setValidity('custom-error', false); assertValidAnimation($animate.queue[0], 'removeClass', 'ng-valid'); assertValidAnimation($animate.queue[1], 'addClass', 'ng-invalid'); assertValidAnimation($animate.queue[2], 'addClass', 'ng-invalid-custom-error'); $animate.queue = []; form.$setValidity('custom-error', true); assertValidAnimation($animate.queue[0], 'addClass', 'ng-valid'); assertValidAnimation($animate.queue[1], 'removeClass', 'ng-invalid'); assertValidAnimation($animate.queue[2], 'addClass', 'ng-valid-custom-error'); assertValidAnimation($animate.queue[3], 'removeClass', 'ng-invalid-custom-error'); })); });