@@ -4,7 +4,7 @@ import {BACKSPACE, DELETE, ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE, TAB} from '@an
44import { createKeyboardEvent , dispatchFakeEvent , dispatchKeyboardEvent } from '@angular/cdk/testing' ;
55import { Component , DebugElement , QueryList , ViewChild , ViewChildren } from '@angular/core' ;
66import { async , ComponentFixture , fakeAsync , TestBed , tick } from '@angular/core/testing' ;
7- import { FormControl , FormsModule , ReactiveFormsModule } from '@angular/forms' ;
7+ import { FormControl , FormsModule , NgForm , ReactiveFormsModule , Validators } from '@angular/forms' ;
88import { MatFormFieldModule } from '@angular/material/form-field' ;
99import { By } from '@angular/platform-browser' ;
1010import { NoopAnimationsModule } from '@angular/platform-browser/animations' ;
@@ -35,6 +35,7 @@ describe('MatChipList', () => {
3535 NoopAnimationsModule
3636 ] ,
3737 declarations : [
38+ ChipListWithFormErrorMessages ,
3839 StandardChipList ,
3940 FormFieldChipList ,
4041 BasicChipList ,
@@ -864,6 +865,121 @@ describe('MatChipList', () => {
864865 } ) ;
865866 } ) ;
866867
868+ describe ( 'error messages' , ( ) => {
869+ let errorTestComponent : ChipListWithFormErrorMessages ;
870+ let containerEl : HTMLElement ;
871+ let chipListEl : HTMLElement ;
872+
873+ beforeEach ( ( ) => {
874+ fixture = TestBed . createComponent ( ChipListWithFormErrorMessages ) ;
875+ fixture . detectChanges ( ) ;
876+ errorTestComponent = fixture . componentInstance ;
877+ containerEl = fixture . debugElement . query ( By . css ( 'mat-form-field' ) ) . nativeElement ;
878+ chipListEl = fixture . debugElement . query ( By . css ( 'mat-chip-list' ) ) . nativeElement ;
879+ } ) ;
880+
881+ it ( 'should not show any errors if the user has not interacted' , ( ) => {
882+ expect ( errorTestComponent . formControl . untouched )
883+ . toBe ( true , 'Expected untouched form control' ) ;
884+ expect ( containerEl . querySelectorAll ( 'mat-error' ) . length ) . toBe ( 0 , 'Expected no error message' ) ;
885+ expect ( chipListEl . getAttribute ( 'aria-invalid' ) )
886+ . toBe ( 'false' , 'Expected aria-invalid to be set to "false".' ) ;
887+ } ) ;
888+
889+ it ( 'should display an error message when the chip list is touched and invalid' , async ( ( ) => {
890+ expect ( errorTestComponent . formControl . invalid )
891+ . toBe ( true , 'Expected form control to be invalid' ) ;
892+ expect ( containerEl . querySelectorAll ( 'mat-error' ) . length )
893+ . toBe ( 0 , 'Expected no error message' ) ;
894+
895+ errorTestComponent . formControl . markAsTouched ( ) ;
896+ fixture . detectChanges ( ) ;
897+
898+ fixture . whenStable ( ) . then ( ( ) => {
899+ expect ( containerEl . classList )
900+ . toContain ( 'mat-form-field-invalid' , 'Expected container to have the invalid CSS class.' ) ;
901+ expect ( containerEl . querySelectorAll ( 'mat-error' ) . length )
902+ . toBe ( 1 , 'Expected one error message to have been rendered.' ) ;
903+ expect ( chipListEl . getAttribute ( 'aria-invalid' ) )
904+ . toBe ( 'true' , 'Expected aria-invalid to be set to "true".' ) ;
905+ } ) ;
906+ } ) ) ;
907+
908+ it ( 'should display an error message when the parent form is submitted' , fakeAsync ( ( ) => {
909+ expect ( errorTestComponent . form . submitted )
910+ . toBe ( false , 'Expected form not to have been submitted' ) ;
911+ expect ( errorTestComponent . formControl . invalid )
912+ . toBe ( true , 'Expected form control to be invalid' ) ;
913+ expect ( containerEl . querySelectorAll ( 'mat-error' ) . length ) . toBe ( 0 , 'Expected no error message' ) ;
914+
915+ dispatchFakeEvent ( fixture . debugElement . query ( By . css ( 'form' ) ) . nativeElement , 'submit' ) ;
916+ fixture . detectChanges ( ) ;
917+
918+ fixture . whenStable ( ) . then ( ( ) => {
919+ expect ( errorTestComponent . form . submitted )
920+ . toBe ( true , 'Expected form to have been submitted' ) ;
921+ expect ( containerEl . classList )
922+ . toContain ( 'mat-form-field-invalid' , 'Expected container to have the invalid CSS class.' ) ;
923+ expect ( containerEl . querySelectorAll ( 'mat-error' ) . length )
924+ . toBe ( 1 , 'Expected one error message to have been rendered.' ) ;
925+ expect ( chipListEl . getAttribute ( 'aria-invalid' ) )
926+ . toBe ( 'true' , 'Expected aria-invalid to be set to "true".' ) ;
927+ } ) ;
928+ } ) ) ;
929+
930+ it ( 'should hide the errors and show the hints once the chip list becomes valid' ,
931+ fakeAsync ( ( ) => {
932+ errorTestComponent . formControl . markAsTouched ( ) ;
933+ fixture . detectChanges ( ) ;
934+
935+ fixture . whenStable ( ) . then ( ( ) => {
936+ expect ( containerEl . classList )
937+ . toContain ( 'mat-form-field-invalid' , 'Expected container to have the invalid CSS class.' ) ;
938+ expect ( containerEl . querySelectorAll ( 'mat-error' ) . length )
939+ . toBe ( 1 , 'Expected one error message to have been rendered.' ) ;
940+ expect ( containerEl . querySelectorAll ( 'mat-hint' ) . length )
941+ . toBe ( 0 , 'Expected no hints to be shown.' ) ;
942+
943+ errorTestComponent . formControl . setValue ( 'something' ) ;
944+ fixture . detectChanges ( ) ;
945+
946+ fixture . whenStable ( ) . then ( ( ) => {
947+ expect ( containerEl . classList ) . not . toContain ( 'mat-form-field-invalid' ,
948+ 'Expected container not to have the invalid class when valid.' ) ;
949+ expect ( containerEl . querySelectorAll ( 'mat-error' ) . length )
950+ . toBe ( 0 , 'Expected no error messages when the input is valid.' ) ;
951+ expect ( containerEl . querySelectorAll ( 'mat-hint' ) . length )
952+ . toBe ( 1 , 'Expected one hint to be shown once the input is valid.' ) ;
953+ } ) ;
954+ } ) ;
955+ } ) ) ;
956+
957+ it ( 'should set the proper role on the error messages' , ( ) => {
958+ errorTestComponent . formControl . markAsTouched ( ) ;
959+ fixture . detectChanges ( ) ;
960+
961+ expect ( containerEl . querySelector ( 'mat-error' ) ! . getAttribute ( 'role' ) ) . toBe ( 'alert' ) ;
962+ } ) ;
963+
964+ it ( 'sets the aria-describedby to reference errors when in error state' , ( ) => {
965+ let hintId = fixture . debugElement . query ( By . css ( '.mat-hint' ) ) . nativeElement . getAttribute ( 'id' ) ;
966+ let describedBy = chipListEl . getAttribute ( 'aria-describedby' ) ;
967+
968+ expect ( hintId ) . toBeTruthy ( 'hint should be shown' ) ;
969+ expect ( describedBy ) . toBe ( hintId ) ;
970+
971+ fixture . componentInstance . formControl . markAsTouched ( ) ;
972+ fixture . detectChanges ( ) ;
973+
974+ let errorIds = fixture . debugElement . queryAll ( By . css ( '.mat-error' ) )
975+ . map ( el => el . nativeElement . getAttribute ( 'id' ) ) . join ( ' ' ) ;
976+ describedBy = chipListEl . getAttribute ( 'aria-describedby' ) ;
977+
978+ expect ( errorIds ) . toBeTruthy ( 'errors should be shown' ) ;
979+ expect ( describedBy ) . toBe ( errorIds ) ;
980+ } ) ;
981+ } ) ;
982+
867983 function setupStandardList ( ) {
868984 fixture = TestBed . createComponent ( StandardChipList ) ;
869985 fixture . detectChanges ( ) ;
@@ -940,14 +1056,14 @@ class FormFieldChipList {
9401056} )
9411057class BasicChipList {
9421058 foods : any [ ] = [
943- { value : 'steak-0' , viewValue : 'Steak' } ,
944- { value : 'pizza-1' , viewValue : 'Pizza' } ,
945- { value : 'tacos-2' , viewValue : 'Tacos' , disabled : true } ,
946- { value : 'sandwich-3' , viewValue : 'Sandwich' } ,
947- { value : 'chips-4' , viewValue : 'Chips' } ,
948- { value : 'eggs-5' , viewValue : 'Eggs' } ,
949- { value : 'pasta-6' , viewValue : 'Pasta' } ,
950- { value : 'sushi-7' , viewValue : 'Sushi' } ,
1059+ { value : 'steak-0' , viewValue : 'Steak' } ,
1060+ { value : 'pizza-1' , viewValue : 'Pizza' } ,
1061+ { value : 'tacos-2' , viewValue : 'Tacos' , disabled : true } ,
1062+ { value : 'sandwich-3' , viewValue : 'Sandwich' } ,
1063+ { value : 'chips-4' , viewValue : 'Chips' } ,
1064+ { value : 'eggs-5' , viewValue : 'Eggs' } ,
1065+ { value : 'pasta-6' , viewValue : 'Pasta' } ,
1066+ { value : 'sushi-7' , viewValue : 'Sushi' } ,
9511067 ] ;
9521068 control = new FormControl ( ) ;
9531069 isRequired : boolean ;
@@ -975,14 +1091,14 @@ class BasicChipList {
9751091} )
9761092class MultiSelectionChipList {
9771093 foods : any [ ] = [
978- { value : 'steak-0' , viewValue : 'Steak' } ,
979- { value : 'pizza-1' , viewValue : 'Pizza' } ,
980- { value : 'tacos-2' , viewValue : 'Tacos' , disabled : true } ,
981- { value : 'sandwich-3' , viewValue : 'Sandwich' } ,
982- { value : 'chips-4' , viewValue : 'Chips' } ,
983- { value : 'eggs-5' , viewValue : 'Eggs' } ,
984- { value : 'pasta-6' , viewValue : 'Pasta' } ,
985- { value : 'sushi-7' , viewValue : 'Sushi' } ,
1094+ { value : 'steak-0' , viewValue : 'Steak' } ,
1095+ { value : 'pizza-1' , viewValue : 'Pizza' } ,
1096+ { value : 'tacos-2' , viewValue : 'Tacos' , disabled : true } ,
1097+ { value : 'sandwich-3' , viewValue : 'Sandwich' } ,
1098+ { value : 'chips-4' , viewValue : 'Chips' } ,
1099+ { value : 'eggs-5' , viewValue : 'Eggs' } ,
1100+ { value : 'pasta-6' , viewValue : 'Pasta' } ,
1101+ { value : 'sushi-7' , viewValue : 'Sushi' } ,
9861102 ] ;
9871103 control = new FormControl ( ) ;
9881104 isRequired : boolean ;
@@ -1013,14 +1129,14 @@ class MultiSelectionChipList {
10131129} )
10141130class InputChipList {
10151131 foods : any [ ] = [
1016- { value : 'steak-0' , viewValue : 'Steak' } ,
1017- { value : 'pizza-1' , viewValue : 'Pizza' } ,
1018- { value : 'tacos-2' , viewValue : 'Tacos' , disabled : true } ,
1019- { value : 'sandwich-3' , viewValue : 'Sandwich' } ,
1020- { value : 'chips-4' , viewValue : 'Chips' } ,
1021- { value : 'eggs-5' , viewValue : 'Eggs' } ,
1022- { value : 'pasta-6' , viewValue : 'Pasta' } ,
1023- { value : 'sushi-7' , viewValue : 'Sushi' } ,
1132+ { value : 'steak-0' , viewValue : 'Steak' } ,
1133+ { value : 'pizza-1' , viewValue : 'Pizza' } ,
1134+ { value : 'tacos-2' , viewValue : 'Tacos' , disabled : true } ,
1135+ { value : 'sandwich-3' , viewValue : 'Sandwich' } ,
1136+ { value : 'chips-4' , viewValue : 'Chips' } ,
1137+ { value : 'eggs-5' , viewValue : 'Eggs' } ,
1138+ { value : 'pasta-6' , viewValue : 'Pasta' } ,
1139+ { value : 'sushi-7' , viewValue : 'Sushi' } ,
10241140 ] ;
10251141 control = new FormControl ( ) ;
10261142
@@ -1061,8 +1177,8 @@ class InputChipList {
10611177} )
10621178class FalsyValueChipList {
10631179 foods : any [ ] = [
1064- { value : 0 , viewValue : 'Steak' } ,
1065- { value : 1 , viewValue : 'Pizza' } ,
1180+ { value : 0 , viewValue : 'Steak' } ,
1181+ { value : 1 , viewValue : 'Pizza' } ,
10661182 ] ;
10671183 control = new FormControl ( ) ;
10681184 @ViewChildren ( MatChip ) chips : QueryList < MatChip > ;
@@ -1079,9 +1195,36 @@ class FalsyValueChipList {
10791195} )
10801196class SelectedChipList {
10811197 foods : any [ ] = [
1082- { value : 0 , viewValue : 'Steak' , selected : true } ,
1083- { value : 1 , viewValue : 'Pizza' , selected : false } ,
1084- { value : 2 , viewValue : 'Pasta' , selected : true } ,
1198+ { value : 0 , viewValue : 'Steak' , selected : true } ,
1199+ { value : 1 , viewValue : 'Pizza' , selected : false } ,
1200+ { value : 2 , viewValue : 'Pasta' , selected : true } ,
10851201 ] ;
10861202 @ViewChildren ( MatChip ) chips : QueryList < MatChip > ;
10871203}
1204+
1205+ @Component ( {
1206+ template : `
1207+ <form #form="ngForm" novalidate>
1208+ <mat-form-field>
1209+ <mat-chip-list [formControl]="formControl">
1210+ <mat-chip *ngFor="let food of foods" [value]="food.value" [selected]="food.selected">
1211+ {{food.viewValue}}
1212+ </mat-chip>
1213+ </mat-chip-list>
1214+ <mat-hint>Please select a chip, or type to add a new chip</mat-hint>
1215+ <mat-error>Should have value</mat-error>
1216+ </mat-form-field>
1217+ </form>
1218+ `
1219+ } )
1220+ class ChipListWithFormErrorMessages {
1221+ foods : any [ ] = [
1222+ { value : 0 , viewValue : 'Steak' , selected : true } ,
1223+ { value : 1 , viewValue : 'Pizza' , selected : false } ,
1224+ { value : 2 , viewValue : 'Pasta' , selected : true } ,
1225+ ] ;
1226+ @ViewChildren ( MatChip ) chips : QueryList < MatChip > ;
1227+
1228+ @ViewChild ( 'form' ) form : NgForm ;
1229+ formControl = new FormControl ( '' , Validators . required ) ;
1230+ }
0 commit comments