sass-references/angular-material/material/input/input.spec.ts

2336 lines
77 KiB
TypeScript
Raw Permalink Normal View History

2024-12-06 10:42:08 +08:00
import {getSupportedInputTypes} from '@angular/cdk/platform';
import {dispatchFakeEvent, wrappedErrorMessage} from '@angular/cdk/testing/private';
import {
ChangeDetectionStrategy,
Component,
Directive,
ErrorHandler,
Provider,
Type,
ViewChild,
} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync, flush, tick} from '@angular/core/testing';
import {
FormControl,
FormGroup,
FormGroupDirective,
FormsModule,
NgForm,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import {
ErrorStateMatcher,
ShowOnDirtyErrorStateMatcher,
ThemePalette,
} from '@angular/material/core';
import {
FloatLabelType,
MAT_FORM_FIELD_DEFAULT_OPTIONS,
MatFormField,
MatFormFieldAppearance,
MatFormFieldModule,
SubscriptSizing,
getMatFormFieldDuplicatedHintError,
getMatFormFieldMissingControlError,
} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {By} from '@angular/platform-browser';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {MAT_INPUT_VALUE_ACCESSOR, MatInput, MatInputModule} from './index';
describe('MatMdcInput without forms', () => {
it('should default to floating labels', fakeAsync(() => {
let fixture = createComponent(MatInputWithLabel);
fixture.detectChanges();
let formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
expect(formField.floatLabel)
.withContext('Expected MatInput to set floatingLabel to auto by default.')
.toBe('auto');
}));
it('should default to global floating label type', fakeAsync(() => {
let fixture = createComponent(MatInputWithLabel, [
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {floatLabel: 'always'},
},
]);
fixture.detectChanges();
let formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
expect(formField.floatLabel)
.withContext('Expected MatInput to set floatingLabel to always from global option.')
.toBe('always');
}));
it('should not be treated as empty if type is date', fakeAsync(() => {
const fixture = createComponent(MatInputDateTestController);
fixture.detectChanges();
if (getSupportedInputTypes().has('date')) {
const formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
expect(formField).toBeTruthy();
expect(formField!._control.empty).toBe(false);
}
}));
it('should be treated as empty if type is date on unsupported browser', fakeAsync(() => {
const fixture = createComponent(MatInputDateTestController);
fixture.detectChanges();
if (!getSupportedInputTypes().has('date')) {
const formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
expect(formField).toBeTruthy();
expect(formField!._control.empty).toBe(true);
}
}));
it('should treat text input type as empty at init', fakeAsync(() => {
let fixture = createComponent(MatInputTextTestController);
fixture.detectChanges();
const formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
expect(formField).toBeTruthy();
expect(formField!._control.empty).toBe(true);
}));
it('should treat password input type as empty at init', fakeAsync(() => {
let fixture = createComponent(MatInputPasswordTestController);
fixture.detectChanges();
const formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
expect(formField).toBeTruthy();
expect(formField!._control.empty).toBe(true);
}));
it('should treat number input type as empty at init', fakeAsync(() => {
let fixture = createComponent(MatInputNumberTestController);
fixture.detectChanges();
const formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
expect(formField).toBeTruthy();
expect(formField!._control.empty).toBe(true);
}));
it('should not be empty after input entered', fakeAsync(() => {
let fixture = createComponent(MatInputTextTestController);
fixture.detectChanges();
let inputEl = fixture.debugElement.query(By.css('input'))!;
const formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
expect(formField).toBeTruthy();
expect(formField!._control.empty).toBe(true);
inputEl.nativeElement.value = 'hello';
// Simulate input event.
inputEl.triggerEventHandler('input', {target: inputEl.nativeElement});
fixture.detectChanges();
expect(formField!._control.empty).toBe(false);
}));
it('should update the placeholder when input entered', fakeAsync(() => {
let fixture = createComponent(MatInputWithStaticLabel);
fixture.detectChanges();
const inputEl = fixture.debugElement.query(By.css('input'))!;
const formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
expect(formField).toBeTruthy();
expect(formField._control.empty).toBe(true);
expect(formField._shouldLabelFloat()).toBe(false);
// Update the value of the input.
inputEl.nativeElement.value = 'Text';
// Fake behavior of the `(input)` event which should trigger a change detection.
fixture.detectChanges();
expect(formField._control.empty).toBe(false);
// should not float label if there is no label
expect(formField._shouldLabelFloat()).toBe(false);
}));
it('should not be empty when the value set before view init', fakeAsync(() => {
let fixture = createComponent(MatInputWithValueBinding);
fixture.detectChanges();
const formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
expect(formField._control.empty).toBe(false);
fixture.componentInstance.value = '';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(formField._control.empty).toBe(true);
}));
it('should add id', fakeAsync(() => {
let fixture = createComponent(MatInputTextTestController);
fixture.detectChanges();
const inputElement: HTMLInputElement = fixture.debugElement.query(
By.css('input'),
)!.nativeElement;
const labelElement: HTMLInputElement = fixture.debugElement.query(
By.css('label'),
)!.nativeElement;
expect(inputElement.id).toBeTruthy();
expect(inputElement.id).toEqual(labelElement.getAttribute('for')!);
}));
it('should add aria-required reflecting the required state', fakeAsync(() => {
const fixture = createComponent(MatInputWithRequired);
fixture.detectChanges();
const inputElement: HTMLInputElement = fixture.debugElement.query(
By.css('input'),
)!.nativeElement;
expect(inputElement.getAttribute('aria-required'))
.withContext('Expected aria-required to reflect required state of false')
.toBe('false');
fixture.componentInstance.required = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(inputElement.getAttribute('aria-required'))
.withContext('Expected aria-required to reflect required state of true')
.toBe('true');
}));
it('should not overwrite existing id', fakeAsync(() => {
let fixture = createComponent(MatInputWithId);
fixture.detectChanges();
const inputElement: HTMLInputElement = fixture.debugElement.query(
By.css('input'),
)!.nativeElement;
const labelElement: HTMLInputElement = fixture.debugElement.query(
By.css('label'),
)!.nativeElement;
expect(inputElement.id).toBe('test-id');
expect(labelElement.getAttribute('for')).toBe('test-id');
}));
it("validates there's only one hint label per side", () => {
let fixture = createComponent(MatInputInvalidHintTestController);
expect(() => fixture.detectChanges()).toThrowError(
wrappedErrorMessage(getMatFormFieldDuplicatedHintError('start')),
);
});
it("validates there's only one hint label per side (attribute)", () => {
let fixture = createComponent(MatInputInvalidHint2TestController);
expect(() => fixture.detectChanges()).toThrowError(
wrappedErrorMessage(getMatFormFieldDuplicatedHintError('start')),
);
});
it('validates that matInput child is present', fakeAsync(() => {
let fixture = createComponent(MatInputMissingMatInputTestController);
expect(() => fixture.detectChanges()).toThrowError(
wrappedErrorMessage(getMatFormFieldMissingControlError()),
);
}));
it('validates that matInput child is present after initialization', fakeAsync(() => {
let fixture = createComponent(MatInputWithNgIf);
expect(() => fixture.detectChanges()).not.toThrowError(
wrappedErrorMessage(getMatFormFieldMissingControlError()),
);
fixture.componentInstance.renderInput = false;
fixture.changeDetectorRef.markForCheck();
expect(() => fixture.detectChanges()).toThrowError(
wrappedErrorMessage(getMatFormFieldMissingControlError()),
);
}));
it('validates the type', fakeAsync(() => {
let fixture = createComponent(MatInputInvalidTypeTestController);
// Technically this throws during the OnChanges detection phase,
// so the error is really a ChangeDetectionError and it becomes
// hard to build a full exception to compare with.
// We just check for any exception in this case.
expect(() => fixture.detectChanges())
.toThrow
/* new MatInputUnsupportedTypeError('file') */
();
}));
it('supports hint labels attribute', fakeAsync(() => {
let fixture = createComponent(MatInputHintLabelTestController);
fixture.detectChanges();
// If the hint label is empty, expect no label.
expect(fixture.debugElement.query(By.css('.mat-mdc-form-field-hint'))).toBeNull();
fixture.componentInstance.label = 'label';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.mat-mdc-form-field-hint'))).not.toBeNull();
}));
it('sets an id on hint labels', fakeAsync(() => {
let fixture = createComponent(MatInputHintLabelTestController);
fixture.componentInstance.label = 'label';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
let hint = fixture.debugElement.query(By.css('.mat-mdc-form-field-hint'))!.nativeElement;
expect(hint.getAttribute('id')).toBeTruthy();
}));
it('supports hint labels elements', fakeAsync(() => {
let fixture = createComponent(MatInputHintLabel2TestController);
fixture.detectChanges();
// In this case, we should have an empty <mat-hint>.
let el = fixture.debugElement.query(By.css('mat-hint'))!.nativeElement;
expect(el.textContent).toBeFalsy();
fixture.componentInstance.label = 'label';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
el = fixture.debugElement.query(By.css('mat-hint'))!.nativeElement;
expect(el.textContent).toBe('label');
}));
it('sets an id on the hint element', fakeAsync(() => {
let fixture = createComponent(MatInputHintLabel2TestController);
fixture.componentInstance.label = 'label';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
let hint = fixture.debugElement.query(By.css('mat-hint'))!.nativeElement;
expect(hint.getAttribute('id')).toBeTruthy();
}));
it('supports label required star', fakeAsync(() => {
const fixture = createComponent(MatInputLabelRequiredTestComponent);
fixture.detectChanges();
const label = fixture.debugElement.query(By.css('label'))!;
expect(label).not.toBeNull();
expect(label.nativeElement.textContent).toBe('hello');
expect(label.nativeElement.querySelector('.mat-mdc-form-field-required-marker')).toBeTruthy();
}));
it('should show the required star when using a FormControl', fakeAsync(() => {
const fixture = createComponent(MatInputWithRequiredFormControl);
fixture.detectChanges();
const label = fixture.debugElement.query(By.css('label'))!;
expect(label).not.toBeNull();
expect(label.nativeElement.textContent).toBe('Hello');
expect(label.nativeElement.querySelector('.mat-mdc-form-field-required-marker')).toBeTruthy();
}));
it('should not hide the required star if input is disabled', () => {
const fixture = createComponent(MatInputLabelRequiredTestComponent);
fixture.componentInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const label = fixture.debugElement.query(By.css('label'))!;
expect(label).not.toBeNull();
expect(label.nativeElement.textContent).toBe('hello');
expect(label.nativeElement.querySelector('.mat-mdc-form-field-required-marker')).toBeTruthy();
});
it('hide label required star when set to hide the required marker', fakeAsync(() => {
const fixture = createComponent(MatInputLabelRequiredTestComponent);
fixture.detectChanges();
const label = fixture.debugElement.query(By.css('label'))!;
expect(label).not.toBeNull();
expect(label.nativeElement.querySelector('.mat-mdc-form-field-required-marker')).toBeTruthy();
expect(label.nativeElement.textContent).toBe('hello');
fixture.componentInstance.hideRequiredMarker = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(label.nativeElement.querySelector('.mat-mdc-form-field-required-marker')).toBeFalsy();
expect(label.nativeElement.textContent).toBe('hello');
}));
it('supports the disabled attribute as binding', fakeAsync(() => {
const fixture = createComponent(MatInputWithDisabled);
fixture.detectChanges();
const wrapperEl = fixture.debugElement.query(
By.css('.mat-mdc-text-field-wrapper'),
)!.nativeElement;
const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
expect(wrapperEl.classList.contains('mdc-text-field--disabled'))
.withContext(`Expected form field not to start out disabled.`)
.toBe(false);
expect(inputEl.disabled).toBe(false);
fixture.componentInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(wrapperEl.classList.contains('mdc-text-field--disabled'))
.withContext(`Expected form field to look disabled after property is set.`)
.toBe(true);
expect(inputEl.disabled).toBe(true);
}));
it('should be able to set an input as being disabled and interactive', fakeAsync(() => {
const fixture = createComponent(MatInputWithDisabled);
fixture.componentInstance.disabled = true;
fixture.detectChanges();
const input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
expect(input.disabled).toBe(true);
expect(input.readOnly).toBe(false);
expect(input.hasAttribute('aria-disabled')).toBe(false);
expect(input.classList).not.toContain('mat-mdc-input-disabled-interactive');
fixture.componentInstance.disabledInteractive = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(input.disabled).toBe(false);
expect(input.readOnly).toBe(true);
expect(input.getAttribute('aria-disabled')).toBe('true');
expect(input.classList).toContain('mat-mdc-input-disabled-interactive');
}));
it('should not float the label when disabled and disabledInteractive are set', fakeAsync(() => {
const fixture = createComponent(MatInputTextTestController);
fixture.componentInstance.disabled = fixture.componentInstance.disabledInteractive = true;
fixture.detectChanges();
const label = fixture.nativeElement.querySelector('label');
const input = fixture.debugElement
.query(By.directive(MatInput))!
.injector.get<MatInput>(MatInput);
expect(label.classList).not.toContain('mdc-floating-label--float-above');
// Call the focus handler directly to avoid flakyness where
// browsers don't focus elements if the window is minimized.
input._focusChanged(true);
fixture.detectChanges();
expect(label.classList).not.toContain('mdc-floating-label--float-above');
}));
it('should float the label when disabledInteractive is set and the input has a value', fakeAsync(() => {
const fixture = createComponent(MatInputWithDynamicLabel);
fixture.componentInstance.shouldFloat = 'auto';
fixture.componentInstance.disabled = fixture.componentInstance.disabledInteractive = true;
fixture.detectChanges();
const input = fixture.nativeElement.querySelector('input');
const label = fixture.nativeElement.querySelector('label');
expect(label.classList).not.toContain('mdc-floating-label--float-above');
input.value = 'Text';
dispatchFakeEvent(input, 'input');
fixture.detectChanges();
expect(label.classList).toContain('mdc-floating-label--float-above');
}));
it('supports the disabled attribute as binding for select', fakeAsync(() => {
const fixture = createComponent(MatInputSelect);
fixture.detectChanges();
const wrapperEl = fixture.debugElement.query(
By.css('.mat-mdc-text-field-wrapper'),
)!.nativeElement;
const selectEl = fixture.debugElement.query(By.css('select'))!.nativeElement;
expect(wrapperEl.classList.contains('mdc-text-field--disabled'))
.withContext(`Expected form field not to start out disabled.`)
.toBe(false);
expect(selectEl.disabled).toBe(false);
fixture.componentInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(wrapperEl.classList.contains('mdc-text-field--disabled'))
.withContext(`Expected form field to look disabled after property is set.`)
.toBe(true);
expect(selectEl.disabled).toBe(true);
}));
it('should add a class to the form-field if animations are disabled', () => {
configureTestingModule(MatInputWithId, {animations: false});
const fixture = TestBed.createComponent(MatInputWithId);
fixture.detectChanges();
const formFieldEl = fixture.nativeElement.querySelector('.mat-mdc-form-field');
expect(formFieldEl.classList).toContain('mat-form-field-no-animations');
});
it('should add a class to the form field if it has a native select', fakeAsync(() => {
const fixture = createComponent(MatInputSelect);
fixture.detectChanges();
const formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
expect(formField.classList).toContain('mat-mdc-form-field-type-mat-native-select');
}));
it('supports the required attribute as binding', fakeAsync(() => {
let fixture = createComponent(MatInputWithRequired);
fixture.detectChanges();
let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
expect(inputEl.required).toBe(false);
fixture.componentInstance.required = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(inputEl.required).toBe(true);
}));
it('supports the required attribute as binding for select', fakeAsync(() => {
const fixture = createComponent(MatInputSelect);
fixture.detectChanges();
const selectEl = fixture.debugElement.query(By.css('select'))!.nativeElement;
expect(selectEl.required).toBe(false);
fixture.componentInstance.required = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(selectEl.required).toBe(true);
}));
it('supports the type attribute as binding', fakeAsync(() => {
let fixture = createComponent(MatInputWithType);
fixture.detectChanges();
let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
expect(inputEl.type).toBe('text');
fixture.componentInstance.type = 'password';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(inputEl.type).toBe('password');
}));
it('supports textarea', fakeAsync(() => {
let fixture = createComponent(MatInputTextareaWithBindings);
fixture.detectChanges();
const textarea: HTMLTextAreaElement = fixture.nativeElement.querySelector('textarea');
expect(textarea).not.toBeNull();
}));
it('supports select', fakeAsync(() => {
const fixture = createComponent(MatInputSelect);
fixture.detectChanges();
const nativeSelect: HTMLTextAreaElement = fixture.nativeElement.querySelector('select');
expect(nativeSelect).not.toBeNull();
}));
it('sets the aria-describedby when a hintLabel is set', fakeAsync(() => {
let fixture = createComponent(MatInputHintLabelTestController);
fixture.componentInstance.label = 'label';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const hint = fixture.debugElement.query(By.css('.mat-mdc-form-field-hint'))!.nativeElement;
const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
const hintId = hint.getAttribute('id');
expect(input.getAttribute('aria-describedby')).toBe(`initial ${hintId}`);
}));
it('supports user binding to aria-describedby', fakeAsync(() => {
let fixture = createComponent(MatInputWithSubscriptAndAriaDescribedBy);
fixture.componentInstance.label = 'label';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const hint = fixture.debugElement.query(By.css('.mat-mdc-form-field-hint'))!.nativeElement;
const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
const hintId = hint.getAttribute('id');
expect(input.getAttribute('aria-describedby')).toBe(hintId);
fixture.componentInstance.userDescribedByValue = 'custom-error custom-error-two';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(input.getAttribute('aria-describedby')).toBe(`custom-error custom-error-two ${hintId}`);
fixture.componentInstance.userDescribedByValue = 'custom-error';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(input.getAttribute('aria-describedby')).toBe(`custom-error ${hintId}`);
fixture.componentInstance.showError = true;
fixture.changeDetectorRef.markForCheck();
fixture.componentInstance.formControl.markAsTouched();
fixture.componentInstance.formControl.setErrors({invalid: true});
fixture.detectChanges();
expect(input.getAttribute('aria-describedby')).toMatch(/^custom-error mat-mdc-error-\w+\d+$/);
fixture.componentInstance.label = '';
fixture.componentInstance.userDescribedByValue = '';
fixture.componentInstance.showError = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(input.hasAttribute('aria-describedby')).toBe(false);
}));
it('sets the aria-describedby to the id of the mat-hint', fakeAsync(() => {
let fixture = createComponent(MatInputHintLabel2TestController);
fixture.componentInstance.label = 'label';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
let hint = fixture.debugElement.query(By.css('.mat-mdc-form-field-hint'))!.nativeElement;
let input = fixture.debugElement.query(By.css('input'))!.nativeElement;
expect(input.getAttribute('aria-describedby')).toBe(hint.getAttribute('id'));
}));
it('sets the aria-describedby with multiple mat-hint instances', fakeAsync(() => {
let fixture = createComponent(MatInputMultipleHintTestController);
fixture.componentInstance.startId = 'start';
fixture.componentInstance.endId = 'end';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
let input = fixture.debugElement.query(By.css('input'))!.nativeElement;
expect(input.getAttribute('aria-describedby')).toBe('start end');
}));
it('should preserve aria-describedby set directly in the DOM', fakeAsync(() => {
const fixture = createComponent(MatInputHintLabel2TestController);
const input = fixture.nativeElement.querySelector('input');
input.setAttribute('aria-describedby', 'custom');
fixture.componentInstance.label = 'label';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const hint = fixture.nativeElement.querySelector('.mat-mdc-form-field-hint');
expect(input.getAttribute('aria-describedby')).toBe(`${hint.getAttribute('id')} custom`);
}));
it('should set a class on the hint element based on its alignment', fakeAsync(() => {
const fixture = createComponent(MatInputMultipleHintTestController);
fixture.componentInstance.startId = 'start';
fixture.componentInstance.endId = 'end';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const start = fixture.nativeElement.querySelector('#start');
const end = fixture.nativeElement.querySelector('#end');
expect(start.classList).not.toContain('mat-mdc-form-field-hint-end');
expect(end.classList).toContain('mat-mdc-form-field-hint-end');
}));
it('sets the aria-describedby when a hintLabel is set, in addition to a mat-hint', fakeAsync(() => {
let fixture = createComponent(MatInputMultipleHintMixedTestController);
fixture.detectChanges();
let hintLabel = fixture.debugElement.query(
By.css('.mat-mdc-form-field-hint:not(.mat-mdc-form-field-hint-end)'),
)!.nativeElement;
let endLabel = fixture.debugElement.query(
By.css('.mat-mdc-form-field-hint.mat-mdc-form-field-hint-end'),
)!.nativeElement;
let input = fixture.debugElement.query(By.css('input'))!.nativeElement;
let ariaValue = input.getAttribute('aria-describedby');
expect(ariaValue).toBe(`${hintLabel.getAttribute('id')} ${endLabel.getAttribute('id')}`);
}));
it('should float when floatLabel is set to default and text is entered', fakeAsync(() => {
let fixture = createComponent(MatInputWithDynamicLabel);
fixture.detectChanges();
let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
let labelEl = fixture.debugElement.query(By.css('label'))!.nativeElement;
expect(labelEl.classList).toContain('mdc-floating-label--float-above');
fixture.componentInstance.shouldFloat = 'auto';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(labelEl.classList).not.toContain('mdc-floating-label--float-above');
// Update the value of the input.
inputEl.value = 'Text';
// Fake behavior of the `(input)` event which should trigger a change detection.
dispatchFakeEvent(inputEl, 'input');
fixture.detectChanges();
expect(labelEl.classList).toContain('mdc-floating-label--float-above');
}));
it('should always float the label when floatLabel is set to always', fakeAsync(() => {
let fixture = createComponent(MatInputWithDynamicLabel);
fixture.detectChanges();
let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
let labelEl = fixture.debugElement.query(By.css('label'))!.nativeElement;
expect(labelEl.classList).toContain('mdc-floating-label--float-above');
fixture.detectChanges();
// Update the value of the input.
inputEl.value = 'Text';
// Fake behavior of the `(input)` event which should trigger a change detection.
fixture.detectChanges();
expect(labelEl.classList).toContain('mdc-floating-label--float-above');
}));
it('should float labels when select has value', fakeAsync(() => {
const fixture = createComponent(MatInputSelect);
fixture.detectChanges();
const labelEl = fixture.debugElement.query(By.css('label'))!.nativeElement;
expect(labelEl.classList).toContain('mdc-floating-label--float-above');
}));
it('should mark a multi-select as being inline', fakeAsync(() => {
const fixture = createComponent(MatInputSelect);
fixture.detectChanges();
const select: HTMLSelectElement = fixture.nativeElement.querySelector('select');
expect(select.classList).not.toContain('mat-mdc-native-select-inline');
select.multiple = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(select.classList).toContain('mat-mdc-native-select-inline');
}));
it('should mark a select with a size as being inline', fakeAsync(() => {
const fixture = createComponent(MatInputSelect);
fixture.detectChanges();
const select: HTMLSelectElement = fixture.nativeElement.querySelector('select');
expect(select.classList).not.toContain('mat-mdc-native-select-inline');
select.size = 3;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(select.classList).toContain('mat-mdc-native-select-inline');
select.size = 1;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(select.classList).not.toContain('mat-mdc-native-select-inline');
}));
it('should not float the label if the selectedIndex is negative', fakeAsync(() => {
const fixture = createComponent(MatInputSelect);
fixture.detectChanges();
const labelEl = fixture.debugElement.query(By.css('label'))!.nativeElement;
const selectEl: HTMLSelectElement = fixture.nativeElement.querySelector('select');
expect(labelEl.classList).toContain('mdc-floating-label--float-above');
selectEl.selectedIndex = -1;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(labelEl.classList).not.toContain('mdc-floating-label--float-above');
}));
it('should not float labels when select has no value, no option label, no option innerHtml', fakeAsync(() => {
const fixture = createComponent(MatInputSelectWithNoLabelNoValue);
fixture.detectChanges();
const labelEl = fixture.debugElement.query(By.css('label'))!.nativeElement;
expect(labelEl.classList).not.toContain('mdc-floating-label--float-above');
}));
it('should floating labels when select has no value but has option label', fakeAsync(() => {
const fixture = createComponent(MatInputSelectWithLabel);
fixture.detectChanges();
const labelEl = fixture.debugElement.query(By.css('label'))!.nativeElement;
expect(labelEl.classList).toContain('mdc-floating-label--float-above');
}));
it('should floating labels when select has no value but has option innerHTML', fakeAsync(() => {
const fixture = createComponent(MatInputSelectWithInnerHtml);
fixture.detectChanges();
const labelEl = fixture.debugElement.query(By.css('label'))!.nativeElement;
expect(labelEl.classList).toContain('mdc-floating-label--float-above');
}));
it('should not throw if a native select does not have options', fakeAsync(() => {
const fixture = createComponent(MatInputSelectWithoutOptions);
expect(() => fixture.detectChanges()).not.toThrow();
}));
it('should be able to toggle the floating label programmatically', fakeAsync(() => {
const fixture = createComponent(MatInputWithId);
fixture.detectChanges();
const formField = fixture.debugElement.query(By.directive(MatFormField))!;
const containerInstance = formField.componentInstance as MatFormField;
const label = formField.nativeElement.querySelector('label');
expect(containerInstance.floatLabel).toBe('auto');
expect(label.classList).not.toContain('mdc-floating-label--float-above');
fixture.componentInstance.floatLabel = 'always';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(containerInstance.floatLabel).toBe('always');
expect(label.classList).toContain('mdc-floating-label--float-above');
}));
it('should not have prefix and suffix elements when none are specified', fakeAsync(() => {
let fixture = createComponent(MatInputWithId);
fixture.detectChanges();
let prefixEl = fixture.debugElement.query(By.css('.mat-mdc-form-field-prefix'));
let suffixEl = fixture.debugElement.query(By.css('.mat-mdc-form-field-suffix'));
expect(prefixEl).toBeNull();
expect(suffixEl).toBeNull();
}));
it('should add prefix and suffix elements when specified', fakeAsync(() => {
const fixture = createComponent(MatInputWithPrefixAndSuffix);
fixture.detectChanges();
const textPrefixEl = fixture.debugElement.query(By.css('.mat-mdc-form-field-text-prefix'))!;
const textSuffixEl = fixture.debugElement.query(By.css('.mat-mdc-form-field-text-suffix'))!;
const iconPrefixEl = fixture.debugElement.query(By.css('.mat-mdc-form-field-icon-prefix'))!;
const iconSuffixEl = fixture.debugElement.query(By.css('.mat-mdc-form-field-icon-suffix'))!;
expect(textPrefixEl).not.toBeNull();
expect(textSuffixEl).not.toBeNull();
expect(iconPrefixEl).not.toBeNull();
expect(iconSuffixEl).not.toBeNull();
expect(textPrefixEl.nativeElement.innerText.trim()).toEqual('Prefix');
expect(textSuffixEl.nativeElement.innerText.trim()).toEqual('Suffix');
expect(iconPrefixEl.nativeElement.innerText.trim()).toEqual('favorite');
expect(iconSuffixEl.nativeElement.innerText.trim()).toEqual('favorite');
}));
it('should allow ng-container as prefix and suffix', () => {
const fixture = createComponent(InputWithNgContainerPrefixAndSuffix);
fixture.detectChanges();
const textPrefixEl = fixture.debugElement.query(By.css('.mat-mdc-form-field-text-prefix'))!;
const textSuffixEl = fixture.debugElement.query(By.css('.mat-mdc-form-field-text-suffix'))!;
const iconPrefixEl = fixture.debugElement.query(By.css('.mat-mdc-form-field-icon-prefix'))!;
const iconSuffixEl = fixture.debugElement.query(By.css('.mat-mdc-form-field-icon-suffix'))!;
expect(textPrefixEl.nativeElement.innerText.trim()).toEqual('text-prefix');
expect(textSuffixEl.nativeElement.innerText.trim()).toEqual('text-suffix');
expect(iconPrefixEl.nativeElement.innerText.trim()).toEqual('icon-prefix');
expect(iconSuffixEl.nativeElement.innerText.trim()).toEqual('icon-suffix');
});
it('should update empty class when value changes programmatically and OnPush', fakeAsync(() => {
let fixture = createComponent(MatInputOnPush);
fixture.detectChanges();
let component = fixture.componentInstance;
let label = fixture.debugElement.query(By.css('label'))!.nativeElement;
expect(label.classList).not.toContain('mdc-floating-label--float-above');
component.formControl.setValue('something');
fixture.detectChanges();
expect(label.classList).toContain('mdc-floating-label--float-above');
}));
it('should set the focused class when the input is focused', fakeAsync(() => {
let fixture = createComponent(MatInputTextTestController);
fixture.detectChanges();
let input = fixture.debugElement
.query(By.directive(MatInput))!
.injector.get<MatInput>(MatInput);
let container = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
// Call the focus handler directly to avoid flakyness where
// browsers don't focus elements if the window is minimized.
input._focusChanged(true);
fixture.detectChanges();
expect(container.classList).toContain('mat-focused');
}));
it('should remove the focused class if the input becomes disabled while focused', fakeAsync(() => {
const fixture = createComponent(MatInputTextTestController);
fixture.detectChanges();
const input = fixture.debugElement
.query(By.directive(MatInput))!
.injector.get<MatInput>(MatInput);
const container = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
// Call the focus handler directly to avoid flakyness where
// browsers don't focus elements if the window is minimized.
input._focusChanged(true);
fixture.detectChanges();
expect(container.classList).toContain('mat-focused');
input.disabled = true;
fixture.detectChanges();
expect(container.classList).not.toContain('mat-focused');
}));
it('should only show the native control placeholder, when there is a label, on focus', () => {
const fixture = createComponent(MatInputWithLabelAndPlaceholder);
fixture.detectChanges();
const container = fixture.debugElement.query(By.css('mat-form-field'))!.nativeElement;
const label = fixture.debugElement.query(By.css('label'))!.nativeElement;
const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
expect(container.classList).toContain('mat-form-field-hide-placeholder');
expect(label.textContent.trim()).toBe('Label');
input.value = 'Value';
dispatchFakeEvent(input, 'input');
fixture.detectChanges();
expect(container.classList).not.toContain('mat-form-field-hide-placeholder');
});
it('should always show the native control placeholder when floatLabel is set to "always"', () => {
const fixture = createComponent(MatInputWithLabelAndPlaceholder);
fixture.componentInstance.floatLabel = 'always';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const container = fixture.debugElement.query(By.css('mat-form-field'))!.nativeElement;
expect(container.classList).not.toContain('mat-form-field-hide-placeholder');
});
it('should not add the `placeholder` attribute if there is no placeholder', () => {
const fixture = createComponent(MatInputWithoutPlaceholder);
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
expect(input.hasAttribute('placeholder')).toBe(false);
});
it('should not add the native select class if the control is not a native select', () => {
const fixture = createComponent(MatInputWithId);
fixture.detectChanges();
const formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
expect(formField.classList).not.toContain('mat-mdc-form-field-type-mat-native-select');
});
it('should preserve the native placeholder on a non-legacy appearance', fakeAsync(() => {
const fixture = createComponent(MatInputWithLabelAndPlaceholder);
fixture.componentInstance.floatLabel = 'auto';
fixture.componentInstance.appearance = 'outline';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('input').getAttribute('placeholder')).toBe(
'Placeholder',
);
}));
it(
'should use the native input value when determining whether ' +
'the element is empty with a custom accessor',
fakeAsync(() => {
let fixture = createComponent(MatInputWithCustomAccessor, [], [], [CustomMatInputAccessor]);
fixture.detectChanges();
let formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
expect(formField._control.empty).toBe(true);
fixture.nativeElement.querySelector('input').value = 'abc';
fixture.detectChanges();
expect(formField._control.empty).toBe(false);
}),
);
it('should default the form field color to primary', fakeAsync(() => {
const fixture = createComponent(MatInputWithColor);
fixture.detectChanges();
const formField = fixture.nativeElement.querySelector('.mat-mdc-form-field');
expect(formField.classList).toContain('mat-primary');
}));
it('should be able to change the form field color', fakeAsync(() => {
const fixture = createComponent(MatInputWithColor);
fixture.componentInstance.color = 'accent';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const formField = fixture.nativeElement.querySelector('.mat-mdc-form-field');
expect(formField.classList).toContain('mat-accent');
fixture.componentInstance.color = 'warn';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(formField.classList).toContain('mat-warn');
}));
it('should set a class on the input depending on whether it is in a form field', fakeAsync(() => {
const fixture = createComponent(MatInputInsideOutsideFormField);
fixture.detectChanges();
const inFormField = fixture.nativeElement.querySelector('.inside');
const outsideFormField = fixture.nativeElement.querySelector('.outside');
expect(inFormField.classList).toContain('mat-mdc-form-field-input-control');
expect(outsideFormField.classList).not.toContain('mat-mdc-form-field-input-control');
}));
});
describe('MatMdcInput with forms', () => {
describe('error messages', () => {
let fixture: ComponentFixture<MatInputWithFormErrorMessages>;
let testComponent: MatInputWithFormErrorMessages;
let containerEl: HTMLElement;
let inputEl: HTMLInputElement;
beforeEach(fakeAsync(() => {
fixture = createComponent(MatInputWithFormErrorMessages);
fixture.detectChanges();
testComponent = fixture.componentInstance;
containerEl = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
}));
it('should not show any errors if the user has not interacted', fakeAsync(() => {
expect(testComponent.formControl.untouched)
.withContext('Expected untouched form control')
.toBe(true);
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected no error message')
.toBe(0);
expect(inputEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "false".')
.toBe('false');
}));
it('should display an error message when the input is touched and invalid', fakeAsync(() => {
expect(testComponent.formControl.invalid)
.withContext('Expected form control to be invalid')
.toBe(true);
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected no error message')
.toBe(0);
inputEl.value = 'not valid';
testComponent.formControl.markAsTouched();
fixture.detectChanges();
flush();
expect(containerEl.classList)
.withContext('Expected container to have the invalid CSS class.')
.toContain('mat-form-field-invalid');
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected one error message to have been rendered.')
.toBe(1);
expect(inputEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "true".')
.toBe('true');
}));
it('should not reset text-field validity if focus changes for an invalid input', fakeAsync(() => {
// Mark the control as touched, so that the form-field displays as invalid.
testComponent.formControl.markAsTouched();
fixture.detectChanges();
flush();
const wrapperEl = containerEl.querySelector('.mdc-text-field')!;
expect(wrapperEl.classList).toContain('mdc-text-field--invalid');
dispatchFakeEvent(inputEl, 'focus');
dispatchFakeEvent(inputEl, 'blur');
flush();
expect(wrapperEl.classList).toContain('mdc-text-field--invalid');
}));
it('should display an error message when the parent form is submitted', fakeAsync(() => {
expect(testComponent.form.submitted)
.withContext('Expected form not to have been submitted')
.toBe(false);
expect(testComponent.formControl.invalid)
.withContext('Expected form control to be invalid')
.toBe(true);
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected no error message')
.toBe(0);
inputEl.value = 'not valid';
dispatchFakeEvent(fixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
fixture.detectChanges();
flush();
expect(testComponent.form.submitted)
.withContext('Expected form to have been submitted')
.toBe(true);
expect(containerEl.classList)
.withContext('Expected container to have the invalid CSS class.')
.toContain('mat-form-field-invalid');
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected one error message to have been rendered.')
.toBe(1);
expect(inputEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "true".')
.toBe('true');
}));
it('should display an error message when the parent form group is submitted', fakeAsync(() => {
fixture.destroy();
TestBed.resetTestingModule();
let groupFixture = createComponent(MatInputWithFormGroupErrorMessages);
let component: MatInputWithFormGroupErrorMessages;
groupFixture.detectChanges();
component = groupFixture.componentInstance;
containerEl = groupFixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
inputEl = groupFixture.debugElement.query(By.css('input'))!.nativeElement;
expect(component.formGroup.invalid)
.withContext('Expected form control to be invalid')
.toBe(true);
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected no error message')
.toBe(0);
expect(inputEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "false".')
.toBe('false');
expect(component.formGroupDirective.submitted)
.withContext('Expected form not to have been submitted')
.toBe(false);
inputEl.value = 'not valid';
dispatchFakeEvent(groupFixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
groupFixture.detectChanges();
flush();
expect(component.formGroupDirective.submitted)
.withContext('Expected form to have been submitted')
.toBe(true);
expect(containerEl.classList)
.withContext('Expected container to have the invalid CSS class.')
.toContain('mat-form-field-invalid');
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected one error message to have been rendered.')
.toBe(1);
expect(inputEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "true".')
.toBe('true');
}));
it('should hide the errors and show the hints once the input becomes valid', fakeAsync(() => {
testComponent.formControl.markAsTouched();
fixture.detectChanges();
flush();
expect(containerEl.classList)
.withContext('Expected container to have the invalid CSS class.')
.toContain('mat-form-field-invalid');
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected one error message to have been rendered.')
.toBe(1);
expect(containerEl.querySelectorAll('mat-hint').length)
.withContext('Expected no hints to be shown.')
.toBe(0);
testComponent.formControl.setValue('valid value');
fixture.detectChanges();
flush();
expect(containerEl.classList).not.toContain(
'mat-form-field-invalid',
'Expected container not to have the invalid class when valid.',
);
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected no error messages when the input is valid.')
.toBe(0);
expect(containerEl.querySelectorAll('mat-hint').length)
.withContext('Expected one hint to be shown once the input is valid.')
.toBe(1);
}));
it('should not hide the hint if there are no error messages', fakeAsync(() => {
testComponent.renderError = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(containerEl.querySelectorAll('mat-hint').length)
.withContext('Expected one hint to be shown on load.')
.toBe(1);
testComponent.formControl.markAsTouched();
fixture.detectChanges();
flush();
expect(containerEl.querySelectorAll('mat-hint').length)
.withContext('Expected one hint to still be shown.')
.toBe(1);
}));
it('should set the proper aria-live attribute on the error messages', fakeAsync(() => {
testComponent.formControl.markAsTouched();
fixture.detectChanges();
expect(containerEl.querySelector('mat-error')!.getAttribute('aria-live')).toBe('polite');
}));
it('sets the aria-describedby to reference errors when in error state', fakeAsync(() => {
let hintId = fixture.debugElement
.query(By.css('.mat-mdc-form-field-hint'))!
.nativeElement.getAttribute('id');
let describedBy = inputEl.getAttribute('aria-describedby');
expect(hintId).withContext('hint should be shown').toBeTruthy();
expect(describedBy).toBe(hintId);
fixture.componentInstance.formControl.markAsTouched();
fixture.detectChanges();
let errorIds = fixture.debugElement
.queryAll(By.css('.mat-mdc-form-field-error'))
.map(el => el.nativeElement.getAttribute('id'))
.join(' ');
describedBy = inputEl.getAttribute('aria-describedby');
expect(errorIds).withContext('errors should be shown').toBeTruthy();
expect(describedBy).toBe(errorIds);
}));
it('should set `aria-invalid` to true if the input is empty', fakeAsync(() => {
// Submit the form since it's the one that triggers the default error state matcher.
dispatchFakeEvent(fixture.nativeElement.querySelector('form'), 'submit');
fixture.detectChanges();
flush();
expect(testComponent.formControl.invalid)
.withContext('Expected form control to be invalid')
.toBe(true);
expect(inputEl.value).toBe('incorrect');
expect(inputEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "true"')
.toBe('true');
inputEl.value = 'not valid';
fixture.detectChanges();
expect(testComponent.formControl.invalid)
.withContext('Expected form control to be invalid')
.toBe(true);
expect(inputEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "true".')
.toBe('true');
}));
});
describe('custom error behavior', () => {
it('should display an error message when a custom error matcher returns true', fakeAsync(() => {
let fixture = createComponent(InputInFormGroup);
fixture.detectChanges();
let component = fixture.componentInstance;
let containerEl = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
const control = component.formGroup.get('name')!;
expect(control.invalid).withContext('Expected form control to be invalid').toBe(true);
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected no error messages')
.toBe(0);
control.markAsTouched();
fixture.detectChanges();
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected no error messages after being touched.')
.toBe(0);
component.errorState = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected one error messages to have been rendered.')
.toBe(1);
}));
it('should display an error message when global error matcher returns true', fakeAsync(() => {
let fixture = createComponent(MatInputWithFormErrorMessages, [
{
provide: ErrorStateMatcher,
useValue: {isErrorState: () => true},
},
]);
fixture.detectChanges();
let containerEl = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
let testComponent = fixture.componentInstance;
// Expect the control to still be untouched but the error to show due to the global setting
// Expect the control to still be untouched but the error to show due to the global setting
expect(testComponent.formControl.untouched)
.withContext('Expected untouched form control')
.toBe(true);
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected an error message')
.toBe(1);
}));
it('should display an error message when using ShowOnDirtyErrorStateMatcher', fakeAsync(() => {
let fixture = createComponent(MatInputWithFormErrorMessages, [
{
provide: ErrorStateMatcher,
useClass: ShowOnDirtyErrorStateMatcher,
},
]);
fixture.detectChanges();
let containerEl = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
let testComponent = fixture.componentInstance;
expect(testComponent.formControl.invalid)
.withContext('Expected form control to be invalid')
.toBe(true);
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected no error message')
.toBe(0);
testComponent.formControl.markAsTouched();
fixture.detectChanges();
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected no error messages when touched')
.toBe(0);
testComponent.formControl.markAsDirty();
fixture.detectChanges();
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected one error message when dirty')
.toBe(1);
}));
});
it('should update the value when using FormControl.setValue', () => {
let fixture = createComponent(MatInputWithFormControl);
fixture.detectChanges();
let input = fixture.debugElement
.query(By.directive(MatInput))!
.injector.get<MatInput>(MatInput);
expect(input.value).toBeFalsy();
fixture.componentInstance.formControl.setValue('something');
expect(input.value).toBe('something');
});
it('should display disabled styles when using FormControl.disable()', fakeAsync(() => {
const fixture = createComponent(MatInputWithFormControl);
fixture.detectChanges();
const formFieldEl = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
expect(formFieldEl.classList).not.toContain(
'mat-form-field-disabled',
`Expected form field not to start out disabled.`,
);
expect(inputEl.disabled).toBe(false);
fixture.componentInstance.formControl.disable();
fixture.detectChanges();
expect(formFieldEl.classList)
.withContext(`Expected form field to look disabled after disable() is called.`)
.toContain('mat-form-field-disabled');
expect(inputEl.disabled).toBe(true);
}));
it('should not treat the number 0 as empty', fakeAsync(() => {
let fixture = createComponent(MatInputZeroTestController);
fixture.detectChanges();
flush();
fixture.detectChanges();
let formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
expect(formField).not.toBeNull();
expect(formField._control.empty).toBe(false);
}));
it('should update when the form field value is patched without emitting', fakeAsync(() => {
const fixture = createComponent(MatInputWithFormControl);
fixture.detectChanges();
let formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
expect(formField._control.empty).toBe(true);
fixture.componentInstance.formControl.patchValue('value', {emitEvent: false});
fixture.detectChanges();
expect(formField._control.empty).toBe(false);
}));
it('should update notch size after changing appearance to outline', fakeAsync(() => {
const fixture = createComponent(MatInputWithAppearance);
fixture.detectChanges();
tick(16);
expect(fixture.nativeElement.querySelector('.mdc-notched-outline__notch')).toBe(null);
fixture.componentInstance.appearance = 'outline';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick(16);
let notch = fixture.nativeElement.querySelector('.mdc-notched-outline__notch')! as HTMLElement;
expect(notch.style.width).toBeFalsy();
fixture.nativeElement.querySelector('input')!.focus();
fixture.detectChanges();
tick(16);
expect(notch.style.width).toBeTruthy();
}));
});
describe('MatFormField default options', () => {
it('should be fill appearance if no default options provided', () => {
const fixture = createComponent(MatInputWithAppearance);
fixture.detectChanges();
expect(fixture.componentInstance.formField.appearance).toBe('fill');
});
it('should be fill appearance if empty default options provided', () => {
const fixture = createComponent(MatInputWithAppearance, [
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {},
},
]);
fixture.detectChanges();
expect(fixture.componentInstance.formField.appearance).toBe('fill');
});
it('should be able to change the default appearance', () => {
const fixture = createComponent(MatInputWithAppearance, [
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {appearance: 'outline'},
},
]);
fixture.detectChanges();
expect(fixture.componentInstance.formField.appearance).toBe('outline');
});
it('should default hideRequiredMarker to false', () => {
const fixture = createComponent(MatInputWithAppearance, [
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {},
},
]);
fixture.detectChanges();
expect(fixture.componentInstance.formField.hideRequiredMarker).toBe(false);
});
it('should be able to change the default value of hideRequiredMarker and appearance', () => {
const fixture = createComponent(MatInputWithAppearance, [
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {
hideRequiredMarker: true,
appearance: 'outline',
},
},
]);
fixture.detectChanges();
expect(fixture.componentInstance.formField.hideRequiredMarker).toBe(true);
expect(fixture.componentInstance.formField.appearance).toBe('outline');
});
it('should be able to change the default color', () => {
const fixture = createComponent(MatInputSimple, [
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {color: 'accent'},
},
]);
fixture.detectChanges();
const formField = fixture.nativeElement.querySelector('.mat-mdc-form-field');
expect(formField.classList).toContain('mat-accent');
});
it('defaults subscriptSizing to false', () => {
const fixture = createComponent(MatInputWithSubscriptSizing);
fixture.detectChanges();
const subscriptElement = fixture.nativeElement.querySelector(
'.mat-mdc-form-field-subscript-wrapper',
);
expect(fixture.componentInstance.formField.subscriptSizing).toBe('fixed');
expect(subscriptElement.classList.contains('mat-mdc-form-field-subscript-dynamic-size')).toBe(
false,
);
fixture.componentInstance.sizing = 'dynamic';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(fixture.componentInstance.formField.subscriptSizing).toBe('dynamic');
expect(subscriptElement.classList.contains('mat-mdc-form-field-subscript-dynamic-size')).toBe(
true,
);
});
it('changes the default value of subscriptSizing (undefined input)', () => {
const fixture = createComponent(MatInputWithSubscriptSizing, [
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {
subscriptSizing: 'dynamic',
},
},
]);
fixture.detectChanges();
expect(fixture.componentInstance.formField.subscriptSizing).toBe('dynamic');
expect(
fixture.nativeElement
.querySelector('.mat-mdc-form-field-subscript-wrapper')
.classList.contains('mat-mdc-form-field-subscript-dynamic-size'),
).toBe(true);
});
it('changes the default value of subscriptSizing (no input)', () => {
const fixture = createComponent(MatInputWithAppearance, [
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {
subscriptSizing: 'dynamic',
},
},
]);
fixture.detectChanges();
expect(fixture.componentInstance.formField.subscriptSizing).toBe('dynamic');
expect(
fixture.nativeElement
.querySelector('.mat-mdc-form-field-subscript-wrapper')
.classList.contains('mat-mdc-form-field-subscript-dynamic-size'),
).toBe(true);
});
});
describe('MatFormField without label', () => {
it('should not float the label when no label is defined.', () => {
let fixture = createComponent(MatInputWithoutDefinedLabel);
fixture.detectChanges();
const inputEl = fixture.debugElement.query(By.css('input'))!;
const formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
// Update the value of the input and set focus.
inputEl.nativeElement.value = 'Text';
fixture.detectChanges();
// should not float label if there is no label
expect(formField._shouldLabelFloat()).toBe(false);
});
it('should not float the label when the label is removed after it has been shown', () => {
let fixture = createComponent(MatInputWithCondictionalLabel);
fixture.detectChanges();
const inputEl = fixture.debugElement.query(By.css('input'))!;
const formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
// initially, label is present
expect(fixture.nativeElement.querySelector('label')).not.toBeNull();
// removing label after it has been shown
fixture.componentInstance.hasLabel = false;
inputEl.nativeElement.value = 'Text';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
// now expected to not have a label
expect(fixture.nativeElement.querySelector('label')).toBeNull();
// should not float label since there is no label
expect(formField._shouldLabelFloat()).toBe(false);
});
it('should float the label when the label is not removed', () => {
let fixture = createComponent(MatInputWithCondictionalLabel);
fixture.detectChanges();
const inputEl = fixture.debugElement.query(By.css('input'))!;
const formField = fixture.debugElement.query(By.directive(MatFormField))!
.componentInstance as MatFormField;
inputEl.nativeElement.value = 'Text';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
// Expected to have a label
expect(fixture.nativeElement.querySelector('label')).not.toBeNull();
// should float label since there is a label
expect(formField._shouldLabelFloat()).toBe(true);
});
});
function configureTestingModule(
component: Type<any>,
options: {
providers?: Provider[];
imports?: any[];
declarations?: any[];
animations?: boolean;
} = {},
) {
const {providers = [], imports = [], declarations = [], animations = true} = options;
TestBed.configureTestingModule({
imports: [
FormsModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
animations ? BrowserAnimationsModule : NoopAnimationsModule,
ReactiveFormsModule,
...imports,
],
declarations: [component, ...declarations],
providers: [
// Custom error handler that re-throws the error. Errors happening within
// change detection phase will be reported through the handler and thrown
// in Ivy. Since we do not want to pollute the "console.error", but rather
// just rely on the actual error interrupting the test, we re-throw here.
{
provide: ErrorHandler,
useValue: {
handleError: (err: any) => {
throw err;
},
},
},
...providers,
],
});
}
function createComponent<T>(
component: Type<T>,
providers: Provider[] = [],
imports: any[] = [],
declarations: any[] = [],
): ComponentFixture<T> {
configureTestingModule(component, {providers, imports, declarations});
return TestBed.createComponent<T>(component);
}
@Component({
template: `
<mat-form-field [floatLabel]="floatLabel">
<mat-label>Label</mat-label>
<input matNativeControl id="test-id" placeholder="test">
</mat-form-field>`,
standalone: false,
})
class MatInputWithId {
floatLabel: 'always' | 'auto' = 'auto';
}
@Component({
template: `
<mat-form-field>
<input matInput [disabled]="disabled" [disabledInteractive]="disabledInteractive">
</mat-form-field>
`,
standalone: false,
})
class MatInputWithDisabled {
disabled = false;
disabledInteractive = false;
}
@Component({
template: `<mat-form-field><input matInput [required]="required"></mat-form-field>`,
standalone: false,
})
class MatInputWithRequired {
required: boolean;
}
@Component({
template: `<mat-form-field><input matInput [type]="type"></mat-form-field>`,
standalone: false,
})
class MatInputWithType {
type: string;
}
@Component({
template: `
<mat-form-field [hideRequiredMarker]="hideRequiredMarker">
<mat-label>hello</mat-label>
<input matInput required [disabled]="disabled">
</mat-form-field>`,
standalone: false,
})
class MatInputLabelRequiredTestComponent {
hideRequiredMarker: boolean = false;
disabled: boolean = false;
}
@Component({
template: `
<mat-form-field>
<input matInput placeholder="Hello" [formControl]="formControl">
</mat-form-field>`,
standalone: false,
})
class MatInputWithFormControl {
formControl = new FormControl('');
}
@Component({
template: `<mat-form-field><input matInput><mat-hint>{{label}}</mat-hint></mat-form-field>`,
standalone: false,
})
class MatInputHintLabel2TestController {
label: string = '';
}
@Component({
template: `
<mat-form-field [hintLabel]="label">
<input matInput aria-describedby="initial">
</mat-form-field>`,
standalone: false,
})
class MatInputHintLabelTestController {
label: string = '';
}
@Component({
template: `
<mat-form-field [hintLabel]="label">
<input matInput [formControl]="formControl" [aria-describedby]="userDescribedByValue">
@if (showError) {
<mat-error>Some error</mat-error>
}
</mat-form-field>`,
standalone: false,
})
class MatInputWithSubscriptAndAriaDescribedBy {
label: string = '';
userDescribedByValue: string = '';
showError = false;
formControl = new FormControl('');
}
@Component({
template: `<mat-form-field><input matInput [type]="t"></mat-form-field>`,
standalone: false,
})
class MatInputInvalidTypeTestController {
t = 'file';
}
@Component({
template: `
<mat-form-field hintLabel="Hello">
<input matInput>
<mat-hint>World</mat-hint>
</mat-form-field>`,
standalone: false,
})
class MatInputInvalidHint2TestController {}
@Component({
template: `
<mat-form-field>
<input matInput>
<mat-hint>Hello</mat-hint>
<mat-hint>World</mat-hint>
</mat-form-field>`,
standalone: false,
})
class MatInputInvalidHintTestController {}
@Component({
template: `
<mat-form-field>
<input matInput>
<mat-hint align="start" [id]="startId">Hello</mat-hint>
<mat-hint align="end" [id]="endId">World</mat-hint>
</mat-form-field>`,
standalone: false,
})
class MatInputMultipleHintTestController {
startId: string;
endId: string;
}
@Component({
template: `
<mat-form-field hintLabel="Hello">
<input matInput>
<mat-hint align="end">World</mat-hint>
</mat-form-field>`,
standalone: false,
})
class MatInputMultipleHintMixedTestController {}
@Component({
template: `
<mat-form-field>
<input matInput type="date" placeholder="Placeholder">
</mat-form-field>`,
standalone: false,
})
class MatInputDateTestController {}
@Component({
template: `
<mat-form-field>
<mat-label>Label</mat-label>
<input
matInput
type="text"
placeholder="Placeholder"
[disabled]="disabled"
[disabledInteractive]="disabledInteractive">
</mat-form-field>`,
standalone: false,
})
class MatInputTextTestController {
disabled = false;
disabledInteractive = false;
}
@Component({
template: `
<mat-form-field>
<input matInput type="password" placeholder="Placeholder">
</mat-form-field>`,
standalone: false,
})
class MatInputPasswordTestController {}
@Component({
template: `
<mat-form-field>
<input matInput type="number" placeholder="Placeholder">
</mat-form-field>`,
standalone: false,
})
class MatInputNumberTestController {}
@Component({
template: `
<mat-form-field>
<input matInput type="number" placeholder="Placeholder" [(ngModel)]="value">
</mat-form-field>`,
standalone: false,
})
class MatInputZeroTestController {
value = 0;
}
@Component({
template: `
<mat-form-field>
<input matInput placeholder="Label" [value]="value">
</mat-form-field>`,
standalone: false,
})
class MatInputWithValueBinding {
value: string = 'Initial';
}
@Component({
template: `
<mat-form-field>
<input matInput placeholder="Label">
</mat-form-field>
`,
standalone: false,
})
class MatInputWithStaticLabel {}
@Component({
template: `
<mat-form-field [floatLabel]="shouldFloat">
<mat-label>Label</mat-label>
<input
matInput
placeholder="Placeholder"
[disabled]="disabled"
[disabledInteractive]="disabledInteractive">
</mat-form-field>`,
standalone: false,
})
class MatInputWithDynamicLabel {
shouldFloat: 'always' | 'auto' = 'always';
disabled = false;
disabledInteractive = false;
}
@Component({
template: `
<mat-form-field>
<input matInput placeholder="Label">
</mat-form-field>
`,
standalone: false,
})
class MatInputWithoutDefinedLabel {}
@Component({
template: `
<mat-form-field>
@if (hasLabel) {
<mat-label>Label</mat-label>
}
<input matInput>
</mat-form-field>`,
standalone: false,
})
class MatInputWithCondictionalLabel {
hasLabel = true;
}
@Component({
template: `
<mat-form-field>
<textarea matNativeControl [rows]="rows" [cols]="cols" [wrap]="wrap" placeholder="Snacks">
</textarea>
</mat-form-field>`,
standalone: false,
})
class MatInputTextareaWithBindings {
rows: number = 4;
cols: number = 8;
wrap: string = 'hard';
}
@Component({
template: `<mat-form-field><input></mat-form-field>`,
standalone: false,
})
class MatInputMissingMatInputTestController {}
@Component({
template: `
<form #form="ngForm" novalidate>
<mat-form-field>
<input matInput [formControl]="formControl">
<mat-hint>Please type something</mat-hint>
@if (renderError) {
<mat-error>This field is required</mat-error>
}
</mat-form-field>
</form>
`,
standalone: false,
})
class MatInputWithFormErrorMessages {
@ViewChild('form') form: NgForm;
formControl = new FormControl('incorrect', [
Validators.required,
Validators.pattern(/valid value/),
]);
renderError = true;
}
@Component({
template: `
<form [formGroup]="formGroup">
<mat-form-field>
<input matInput
formControlName="name"
[errorStateMatcher]="customErrorStateMatcher">
<mat-hint>Please type something</mat-hint>
<mat-error>This field is required</mat-error>
</mat-form-field>
</form>
`,
standalone: false,
})
class InputInFormGroup {
formGroup = new FormGroup({
name: new FormControl('', [Validators.required, Validators.pattern(/valid value/)]),
});
errorState = false;
customErrorStateMatcher = {
isErrorState: () => this.errorState,
};
}
@Component({
template: `
<form [formGroup]="formGroup" novalidate>
<mat-form-field>
<input matInput formControlName="name">
<mat-hint>Please type something</mat-hint>
<mat-error>This field is required</mat-error>
</mat-form-field>
</form>
`,
standalone: false,
})
class MatInputWithFormGroupErrorMessages {
@ViewChild(FormGroupDirective) formGroupDirective: FormGroupDirective;
formGroup = new FormGroup({
name: new FormControl('incorrect', [Validators.required, Validators.pattern(/valid value/)]),
});
}
@Component({
template: `
<mat-form-field>
<mat-icon matIconPrefix>favorite</mat-icon>
<div matTextPrefix>Prefix</div>
<input matInput>
<div matTextSuffix>Suffix</div>
<mat-icon matIconSuffix>favorite</mat-icon>
</mat-form-field>
`,
standalone: false,
})
class MatInputWithPrefixAndSuffix {}
@Component({
template: `
<mat-form-field>
@if (renderInput) {
<input matInput>
}
</mat-form-field>
`,
standalone: false,
})
class MatInputWithNgIf {
renderInput = true;
}
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<mat-form-field>
<mat-label>Label</mat-label>
<input matInput [formControl]="formControl">
</mat-form-field>
`,
standalone: false,
})
class MatInputOnPush {
formControl = new FormControl('');
}
@Component({
template: `
<mat-form-field>
<mat-label>Label</mat-label>
<input matInput>
</mat-form-field>
`,
standalone: false,
})
class MatInputWithLabel {}
@Component({
template: `
<mat-form-field [floatLabel]="floatLabel" [appearance]="appearance">
<mat-label>Label</mat-label>
<input matInput placeholder="Placeholder">
</mat-form-field>
`,
standalone: false,
})
class MatInputWithLabelAndPlaceholder {
floatLabel: FloatLabelType;
appearance: MatFormFieldAppearance;
}
@Component({
template: `
<mat-form-field [appearance]="appearance">
<mat-label>My Label</mat-label>
<input matInput placeholder="Placeholder">
</mat-form-field>
`,
standalone: false,
})
class MatInputWithAppearance {
@ViewChild(MatFormField) formField: MatFormField;
appearance: MatFormFieldAppearance;
}
@Component({
template: `
<mat-form-field [subscriptSizing]="sizing">
<mat-label>My Label</mat-label>
<input matInput placeholder="Placeholder" required>
</mat-form-field>
`,
standalone: false,
})
class MatInputWithSubscriptSizing {
@ViewChild(MatFormField) formField: MatFormField;
sizing: SubscriptSizing;
}
@Component({
template: `
<mat-form-field>
<input matInput>
</mat-form-field>
`,
standalone: false,
})
class MatInputWithoutPlaceholder {}
@Component({
template: `
<mat-form-field>
<mat-label>Label</mat-label>
<select matNativeControl id="test-id" [disabled]="disabled" [required]="required">
<option value="volvo">Volvo</option>
<option value="saab">Saab</option>
<option value="mercedes">Mercedes</option>
<option value="audi">Audi</option>
</select>
</mat-form-field>`,
standalone: false,
})
class MatInputSelect {
disabled: boolean;
required: boolean;
}
@Component({
template: `
<mat-form-field>
<mat-label>Form-field label</mat-label>
<select matNativeControl>
<option value="" disabled selected></option>
<option value="saab">Saab</option>
<option value="mercedes">Mercedes</option>
<option value="audi">Audi</option>
</select>
</mat-form-field>`,
standalone: false,
})
class MatInputSelectWithNoLabelNoValue {}
@Component({
template: `
<mat-form-field>
<mat-label>Label</mat-label>
<select matNativeControl>
<option value="" label="select a car"></option>
<option value="saab">Saab</option>
<option value="mercedes">Mercedes</option>
<option value="audi">Audi</option>
</select>
</mat-form-field>`,
standalone: false,
})
class MatInputSelectWithLabel {}
@Component({
template: `
<mat-form-field>
<mat-label>Label</mat-label>
<select matNativeControl>
<option value="">select a car</option>
<option value="saab">Saab</option>
<option value="mercedes">Mercedes</option>
<option value="audi">Audi</option>
</select>
</mat-form-field>`,
standalone: false,
})
class MatInputSelectWithInnerHtml {}
@Component({
template: `
<mat-form-field>
<input matInput customInputAccessor placeholder="Placeholder">
</mat-form-field>`,
standalone: false,
})
class MatInputWithCustomAccessor {}
@Component({
template: `
<mat-form-field>
<select matNativeControl>
</select>
</mat-form-field>`,
standalone: false,
})
class MatInputSelectWithoutOptions {}
/** Custom component that never has a value. Used for testing the `MAT_INPUT_VALUE_ACCESSOR`. */
@Directive({
selector: 'input[customInputAccessor]',
providers: [
{
provide: MAT_INPUT_VALUE_ACCESSOR,
useExisting: CustomMatInputAccessor,
},
],
standalone: false,
})
class CustomMatInputAccessor {
get value() {
return this._value;
}
set value(_value: any) {}
private _value = null;
}
@Component({
template: `
<mat-form-field [color]="color">
<input matNativeControl>
</mat-form-field>`,
standalone: false,
})
class MatInputWithColor {
color: ThemePalette;
}
@Component({
template: `
<mat-form-field>
<input class="inside" matNativeControl>
</mat-form-field>
<input class="outside" matNativeControl>
`,
standalone: false,
})
class MatInputInsideOutsideFormField {}
@Component({
template: `
<mat-form-field>
<mat-label>Hello</mat-label>
<input matInput [formControl]="formControl">
</mat-form-field>`,
standalone: false,
})
class MatInputWithRequiredFormControl {
formControl = new FormControl('', [Validators.required]);
}
@Component({
template: `
<mat-form-field>
<input matInput>
</mat-form-field>
`,
standalone: false,
})
class MatInputSimple {}
@Component({
template: `
<mat-form-field>
<ng-container matIconPrefix>icon-prefix</ng-container>
<ng-container matTextPrefix>text-prefix</ng-container>
<input matInput>
<ng-container matTextSuffix>text-suffix</ng-container>
<ng-container matIconSuffix>icon-suffix</ng-container>
</mat-form-field>
`,
standalone: false,
})
class InputWithNgContainerPrefixAndSuffix {}