/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {Component, Directive, Input, forwardRef} from '@angular/core'; import {ComponentFixture, TestBed, async, fakeAsync, tick} from '@angular/core/testing'; import {AbstractControl, ControlValueAccessor, FormsModule, NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR, NgForm, Validator} from '@angular/forms'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {dispatchEvent} from '@angular/platform-browser/testing/browser_util'; export function main() { describe('template-driven forms integration tests', () => { beforeEach(() => { TestBed.configureTestingModule({ declarations: [ StandaloneNgModel, NgModelForm, NgModelGroupForm, NgModelValidBinding, NgModelNgIfForm, NgModelRadioForm, NgModelRangeForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName, NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper, NgModelValidationBindings, NgModelMultipleValidators, NgAsyncValidator, NgModelAsyncValidation, NgModelSelectMultipleForm, NgModelSelectWithNullForm ], imports: [FormsModule] }); }); describe('basic functionality', () => { it('should support ngModel for standalone fields', fakeAsync(() => { const fixture = TestBed.createComponent(StandaloneNgModel); fixture.componentInstance.name = 'oldValue'; fixture.detectChanges(); tick(); // model -> view const input = fixture.debugElement.query(By.css('input')).nativeElement; expect(input.value).toEqual('oldValue'); input.value = 'updatedValue'; dispatchEvent(input, 'input'); tick(); // view -> model expect(fixture.componentInstance.name).toEqual('updatedValue'); })); it('should support ngModel registration with a parent form', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelForm); fixture.componentInstance.name = 'Nancy'; fixture.detectChanges(); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.value).toEqual({name: 'Nancy'}); expect(form.valid).toBe(false); })); it('should add novalidate by default to form element', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelForm); fixture.detectChanges(); tick(); const form = fixture.debugElement.query(By.css('form')); expect(form.nativeElement.getAttribute('novalidate')).toEqual(''); })); it('should support ngModelGroup', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelGroupForm); fixture.componentInstance.first = 'Nancy'; fixture.componentInstance.last = 'Drew'; fixture.componentInstance.email = 'some email'; fixture.detectChanges(); tick(); // model -> view const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.value).toEqual('Nancy'); expect(inputs[1].nativeElement.value).toEqual('Drew'); inputs[0].nativeElement.value = 'Carson'; dispatchEvent(inputs[0].nativeElement, 'input'); tick(); // view -> model const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.value).toEqual({name: {first: 'Carson', last: 'Drew'}, email: 'some email'}); })); it('should add controls and control groups to form control model', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelGroupForm); fixture.componentInstance.first = 'Nancy'; fixture.componentInstance.last = 'Drew'; fixture.componentInstance.email = 'some email'; fixture.detectChanges(); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.control.get('name').value).toEqual({first: 'Nancy', last: 'Drew'}); expect(form.control.get('name.first').value).toEqual('Nancy'); expect(form.control.get('email').value).toEqual('some email'); })); it('should remove controls and control groups from form control model', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelNgIfForm); fixture.componentInstance.emailShowing = true; fixture.componentInstance.first = 'Nancy'; fixture.componentInstance.email = 'some email'; fixture.detectChanges(); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.control.get('email').value).toEqual('some email'); expect(form.value).toEqual({name: {first: 'Nancy'}, email: 'some email'}); // should remove individual control successfully fixture.componentInstance.emailShowing = false; fixture.detectChanges(); tick(); expect(form.control.get('email')).toBe(null); expect(form.value).toEqual({name: {first: 'Nancy'}}); expect(form.control.get('name').value).toEqual({first: 'Nancy'}); expect(form.control.get('name.first').value).toEqual('Nancy'); // should remove form group successfully fixture.componentInstance.groupShowing = false; fixture.detectChanges(); tick(); expect(form.control.get('name')).toBe(null); expect(form.control.get('name.first')).toBe(null); expect(form.value).toEqual({}); })); it('should set status classes with ngModel', async(() => { const fixture = TestBed.createComponent(NgModelForm); fixture.componentInstance.name = 'aa'; fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); const input = fixture.debugElement.query(By.css('input')).nativeElement; expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']); dispatchEvent(input, 'blur'); fixture.detectChanges(); expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-touched']); input.value = 'updatedValue'; dispatchEvent(input, 'input'); fixture.detectChanges(); expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); }); })); it('should set status classes with ngModel and async validators', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelAsyncValidation); fixture.whenStable().then(() => { fixture.detectChanges(); const input = fixture.debugElement.query(By.css('input')).nativeElement; expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-untouched']); dispatchEvent(input, 'blur'); fixture.detectChanges(); expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-touched']); input.value = 'updatedValue'; dispatchEvent(input, 'input'); tick(); fixture.detectChanges(); expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); }); })); it('should set status classes with ngModelGroup and ngForm', async(() => { const fixture = TestBed.createComponent(NgModelGroupForm); fixture.componentInstance.first = ''; fixture.detectChanges(); const form = fixture.debugElement.query(By.css('form')).nativeElement; const modelGroup = fixture.debugElement.query(By.css('[ngModelGroup]')).nativeElement; const input = fixture.debugElement.query(By.css('input')).nativeElement; // ngModelGroup creates its control asynchronously fixture.whenStable().then(() => { fixture.detectChanges(); expect(sortedClassList(modelGroup)).toEqual([ 'ng-invalid', 'ng-pristine', 'ng-untouched' ]); expect(sortedClassList(form)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']); dispatchEvent(input, 'blur'); fixture.detectChanges(); expect(sortedClassList(modelGroup)).toEqual([ 'ng-invalid', 'ng-pristine', 'ng-touched' ]); expect(sortedClassList(form)).toEqual(['ng-invalid', 'ng-pristine', 'ng-touched']); input.value = 'updatedValue'; dispatchEvent(input, 'input'); fixture.detectChanges(); expect(sortedClassList(modelGroup)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); expect(sortedClassList(form)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); }); })); it('should not create a template-driven form when ngNoForm is used', () => { const fixture = TestBed.createComponent(NgNoFormComp); fixture.detectChanges(); expect(fixture.debugElement.children[0].providerTokens.length).toEqual(0); }); it('should not add novalidate when ngNoForm is used', () => { const fixture = TestBed.createComponent(NgNoFormComp); fixture.detectChanges(); const form = fixture.debugElement.query(By.css('form')); expect(form.nativeElement.hasAttribute('novalidate')).toBeFalsy(); }); }); describe('name and ngModelOptions', () => { it('should throw if ngModel has a parent form but no name attr or standalone label', () => { const fixture = TestBed.createComponent(InvalidNgModelNoName); expect(() => fixture.detectChanges()) .toThrowError(new RegExp(`name attribute must be set`)); }); it('should not throw if ngModel has a parent form, no name attr, and a standalone label', () => { const fixture = TestBed.createComponent(NgModelOptionsStandalone); expect(() => fixture.detectChanges()).not.toThrow(); }); it('should not register standalone ngModels with parent form', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelOptionsStandalone); fixture.componentInstance.one = 'some data'; fixture.componentInstance.two = 'should not show'; fixture.detectChanges(); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); const inputs = fixture.debugElement.queryAll(By.css('input')); tick(); expect(form.value).toEqual({one: 'some data'}); expect(inputs[1].nativeElement.value).toEqual('should not show'); })); it('should override name attribute with ngModelOptions name if provided', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelForm); fixture.componentInstance.options = {name: 'override'}; fixture.componentInstance.name = 'some data'; fixture.detectChanges(); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.value).toEqual({override: 'some data'}); })); }); describe('submit and reset events', () => { it('should emit ngSubmit event with the original submit event on submit', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelForm); fixture.componentInstance.event = null; const form = fixture.debugElement.query(By.css('form')); dispatchEvent(form.nativeElement, 'submit'); tick(); expect(fixture.componentInstance.event.type).toEqual('submit'); })); it('should mark NgForm as submitted on submit event', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelForm); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.submitted).toBe(false); const formEl = fixture.debugElement.query(By.css('form')).nativeElement; dispatchEvent(formEl, 'submit'); tick(); expect(form.submitted).toBe(true); })); it('should reset the form to empty when reset event is fired', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelForm); fixture.componentInstance.name = 'should be cleared'; fixture.detectChanges(); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); const formEl = fixture.debugElement.query(By.css('form')); const input = fixture.debugElement.query(By.css('input')); expect(input.nativeElement.value).toBe('should be cleared'); // view value expect(fixture.componentInstance.name).toBe('should be cleared'); // ngModel value expect(form.value.name).toEqual('should be cleared'); // control value dispatchEvent(formEl.nativeElement, 'reset'); fixture.detectChanges(); tick(); expect(input.nativeElement.value).toBe(''); // view value expect(fixture.componentInstance.name).toBe(null); // ngModel value expect(form.value.name).toEqual(null); // control value })); it('should reset the form submit state when reset button is clicked', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelForm); const form = fixture.debugElement.children[0].injector.get(NgForm); const formEl = fixture.debugElement.query(By.css('form')); dispatchEvent(formEl.nativeElement, 'submit'); fixture.detectChanges(); tick(); expect(form.submitted).toBe(true); dispatchEvent(formEl.nativeElement, 'reset'); fixture.detectChanges(); tick(); expect(form.submitted).toBe(false); })); }); describe('valueChange and statusChange events', () => { it('should emit valueChanges and statusChanges on init', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelForm); const form = fixture.debugElement.children[0].injector.get(NgForm); fixture.componentInstance.name = 'aa'; fixture.detectChanges(); expect(form.valid).toEqual(true); expect(form.value).toEqual({}); let formValidity: string; let formValue: Object; form.statusChanges.subscribe((status: string) => formValidity = status); form.valueChanges.subscribe((value: string) => formValue = value); tick(); expect(formValidity).toEqual('INVALID'); expect(formValue).toEqual({name: 'aa'}); })); it('should mark controls dirty before emitting the value change event', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelForm); const form = fixture.debugElement.children[0].injector.get(NgForm).form; fixture.detectChanges(); tick(); form.get('name').valueChanges.subscribe( () => { expect(form.get('name').dirty).toBe(true); }); const inputEl = fixture.debugElement.query(By.css('input')).nativeElement; inputEl.value = 'newValue'; dispatchEvent(inputEl, 'input'); })); it('should mark controls pristine before emitting the value change event when resetting ', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelForm); fixture.detectChanges(); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm).form; const formEl = fixture.debugElement.query(By.css('form')).nativeElement; const inputEl = fixture.debugElement.query(By.css('input')).nativeElement; inputEl.value = 'newValue'; dispatchEvent(inputEl, 'input'); expect(form.get('name').pristine).toBe(false); form.get('name').valueChanges.subscribe( () => { expect(form.get('name').pristine).toBe(true); }); dispatchEvent(formEl, 'reset'); })); }); describe('disabled controls', () => { it('should not consider disabled controls in value or validation', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelGroupForm); fixture.componentInstance.isDisabled = false; fixture.componentInstance.first = ''; fixture.componentInstance.last = 'Drew'; fixture.componentInstance.email = 'some email'; fixture.detectChanges(); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.value).toEqual({name: {first: '', last: 'Drew'}, email: 'some email'}); expect(form.valid).toBe(false); expect(form.control.get('name.first').disabled).toBe(false); fixture.componentInstance.isDisabled = true; fixture.detectChanges(); tick(); expect(form.value).toEqual({name: {last: 'Drew'}, email: 'some email'}); expect(form.valid).toBe(true); expect(form.control.get('name.first').disabled).toBe(true); })); it('should add disabled attribute in the UI if disable() is called programmatically', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelGroupForm); fixture.componentInstance.isDisabled = false; fixture.componentInstance.first = 'Nancy'; fixture.detectChanges(); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); form.control.get('name.first').disable(); fixture.detectChanges(); tick(); const input = fixture.debugElement.query(By.css(`[name="first"]`)); expect(input.nativeElement.disabled).toBe(true); })); it('should disable a custom control if disabled attr is added', async(() => { const fixture = TestBed.createComponent(NgModelCustomWrapper); fixture.componentInstance.name = 'Nancy'; fixture.componentInstance.isDisabled = true; fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); fixture.whenStable().then(() => { const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.control.get('name').disabled).toBe(true); const customInput = fixture.debugElement.query(By.css('[name="custom"]')); expect(customInput.nativeElement.disabled).toEqual(true); }); }); })); it('should disable a control with unbound disabled attr', fakeAsync(() => { TestBed.overrideComponent(NgModelForm, { set: { template: `
`, } }); const fixture = TestBed.createComponent(NgModelForm); fixture.detectChanges(); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.control.get('name').disabled).toBe(true); const input = fixture.debugElement.query(By.css('input')); expect(input.nativeElement.disabled).toEqual(true); form.control.enable(); fixture.detectChanges(); tick(); expect(input.nativeElement.disabled).toEqual(false); })); it('should disable radio controls properly with programmatic call', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelRadioForm); fixture.componentInstance.food = 'fish'; fixture.detectChanges(); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); form.control.get('food').disable(); tick(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.disabled).toBe(true); expect(inputs[1].nativeElement.disabled).toBe(true); expect(inputs[2].nativeElement.disabled).toBe(false); expect(inputs[3].nativeElement.disabled).toBe(false); form.control.disable(); tick(); expect(inputs[0].nativeElement.disabled).toBe(true); expect(inputs[1].nativeElement.disabled).toBe(true); expect(inputs[2].nativeElement.disabled).toBe(true); expect(inputs[3].nativeElement.disabled).toBe(true); form.control.enable(); tick(); expect(inputs[0].nativeElement.disabled).toBe(false); expect(inputs[1].nativeElement.disabled).toBe(false); expect(inputs[2].nativeElement.disabled).toBe(false); expect(inputs[3].nativeElement.disabled).toBe(false); })); }); describe('range control', () => { it('should support ', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelRangeForm); // model -> view fixture.componentInstance.val = 4; fixture.detectChanges(); tick(); const input = fixture.debugElement.query(By.css('input')); expect(input.nativeElement.value).toBe('4'); fixture.detectChanges(); tick(); const newVal = '4'; input.triggerEventHandler('input', {target: {value: newVal}}); tick(); // view -> model fixture.detectChanges(); expect(typeof(fixture.componentInstance.val)).toBe('number'); })); }); describe('radio controls', () => { it('should support ', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelRadioForm); fixture.componentInstance.food = 'fish'; fixture.detectChanges(); tick(); // model -> view const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.checked).toEqual(false); expect(inputs[1].nativeElement.checked).toEqual(true); dispatchEvent(inputs[0].nativeElement, 'change'); tick(); // view -> model expect(fixture.componentInstance.food).toEqual('chicken'); expect(inputs[1].nativeElement.checked).toEqual(false); })); it('should support multiple named groups', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelRadioForm); fixture.componentInstance.food = 'fish'; fixture.componentInstance.drink = 'sprite'; fixture.detectChanges(); tick(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.checked).toEqual(false); expect(inputs[1].nativeElement.checked).toEqual(true); expect(inputs[2].nativeElement.checked).toEqual(false); expect(inputs[3].nativeElement.checked).toEqual(true); dispatchEvent(inputs[0].nativeElement, 'change'); tick(); expect(fixture.componentInstance.food).toEqual('chicken'); expect(fixture.componentInstance.drink).toEqual('sprite'); expect(inputs[1].nativeElement.checked).toEqual(false); expect(inputs[2].nativeElement.checked).toEqual(false); expect(inputs[3].nativeElement.checked).toEqual(true); })); it('should support initial undefined value', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelRadioForm); fixture.detectChanges(); tick(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.checked).toEqual(false); expect(inputs[1].nativeElement.checked).toEqual(false); expect(inputs[2].nativeElement.checked).toEqual(false); expect(inputs[3].nativeElement.checked).toEqual(false); })); it('should support resetting properly', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelRadioForm); fixture.componentInstance.food = 'chicken'; fixture.detectChanges(); tick(); const form = fixture.debugElement.query(By.css('form')); dispatchEvent(form.nativeElement, 'reset'); fixture.detectChanges(); tick(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.checked).toEqual(false); expect(inputs[1].nativeElement.checked).toEqual(false); })); it('should support setting value to null and undefined', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelRadioForm); fixture.componentInstance.food = 'chicken'; fixture.detectChanges(); tick(); fixture.componentInstance.food = null; fixture.detectChanges(); tick(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.checked).toEqual(false); expect(inputs[1].nativeElement.checked).toEqual(false); fixture.componentInstance.food = 'chicken'; fixture.detectChanges(); tick(); fixture.componentInstance.food = undefined; fixture.detectChanges(); tick(); expect(inputs[0].nativeElement.checked).toEqual(false); expect(inputs[1].nativeElement.checked).toEqual(false); })); }); describe('select controls', () => { it('with option values that are objects', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelSelectForm); const comp = fixture.componentInstance; comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}]; comp.selectedCity = comp.cities[1]; fixture.detectChanges(); tick(); const select = fixture.debugElement.query(By.css('select')); const nycOption = fixture.debugElement.queryAll(By.css('option'))[1]; // model -> view expect(select.nativeElement.value).toEqual('1: Object'); expect(nycOption.nativeElement.selected).toBe(true); select.nativeElement.value = '2: Object'; dispatchEvent(select.nativeElement, 'change'); fixture.detectChanges(); tick(); // view -> model expect(comp.selectedCity['name']).toEqual('Buffalo'); })); it('when new options are added', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelSelectForm); const comp = fixture.componentInstance; comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; comp.selectedCity = comp.cities[1]; fixture.detectChanges(); tick(); comp.cities.push({'name': 'Buffalo'}); comp.selectedCity = comp.cities[2]; fixture.detectChanges(); tick(); const select = fixture.debugElement.query(By.css('select')); const buffalo = fixture.debugElement.queryAll(By.css('option'))[2]; expect(select.nativeElement.value).toEqual('2: Object'); expect(buffalo.nativeElement.selected).toBe(true); })); it('when options are removed', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelSelectForm); const comp = fixture.componentInstance; comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; comp.selectedCity = comp.cities[1]; fixture.detectChanges(); tick(); const select = fixture.debugElement.query(By.css('select')); expect(select.nativeElement.value).toEqual('1: Object'); comp.cities.pop(); fixture.detectChanges(); tick(); expect(select.nativeElement.value).not.toEqual('1: Object'); })); it('when option values have same content, but different identities', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelSelectForm); const comp = fixture.componentInstance; comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'NYC'}]; comp.selectedCity = comp.cities[0]; fixture.detectChanges(); comp.selectedCity = comp.cities[2]; fixture.detectChanges(); tick(); const select = fixture.debugElement.query(By.css('select')); const secondNYC = fixture.debugElement.queryAll(By.css('option'))[2]; expect(select.nativeElement.value).toEqual('2: Object'); expect(secondNYC.nativeElement.selected).toBe(true); })); it('should work with null option', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelSelectWithNullForm); const comp = fixture.componentInstance; comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; comp.selectedCity = null; fixture.detectChanges(); const select = fixture.debugElement.query(By.css('select')); select.nativeElement.value = '2: Object'; dispatchEvent(select.nativeElement, 'change'); fixture.detectChanges(); tick(); expect(comp.selectedCity['name']).toEqual('NYC'); select.nativeElement.value = '0: null'; dispatchEvent(select.nativeElement, 'change'); fixture.detectChanges(); tick(); expect(comp.selectedCity).toEqual(null); })); }); describe('select multiple controls', () => { let fixture: ComponentFixture; let comp: NgModelSelectMultipleForm; beforeEach(() => { fixture = TestBed.createComponent(NgModelSelectMultipleForm); comp = fixture.componentInstance; comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}]; }); const setSelectedCities = (selectedCities: any): void => { comp.selectedCities = selectedCities; fixture.detectChanges(); tick(); }; const assertOptionElementSelectedState = (selectedStates: boolean[]): void => { const options = fixture.debugElement.queryAll(By.css('option')); if (options.length !== selectedStates.length) { throw 'the selected state values to assert does not match the number of options'; } for (let i = 0; i < selectedStates.length; i++) { expect(options[i].nativeElement.selected).toBe(selectedStates[i]); } }; const testNewModelValueUnselectsAllOptions = (modelValue: any): void => { setSelectedCities([comp.cities[1]]); assertOptionElementSelectedState([false, true, false]); setSelectedCities(modelValue); const select = fixture.debugElement.query(By.css('select')); expect(select.nativeElement.value).toEqual(''); assertOptionElementSelectedState([false, false, false]); }; it('should support setting value to null and undefined', fakeAsync(() => { testNewModelValueUnselectsAllOptions(null); testNewModelValueUnselectsAllOptions(undefined); })); it('should clear all selected option elements when value of wrong type supplied', fakeAsync(() => { testNewModelValueUnselectsAllOptions(''); })); it('should set option elements to selected that are present in model', fakeAsync(() => { setSelectedCities([comp.cities[0], comp.cities[2]]); assertOptionElementSelectedState([true, false, true]); })); it('should clear selected option elements since removed from model', fakeAsync(() => { const selectedCities: [{'name:string': string}] = <[{'name:string': string}]>[]; selectedCities.push.apply(selectedCities, comp.cities); setSelectedCities(selectedCities); setSelectedCities([comp.cities[1]]); assertOptionElementSelectedState([false, true, false]); })); }); describe('custom value accessors', () => { it('should support standard writing to view and model', async(() => { const fixture = TestBed.createComponent(NgModelCustomWrapper); fixture.componentInstance.name = 'Nancy'; fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); fixture.whenStable().then(() => { // model -> view const customInput = fixture.debugElement.query(By.css('[name="custom"]')); expect(customInput.nativeElement.value).toEqual('Nancy'); customInput.nativeElement.value = 'Carson'; dispatchEvent(customInput.nativeElement, 'input'); fixture.detectChanges(); // view -> model expect(fixture.componentInstance.name).toEqual('Carson'); }); }); })); }); describe('validation directives', () => { it('should support dir validators using bindings', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelValidationBindings); fixture.componentInstance.required = true; fixture.componentInstance.minLen = 3; fixture.componentInstance.maxLen = 3; fixture.componentInstance.pattern = '.{3,}'; fixture.detectChanges(); tick(); const required = fixture.debugElement.query(By.css('[name=required]')); const minLength = fixture.debugElement.query(By.css('[name=minlength]')); const maxLength = fixture.debugElement.query(By.css('[name=maxlength]')); const pattern = fixture.debugElement.query(By.css('[name=pattern]')); required.nativeElement.value = ''; minLength.nativeElement.value = '1'; maxLength.nativeElement.value = '1234'; pattern.nativeElement.value = '12'; dispatchEvent(required.nativeElement, 'input'); dispatchEvent(minLength.nativeElement, 'input'); dispatchEvent(maxLength.nativeElement, 'input'); dispatchEvent(pattern.nativeElement, 'input'); fixture.detectChanges(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.control.hasError('required', ['required'])).toEqual(true); expect(form.control.hasError('minlength', ['minlength'])).toEqual(true); expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(true); expect(form.control.hasError('pattern', ['pattern'])).toEqual(true); required.nativeElement.value = '1'; minLength.nativeElement.value = '123'; maxLength.nativeElement.value = '123'; pattern.nativeElement.value = '123'; dispatchEvent(required.nativeElement, 'input'); dispatchEvent(minLength.nativeElement, 'input'); dispatchEvent(maxLength.nativeElement, 'input'); dispatchEvent(pattern.nativeElement, 'input'); expect(form.valid).toEqual(true); })); it('should support optional fields with string pattern validator', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelMultipleValidators); fixture.componentInstance.required = false; fixture.componentInstance.pattern = '[a-z]+'; fixture.detectChanges(); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); const input = fixture.debugElement.query(By.css('input')); input.nativeElement.value = ''; dispatchEvent(input.nativeElement, 'input'); fixture.detectChanges(); expect(form.valid).toBeTruthy(); input.nativeElement.value = '1'; dispatchEvent(input.nativeElement, 'input'); fixture.detectChanges(); expect(form.valid).toBeFalsy(); expect(form.control.hasError('pattern', ['tovalidate'])).toBeTruthy(); })); it('should support optional fields with RegExp pattern validator', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelMultipleValidators); fixture.componentInstance.required = false; fixture.componentInstance.pattern = /^[a-z]+$/; fixture.detectChanges(); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); const input = fixture.debugElement.query(By.css('input')); input.nativeElement.value = ''; dispatchEvent(input.nativeElement, 'input'); fixture.detectChanges(); expect(form.valid).toBeTruthy(); input.nativeElement.value = '1'; dispatchEvent(input.nativeElement, 'input'); fixture.detectChanges(); expect(form.valid).toBeFalsy(); expect(form.control.hasError('pattern', ['tovalidate'])).toBeTruthy(); })); it('should support optional fields with minlength validator', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelMultipleValidators); fixture.componentInstance.required = false; fixture.componentInstance.minLen = 2; fixture.detectChanges(); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); const input = fixture.debugElement.query(By.css('input')); input.nativeElement.value = ''; dispatchEvent(input.nativeElement, 'input'); fixture.detectChanges(); expect(form.valid).toBeTruthy(); input.nativeElement.value = '1'; dispatchEvent(input.nativeElement, 'input'); fixture.detectChanges(); expect(form.valid).toBeFalsy(); expect(form.control.hasError('minlength', ['tovalidate'])).toBeTruthy(); })); it('changes on bound properties should change the validation state of the form', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelValidationBindings); fixture.detectChanges(); tick(); const required = fixture.debugElement.query(By.css('[name=required]')); const minLength = fixture.debugElement.query(By.css('[name=minlength]')); const maxLength = fixture.debugElement.query(By.css('[name=maxlength]')); const pattern = fixture.debugElement.query(By.css('[name=pattern]')); required.nativeElement.value = ''; minLength.nativeElement.value = '1'; maxLength.nativeElement.value = '1234'; pattern.nativeElement.value = '12'; dispatchEvent(required.nativeElement, 'input'); dispatchEvent(minLength.nativeElement, 'input'); dispatchEvent(maxLength.nativeElement, 'input'); dispatchEvent(pattern.nativeElement, 'input'); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.control.hasError('required', ['required'])).toEqual(false); expect(form.control.hasError('minlength', ['minlength'])).toEqual(false); expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(false); expect(form.control.hasError('pattern', ['pattern'])).toEqual(false); expect(form.valid).toEqual(true); fixture.componentInstance.required = true; fixture.componentInstance.minLen = 3; fixture.componentInstance.maxLen = 3; fixture.componentInstance.pattern = '.{3,}'; fixture.detectChanges(); dispatchEvent(required.nativeElement, 'input'); dispatchEvent(minLength.nativeElement, 'input'); dispatchEvent(maxLength.nativeElement, 'input'); dispatchEvent(pattern.nativeElement, 'input'); expect(form.control.hasError('required', ['required'])).toEqual(true); expect(form.control.hasError('minlength', ['minlength'])).toEqual(true); expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(true); expect(form.control.hasError('pattern', ['pattern'])).toEqual(true); expect(form.valid).toEqual(false); expect(required.nativeElement.getAttribute('required')).toEqual(''); expect(fixture.componentInstance.minLen.toString()) .toEqual(minLength.nativeElement.getAttribute('minlength')); expect(fixture.componentInstance.maxLen.toString()) .toEqual(maxLength.nativeElement.getAttribute('maxlength')); expect(fixture.componentInstance.pattern.toString()) .toEqual(pattern.nativeElement.getAttribute('pattern')); fixture.componentInstance.required = false; fixture.componentInstance.minLen = null; fixture.componentInstance.maxLen = null; fixture.componentInstance.pattern = null; fixture.detectChanges(); expect(form.control.hasError('required', ['required'])).toEqual(false); expect(form.control.hasError('minlength', ['minlength'])).toEqual(false); expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(false); expect(form.control.hasError('pattern', ['pattern'])).toEqual(false); expect(form.valid).toEqual(true); expect(required.nativeElement.getAttribute('required')).toEqual(null); expect(required.nativeElement.getAttribute('minlength')).toEqual(null); expect(required.nativeElement.getAttribute('maxlength')).toEqual(null); expect(required.nativeElement.getAttribute('pattern')).toEqual(null); })); }); describe('ngModel corner cases', () => { it('should update the view when the model is set back to what used to be in the view', fakeAsync(() => { const fixture = TestBed.createComponent(StandaloneNgModel); fixture.componentInstance.name = ''; fixture.detectChanges(); tick(); const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'aa'; input.selectionStart = 1; dispatchEvent(input, 'input'); fixture.detectChanges(); tick(); expect(fixture.componentInstance.name).toEqual('aa'); // Programmatically update the input value to be "bb". fixture.componentInstance.name = 'bb'; fixture.detectChanges(); tick(); expect(input.value).toEqual('bb'); // Programatically set it back to "aa". fixture.componentInstance.name = 'aa'; fixture.detectChanges(); tick(); expect(input.value).toEqual('aa'); })); it('should not crash when validity is checked from a binding', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelValidBinding); tick(); expect(() => fixture.detectChanges()).not.toThrowError(); })); }); }); } @Component({ selector: 'standalone-ng-model', template: ` ` }) class StandaloneNgModel { name: string; } @Component({ selector: 'ng-model-form', template: `
` }) class NgModelForm { name: string; event: Event; options = {}; onReset() {} } @Component({ selector: 'ng-model-group-form', template: `
` }) class NgModelGroupForm { first: string; last: string; email: string; isDisabled: boolean; } @Component({ selector: 'ng-model-valid-binding', template: `
{{ group.valid }}
` }) class NgModelValidBinding { first: string; } @Component({ selector: 'ng-model-ngif-form', template: `
` }) class NgModelNgIfForm { first: string; groupShowing = true; emailShowing = true; email: string; } @Component({ selector: 'ng-no-form', template: `
` }) class NgNoFormComp { } @Component({ selector: 'invalid-ng-model-noname', template: `
` }) class InvalidNgModelNoName { } @Component({ selector: 'ng-model-options-standalone', template: `
` }) class NgModelOptionsStandalone { one: string; two: string; } @Component({selector: 'ng-model-range-form', template: ''}) class NgModelRangeForm { val: any; } @Component({ selector: 'ng-model-radio-form', template: `
` }) class NgModelRadioForm { food: string; drink: string; } @Component({ selector: 'ng-model-select-form', template: ` ` }) class NgModelSelectForm { selectedCity: {[k: string]: string} = {}; cities: any[] = []; } @Component({ selector: 'ng-model-select-null-form', template: ` ` }) class NgModelSelectWithNullForm { selectedCity: {[k: string]: string} = {}; cities: any[] = []; } @Component({ selector: 'ng-model-select-multiple-form', template: ` ` }) class NgModelSelectMultipleForm { selectedCities: any[]; cities: any[] = []; } @Component({ selector: 'ng-model-custom-comp', template: ` `, providers: [{provide: NG_VALUE_ACCESSOR, multi: true, useExisting: NgModelCustomComp}] }) class NgModelCustomComp implements ControlValueAccessor { model: string; @Input('disabled') isDisabled: boolean = false; changeFn: (value: any) => void; writeValue(value: any) { this.model = value; } registerOnChange(fn: (value: any) => void) { this.changeFn = fn; } registerOnTouched() {} setDisabledState(isDisabled: boolean) { this.isDisabled = isDisabled; } } @Component({ selector: 'ng-model-custom-wrapper', template: `
` }) class NgModelCustomWrapper { name: string; isDisabled = false; } @Component({ selector: 'ng-model-validation-bindings', template: `
` }) class NgModelValidationBindings { required: boolean; minLen: number; maxLen: number; pattern: string; } @Component({ selector: 'ng-model-multiple-validators', template: `
` }) class NgModelMultipleValidators { required: boolean; minLen: number; pattern: string|RegExp; } @Directive({ selector: '[ng-async-validator]', providers: [ {provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => NgAsyncValidator), multi: true} ] }) class NgAsyncValidator implements Validator { validate(c: AbstractControl) { return Promise.resolve(null); } } @Component({ selector: 'ng-model-async-validation', template: `` }) class NgModelAsyncValidation { } function sortedClassList(el: HTMLElement) { const l = getDOM().classList(el); l.sort(); return l; }