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

1304 lines
46 KiB
TypeScript
Raw Normal View History

2024-12-06 10:42:08 +08:00
import {dispatchFakeEvent} from '@angular/cdk/testing/private';
import {Component, DebugElement, ViewChild} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync, tick, waitForAsync} from '@angular/core/testing';
import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';
import {By} from '@angular/platform-browser';
import {
MAT_RADIO_DEFAULT_OPTIONS,
MatRadioButton,
MatRadioChange,
MatRadioGroup,
MatRadioModule,
} from './index';
describe('MatRadio', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
MatRadioModule,
FormsModule,
ReactiveFormsModule,
DisableableRadioButton,
FocusableRadioButton,
RadiosInsideRadioGroup,
RadioGroupWithNgModel,
RadioGroupWithFormControl,
StandaloneRadioButtons,
InterleavedRadioGroup,
TranscludingWrapper,
RadioButtonWithPredefinedTabindex,
RadioButtonWithPredefinedAriaAttributes,
RadiosInsidePreCheckedRadioGroup,
PreselectedRadioWithStaticValueAndNgIf,
],
});
}));
describe('inside of a group', () => {
let fixture: ComponentFixture<RadiosInsideRadioGroup>;
let groupDebugElement: DebugElement;
let radioDebugElements: DebugElement[];
let radioNativeElements: HTMLElement[];
let radioLabelElements: HTMLLabelElement[];
let radioInputElements: HTMLInputElement[];
let radioFormFieldElements: HTMLInputElement[];
let groupInstance: MatRadioGroup;
let radioInstances: MatRadioButton[];
let testComponent: RadiosInsideRadioGroup;
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(RadiosInsideRadioGroup);
fixture.detectChanges();
testComponent = fixture.debugElement.componentInstance;
groupDebugElement = fixture.debugElement.query(By.directive(MatRadioGroup))!;
groupInstance = groupDebugElement.injector.get<MatRadioGroup>(MatRadioGroup);
radioDebugElements = fixture.debugElement.queryAll(By.directive(MatRadioButton));
radioNativeElements = radioDebugElements.map(debugEl => debugEl.nativeElement);
radioInstances = radioDebugElements.map(debugEl => debugEl.componentInstance);
radioLabelElements = radioDebugElements.map(
debugEl => debugEl.query(By.css('label'))!.nativeElement,
);
radioInputElements = radioDebugElements.map(
debugEl => debugEl.query(By.css('input'))!.nativeElement,
);
radioFormFieldElements = radioDebugElements.map(
debugEl => debugEl.query(By.css('.mdc-form-field'))!.nativeElement,
);
}));
it('should set individual radio names based on the group name', () => {
expect(groupInstance.name).toBeTruthy();
for (const radio of radioInstances) {
expect(radio.name).toBe(groupInstance.name);
}
});
it('should coerce the disabled binding on the radio group', () => {
(testComponent as any).isGroupDisabled = '';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
radioLabelElements[0].click();
fixture.detectChanges();
expect(radioInstances[0].checked).toBe(false);
expect(groupInstance.disabled).toBe(true);
});
it('should disable click interaction when the group is disabled', () => {
testComponent.isGroupDisabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
radioLabelElements[0].click();
fixture.detectChanges();
expect(radioInstances[0].checked).toBe(false);
});
it('should set label position based on the group labelPosition', () => {
testComponent.labelPos = 'before';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
for (const radio of radioInstances) {
expect(radio.labelPosition).toBe('before');
}
testComponent.labelPos = 'after';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
for (const radio of radioInstances) {
expect(radio.labelPosition).toBe('after');
}
});
it('should disable each individual radio when the group is disabled', () => {
testComponent.isGroupDisabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
for (const radio of radioInstances) {
expect(radio.disabled).toBe(true);
}
});
it('should make all disabled buttons interactive if the group is marked as disabledInteractive', () => {
testComponent.isGroupDisabledInteractive = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(radioInstances.every(radio => radio.disabledInteractive)).toBe(true);
});
it('should prevent the click action when disabledInteractive and disabled', () => {
testComponent.isGroupDisabled = true;
testComponent.isGroupDisabledInteractive = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
// We can't monitor the `defaultPrevented` state on the
// native `click` so we dispatch an extra one.
const fakeEvent = dispatchFakeEvent(radioInputElements[0], 'click');
radioInputElements[0].click();
fixture.detectChanges();
expect(fakeEvent.defaultPrevented).toBe(true);
expect(radioInstances[0].checked).toBe(false);
});
it('should set required to each radio button when the group is required', () => {
testComponent.isGroupRequired = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
for (const radio of radioInstances) {
expect(radio.required).toBe(true);
}
});
it('should update the group value when one of the radios changes', () => {
expect(groupInstance.value).toBeFalsy();
radioInstances[0].checked = true;
fixture.detectChanges();
expect(groupInstance.value).toBe('fire');
expect(groupInstance.selected).toBe(radioInstances[0]);
});
it('should update the group and radios when one of the radios is clicked', () => {
expect(groupInstance.value).toBeFalsy();
radioLabelElements[0].click();
fixture.detectChanges();
expect(groupInstance.value).toBe('fire');
expect(groupInstance.selected).toBe(radioInstances[0]);
expect(radioInstances[0].checked).toBe(true);
expect(radioInstances[1].checked).toBe(false);
radioLabelElements[1].click();
fixture.detectChanges();
expect(groupInstance.value).toBe('water');
expect(groupInstance.selected).toBe(radioInstances[1]);
expect(radioInstances[0].checked).toBe(false);
expect(radioInstances[1].checked).toBe(true);
});
it('should check a radio upon interaction with the underlying native radio button', () => {
radioInputElements[0].click();
fixture.detectChanges();
expect(radioInstances[0].checked).toBe(true);
expect(groupInstance.value).toBe('fire');
expect(groupInstance.selected).toBe(radioInstances[0]);
});
it('should emit a change event from radio buttons', () => {
expect(radioInstances[0].checked).toBe(false);
const spies = radioInstances.map((radio, index) =>
jasmine.createSpy(`onChangeSpy ${index} for ${radio.name}`),
);
spies.forEach((spy, index) => radioInstances[index].change.subscribe(spy));
radioLabelElements[0].click();
fixture.detectChanges();
expect(spies[0]).toHaveBeenCalled();
radioLabelElements[1].click();
fixture.detectChanges();
// To match the native radio button behavior, the change event shouldn't
// be triggered when the radio got unselected.
expect(spies[0]).toHaveBeenCalledTimes(1);
expect(spies[1]).toHaveBeenCalledTimes(1);
});
it(`should not emit a change event from the radio group when change group value
programmatically`, () => {
expect(groupInstance.value).toBeFalsy();
const changeSpy = jasmine.createSpy('radio-group change listener');
groupInstance.change.subscribe(changeSpy);
radioLabelElements[0].click();
fixture.detectChanges();
expect(changeSpy).toHaveBeenCalledTimes(1);
groupInstance.value = 'water';
fixture.detectChanges();
expect(changeSpy).toHaveBeenCalledTimes(1);
});
it('should update the group and radios when updating the group value', () => {
expect(groupInstance.value).toBeFalsy();
testComponent.groupValue = 'fire';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(groupInstance.value).toBe('fire');
expect(groupInstance.selected).toBe(radioInstances[0]);
expect(radioInstances[0].checked).toBe(true);
expect(radioInstances[1].checked).toBe(false);
testComponent.groupValue = 'water';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(groupInstance.value).toBe('water');
expect(groupInstance.selected).toBe(radioInstances[1]);
expect(radioInstances[0].checked).toBe(false);
expect(radioInstances[1].checked).toBe(true);
});
it('should deselect all of the radios when the group value is cleared', () => {
radioInstances[0].checked = true;
expect(groupInstance.value).toBeTruthy();
groupInstance.value = null;
expect(radioInstances.every(radio => !radio.checked)).toBe(true);
});
it('should not show ripples on disabled radio buttons', () => {
testComponent.isFirstDisabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
dispatchFakeEvent(radioFormFieldElements[0], 'mousedown');
dispatchFakeEvent(radioFormFieldElements[0], 'mouseup');
let rippleAmount = radioNativeElements[0].querySelectorAll(
'.mat-ripple-element:not(.mat-radio-persistent-ripple)',
).length;
expect(rippleAmount)
.withContext('Expected a disabled radio button to not show ripples')
.toBe(0);
testComponent.isFirstDisabled = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
dispatchFakeEvent(radioFormFieldElements[0], 'mousedown');
dispatchFakeEvent(radioFormFieldElements[0], 'mouseup');
rippleAmount = radioNativeElements[0].querySelectorAll(
'.mat-ripple-element:not(.mat-radio-persistent-ripple)',
).length;
expect(rippleAmount).withContext('Expected an enabled radio button to show ripples').toBe(1);
});
it('should not show ripples if matRippleDisabled input is set', () => {
testComponent.disableRipple = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
for (const radioFormField of radioFormFieldElements) {
dispatchFakeEvent(radioFormField, 'mousedown');
dispatchFakeEvent(radioFormField, 'mouseup');
const rippleAmount = radioNativeElements[0].querySelectorAll(
'.mat-ripple-element:not(.mat-radio-persistent-ripple)',
).length;
expect(rippleAmount).toBe(0);
}
testComponent.disableRipple = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
for (const radioFormField of radioFormFieldElements) {
dispatchFakeEvent(radioFormField, 'mousedown');
dispatchFakeEvent(radioFormField, 'mouseup');
const rippleAmount = radioNativeElements[0].querySelectorAll(
'.mat-ripple-element:not(.mat-radio-persistent-ripple)',
).length;
expect(rippleAmount).toBe(1);
}
});
it(`should update the group's selected radio to null when unchecking that radio
programmatically`, () => {
const changeSpy = jasmine.createSpy('radio-group change listener');
groupInstance.change.subscribe(changeSpy);
radioInstances[0].checked = true;
fixture.detectChanges();
expect(changeSpy).not.toHaveBeenCalled();
expect(groupInstance.value).toBeTruthy();
radioInstances[0].checked = false;
fixture.detectChanges();
expect(changeSpy).not.toHaveBeenCalled();
expect(groupInstance.value).toBeFalsy();
expect(radioInstances.every(radio => !radio.checked)).toBe(true);
expect(groupInstance.selected).toBeNull();
});
it('should not fire a change event from the group when a radio checked state changes', () => {
const changeSpy = jasmine.createSpy('radio-group change listener');
groupInstance.change.subscribe(changeSpy);
radioInstances[0].checked = true;
fixture.detectChanges();
expect(changeSpy).not.toHaveBeenCalled();
expect(groupInstance.value).toBeTruthy();
expect(groupInstance.value).toBe('fire');
radioInstances[1].checked = true;
fixture.detectChanges();
expect(groupInstance.value).toBe('water');
expect(changeSpy).not.toHaveBeenCalled();
});
it(`should update checked status if changed value to radio group's value`, () => {
const changeSpy = jasmine.createSpy('radio-group change listener');
groupInstance.change.subscribe(changeSpy);
groupInstance.value = 'apple';
expect(changeSpy).not.toHaveBeenCalled();
expect(groupInstance.value).toBe('apple');
expect(groupInstance.selected).withContext('expect group selected to be null').toBeFalsy();
expect(radioInstances[0].checked)
.withContext('should not select the first button')
.toBeFalsy();
expect(radioInstances[1].checked)
.withContext('should not select the second button')
.toBeFalsy();
expect(radioInstances[2].checked)
.withContext('should not select the third button')
.toBeFalsy();
radioInstances[0].value = 'apple';
fixture.detectChanges();
expect(groupInstance.selected)
.withContext('expect group selected to be first button')
.toBe(radioInstances[0]);
expect(radioInstances[0].checked)
.withContext('expect group select the first button')
.toBeTruthy();
expect(radioInstances[1].checked)
.withContext('should not select the second button')
.toBeFalsy();
expect(radioInstances[2].checked)
.withContext('should not select the third button')
.toBeFalsy();
});
it('should apply class based on color attribute', () => {
expect(radioNativeElements.every(radioEl => radioEl.classList.contains('mat-accent')))
.withContext('Expected every radio element to use the accent color by default.')
.toBe(true);
testComponent.color = 'primary';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(radioNativeElements.every(radioEl => radioEl.classList.contains('mat-primary')))
.withContext('Expected every radio element to use the primary color from the binding.')
.toBe(true);
testComponent.color = 'warn';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(radioNativeElements.every(radioEl => radioEl.classList.contains('mat-warn')))
.withContext('Expected every radio element to use the primary color from the binding.')
.toBe(true);
testComponent.color = null;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(radioNativeElements.every(radioEl => radioEl.classList.contains('mat-accent')))
.withContext('Expected every radio element to fallback to accent color if value is falsy.')
.toBe(true);
});
it('should be able to inherit the color from the radio group', () => {
groupInstance.color = 'warn';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(radioNativeElements.every(radioEl => radioEl.classList.contains('mat-warn')))
.withContext('Expected every radio element to have the warn color.')
.toBe(true);
});
it('should have the individual button color take precedence over the group color', () => {
radioInstances[1].color = 'primary';
groupInstance.color = 'warn';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(radioNativeElements[0].classList).toContain('mat-warn');
expect(radioNativeElements[1].classList).toContain('mat-primary');
expect(radioNativeElements[2].classList).toContain('mat-warn');
});
it('should have a focus indicator', () => {
const radioRippleNativeElements = radioNativeElements.map(
element => element.querySelector('.mat-radio-ripple')!,
);
expect(
radioRippleNativeElements.every(element =>
element.classList.contains('mat-focus-indicator'),
),
).toBe(true);
});
it('should set the input tabindex based on the selected radio button', () => {
const getTabIndexes = () => {
return radioInputElements.map(element => parseInt(element.getAttribute('tabindex') || ''));
};
expect(getTabIndexes()).toEqual([0, 0, 0]);
radioLabelElements[0].click();
fixture.detectChanges();
expect(getTabIndexes()).toEqual([0, -1, -1]);
radioLabelElements[1].click();
fixture.detectChanges();
expect(getTabIndexes()).toEqual([-1, 0, -1]);
radioLabelElements[2].click();
fixture.detectChanges();
expect(getTabIndexes()).toEqual([-1, -1, 0]);
});
it('should set the input tabindex correctly with a pre-checked radio button', () => {
const precheckedFixture = TestBed.createComponent(RadiosInsidePreCheckedRadioGroup);
precheckedFixture.detectChanges();
const radios: NodeListOf<HTMLElement> =
precheckedFixture.nativeElement.querySelectorAll('mat-radio-button input');
expect(
Array.from(radios).map(radio => {
return radio.getAttribute('tabindex');
}),
).toEqual(['-1', '-1', '0']);
});
it('should clear the selected radio button but preserve the value on destroy', () => {
radioLabelElements[0].click();
fixture.detectChanges();
expect(groupInstance.selected).toBe(radioInstances[0]);
expect(groupInstance.value).toBe('fire');
fixture.componentInstance.isFirstShown = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(groupInstance.selected).toBe(null);
expect(groupInstance.value).toBe('fire');
});
});
describe('group with ngModel', () => {
let fixture: ComponentFixture<RadioGroupWithNgModel>;
let groupDebugElement: DebugElement;
let radioDebugElements: DebugElement[];
let innerRadios: DebugElement[];
let radioLabelElements: HTMLLabelElement[];
let groupInstance: MatRadioGroup;
let radioInstances: MatRadioButton[];
let testComponent: RadioGroupWithNgModel;
let groupNgModel: NgModel;
beforeEach(() => {
fixture = TestBed.createComponent(RadioGroupWithNgModel);
fixture.detectChanges();
testComponent = fixture.debugElement.componentInstance;
groupDebugElement = fixture.debugElement.query(By.directive(MatRadioGroup))!;
groupInstance = groupDebugElement.injector.get<MatRadioGroup>(MatRadioGroup);
groupNgModel = groupDebugElement.injector.get<NgModel>(NgModel);
radioDebugElements = fixture.debugElement.queryAll(By.directive(MatRadioButton));
radioInstances = radioDebugElements.map(debugEl => debugEl.componentInstance);
innerRadios = fixture.debugElement.queryAll(By.css('input[type="radio"]'));
radioLabelElements = radioDebugElements.map(
debugEl => debugEl.query(By.css('label'))!.nativeElement,
);
});
it('should set individual radio names based on the group name', () => {
expect(groupInstance.name).toBeTruthy();
for (const radio of radioInstances) {
expect(radio.name).toBe(groupInstance.name);
}
groupInstance.name = 'new name';
for (const radio of radioInstances) {
expect(radio.name).toBe(groupInstance.name);
}
});
it('should update the name of radio DOM elements if the name of the group changes', () => {
const nodes: HTMLInputElement[] = innerRadios.map(radio => radio.nativeElement);
expect(nodes.every(radio => radio.getAttribute('name') === groupInstance.name))
.withContext('Expected all radios to have the initial name.')
.toBe(true);
fixture.componentInstance.groupName = 'changed-name';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(groupInstance.name).toBe('changed-name');
expect(nodes.every(radio => radio.getAttribute('name') === groupInstance.name))
.withContext('Expected all radios to have the new name.')
.toBe(true);
});
it('should check the corresponding radio button on group value change', () => {
expect(groupInstance.value).toBeFalsy();
for (const radio of radioInstances) {
expect(radio.checked).toBeFalsy();
}
groupInstance.value = 'vanilla';
for (const radio of radioInstances) {
expect(radio.checked).toBe(groupInstance.value === radio.value);
}
expect(groupInstance.selected!.value).toBe(groupInstance.value);
});
it('should have the correct control state initially and after interaction', () => {
// The control should start off valid, pristine, and untouched.
expect(groupNgModel.valid).toBe(true);
expect(groupNgModel.pristine).toBe(true);
expect(groupNgModel.touched).toBe(false);
// After changing the value programmatically, the control should stay pristine
// but remain untouched.
radioInstances[1].checked = true;
fixture.detectChanges();
expect(groupNgModel.valid).toBe(true);
expect(groupNgModel.pristine).toBe(true);
expect(groupNgModel.touched).toBe(false);
// After a user interaction occurs (such as a click), the control should become dirty and
// now also be touched.
radioLabelElements[2].click();
fixture.detectChanges();
expect(groupNgModel.valid).toBe(true);
expect(groupNgModel.pristine).toBe(false);
expect(groupNgModel.touched).toBe(false);
// Blur the input element in order to verify that the ng-touched state has been set to true.
// The touched state should be only set to true after the form control has been blurred.
dispatchFakeEvent(innerRadios[2].nativeElement, 'blur');
expect(groupNgModel.valid).toBe(true);
expect(groupNgModel.pristine).toBe(false);
expect(groupNgModel.touched).toBe(true);
});
it('should write to the radio button based on ngModel', fakeAsync(() => {
testComponent.modelValue = 'chocolate';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(innerRadios[1].nativeElement.checked).toBe(true);
expect(radioInstances[1].checked).toBe(true);
}));
it('should update the ngModel value when selecting a radio button', () => {
dispatchFakeEvent(innerRadios[1].nativeElement, 'change');
fixture.detectChanges();
expect(testComponent.modelValue).toBe('chocolate');
});
it('should update the model before firing change event', () => {
expect(testComponent.modelValue).toBeUndefined();
expect(testComponent.lastEvent).toBeUndefined();
dispatchFakeEvent(innerRadios[1].nativeElement, 'change');
fixture.detectChanges();
expect(testComponent.lastEvent.value).toBe('chocolate');
dispatchFakeEvent(innerRadios[0].nativeElement, 'change');
fixture.detectChanges();
expect(testComponent.lastEvent.value).toBe('vanilla');
});
});
describe('group with FormControl', () => {
it('should toggle the disabled state', () => {
const fixture = TestBed.createComponent(RadioGroupWithFormControl);
fixture.detectChanges();
expect(fixture.componentInstance.group.disabled).toBeFalsy();
fixture.componentInstance.formControl.disable();
fixture.detectChanges();
expect(fixture.componentInstance.group.disabled).toBeTruthy();
fixture.componentInstance.formControl.enable();
fixture.detectChanges();
expect(fixture.componentInstance.group.disabled).toBeFalsy();
});
it('should have a selected button when one matches the initial value', () => {
const fixture = TestBed.createComponent(RadioGroupWithFormControl);
fixture.componentInstance.formControl.setValue('2');
fixture.detectChanges();
expect(fixture.componentInstance.group.selected?.value).toBe('2');
});
});
describe('disableable', () => {
let fixture: ComponentFixture<DisableableRadioButton>;
let radioInstance: MatRadioButton;
let radioNativeElement: HTMLInputElement;
let radioHost: HTMLElement;
let testComponent: DisableableRadioButton;
beforeEach(() => {
fixture = TestBed.createComponent(DisableableRadioButton);
fixture.detectChanges();
testComponent = fixture.debugElement.componentInstance;
const radioDebugElement = fixture.debugElement.query(By.directive(MatRadioButton))!;
radioHost = radioDebugElement.nativeElement;
radioInstance = radioDebugElement.injector.get<MatRadioButton>(MatRadioButton);
radioNativeElement = radioHost.querySelector('input')!;
});
it('should toggle the disabled state', () => {
expect(radioInstance.disabled).toBeFalsy();
expect(radioNativeElement.disabled).toBeFalsy();
testComponent.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(radioInstance.disabled).toBeTruthy();
expect(radioNativeElement.disabled).toBeTruthy();
testComponent.disabled = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(radioInstance.disabled).toBeFalsy();
expect(radioNativeElement.disabled).toBeFalsy();
});
it('should keep the button interactive if disabledInteractive is enabled', () => {
testComponent.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(radioNativeElement.disabled).toBe(true);
expect(radioNativeElement.hasAttribute('aria-disabled')).toBe(false);
expect(radioHost.classList).not.toContain('mat-mdc-radio-disabled-interactive');
testComponent.disabledInteractive = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(radioNativeElement.disabled).toBe(false);
expect(radioNativeElement.getAttribute('aria-disabled')).toBe('true');
expect(radioHost.classList).toContain('mat-mdc-radio-disabled-interactive');
});
});
describe('as standalone', () => {
let fixture: ComponentFixture<StandaloneRadioButtons>;
let radioDebugElements: DebugElement[];
let seasonRadioInstances: MatRadioButton[];
let weatherRadioInstances: MatRadioButton[];
let fruitRadioInstances: MatRadioButton[];
let fruitRadioNativeElements: HTMLElement[];
let fruitRadioNativeInputs: HTMLElement[];
let testComponent: StandaloneRadioButtons;
beforeEach(() => {
fixture = TestBed.createComponent(StandaloneRadioButtons);
fixture.detectChanges();
testComponent = fixture.debugElement.componentInstance;
radioDebugElements = fixture.debugElement.queryAll(By.directive(MatRadioButton));
seasonRadioInstances = radioDebugElements
.filter(debugEl => debugEl.componentInstance.name == 'season')
.map(debugEl => debugEl.componentInstance);
weatherRadioInstances = radioDebugElements
.filter(debugEl => debugEl.componentInstance.name == 'weather')
.map(debugEl => debugEl.componentInstance);
fruitRadioInstances = radioDebugElements
.filter(debugEl => debugEl.componentInstance.name == 'fruit')
.map(debugEl => debugEl.componentInstance);
fruitRadioNativeElements = radioDebugElements
.filter(debugEl => debugEl.componentInstance.name == 'fruit')
.map(debugEl => debugEl.nativeElement);
fruitRadioNativeInputs = [];
for (const element of fruitRadioNativeElements) {
fruitRadioNativeInputs.push(<HTMLElement>element.querySelector('input'));
}
});
it('should uniquely select radios by a name', () => {
seasonRadioInstances[0].checked = true;
weatherRadioInstances[1].checked = true;
fixture.detectChanges();
expect(seasonRadioInstances[0].checked).toBe(true);
expect(seasonRadioInstances[1].checked).toBe(false);
expect(seasonRadioInstances[2].checked).toBe(false);
expect(weatherRadioInstances[0].checked).toBe(false);
expect(weatherRadioInstances[1].checked).toBe(true);
expect(weatherRadioInstances[2].checked).toBe(false);
seasonRadioInstances[1].checked = true;
fixture.detectChanges();
expect(seasonRadioInstances[0].checked).toBe(false);
expect(seasonRadioInstances[1].checked).toBe(true);
expect(seasonRadioInstances[2].checked).toBe(false);
expect(weatherRadioInstances[0].checked).toBe(false);
expect(weatherRadioInstances[1].checked).toBe(true);
expect(weatherRadioInstances[2].checked).toBe(false);
weatherRadioInstances[2].checked = true;
expect(seasonRadioInstances[0].checked).toBe(false);
expect(seasonRadioInstances[1].checked).toBe(true);
expect(seasonRadioInstances[2].checked).toBe(false);
expect(weatherRadioInstances[0].checked).toBe(false);
expect(weatherRadioInstances[1].checked).toBe(false);
expect(weatherRadioInstances[2].checked).toBe(true);
});
it('should add required attribute to the underlying input element if defined', () => {
const radioInstance = seasonRadioInstances[0];
radioInstance.required = true;
fixture.detectChanges();
expect(radioInstance.required).toBe(true);
});
it('should add value attribute to the underlying input element', () => {
expect(fruitRadioNativeInputs[0].getAttribute('value')).toBe('banana');
expect(fruitRadioNativeInputs[1].getAttribute('value')).toBe('raspberry');
});
it('should add aria-label attribute to the underlying input element if defined', () => {
expect(fruitRadioNativeInputs[0].getAttribute('aria-label')).toBe('Banana');
});
it('should not add aria-label attribute if not defined', () => {
expect(fruitRadioNativeInputs[1].hasAttribute('aria-label')).toBeFalsy();
});
it('should change aria-label attribute if property is changed at runtime', () => {
expect(fruitRadioNativeInputs[0].getAttribute('aria-label')).toBe('Banana');
testComponent.ariaLabel = 'Pineapple';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(fruitRadioNativeInputs[0].getAttribute('aria-label')).toBe('Pineapple');
});
it('should add aria-labelledby attribute to the underlying input element if defined', () => {
expect(fruitRadioNativeInputs[0].getAttribute('aria-labelledby')).toBe('xyz');
});
it('should not add aria-labelledby attribute if not defined', () => {
expect(fruitRadioNativeInputs[1].hasAttribute('aria-labelledby')).toBeFalsy();
});
it('should change aria-labelledby attribute if property is changed at runtime', () => {
expect(fruitRadioNativeInputs[0].getAttribute('aria-labelledby')).toBe('xyz');
testComponent.ariaLabelledby = 'uvw';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(fruitRadioNativeInputs[0].getAttribute('aria-labelledby')).toBe('uvw');
});
it('should add aria-describedby attribute to the underlying input element if defined', () => {
expect(fruitRadioNativeInputs[0].getAttribute('aria-describedby')).toBe('abc');
});
it('should not add aria-describedby attribute if not defined', () => {
expect(fruitRadioNativeInputs[1].hasAttribute('aria-describedby')).toBeFalsy();
});
it('should change aria-describedby attribute if property is changed at runtime', () => {
expect(fruitRadioNativeInputs[0].getAttribute('aria-describedby')).toBe('abc');
testComponent.ariaDescribedby = 'uvw';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(fruitRadioNativeInputs[0].getAttribute('aria-describedby')).toBe('uvw');
});
it('should focus on underlying input element when focus() is called', () => {
for (let i = 0; i < fruitRadioInstances.length; i++) {
expect(document.activeElement).not.toBe(fruitRadioNativeInputs[i]);
fruitRadioInstances[i].focus();
fixture.detectChanges();
expect(document.activeElement).toBe(fruitRadioNativeInputs[i]);
}
});
it('should focus on underlying input element when clicking on the touch target', () => {
const input = radioDebugElements[0].nativeElement.querySelector('input');
expect(document.activeElement).not.toBe(input);
radioDebugElements[0].nativeElement.querySelector('.mat-mdc-radio-touch-target').click();
fixture.detectChanges();
expect(document.activeElement).toBe(input);
});
it('should not change focus origin if origin not specified', () => {
fruitRadioInstances[0].focus(undefined, 'mouse');
fruitRadioInstances[1].focus();
expect(fruitRadioNativeElements[1].classList).toContain('cdk-focused');
expect(fruitRadioNativeElements[1].classList).toContain('cdk-mouse-focused');
});
it('should not add the "name" attribute if it is not passed in', () => {
const radio = fixture.debugElement.nativeElement.querySelector('#nameless input');
expect(radio.hasAttribute('name')).toBe(false);
});
it('should default the radio color to `accent`', () => {
expect(seasonRadioInstances.every(radio => radio.color === 'accent')).toBe(true);
});
});
describe('with tabindex', () => {
let fixture: ComponentFixture<FocusableRadioButton>;
beforeEach(() => {
fixture = TestBed.createComponent(FocusableRadioButton);
fixture.detectChanges();
});
it('should forward focus to native input', () => {
let radioButtonEl = fixture.debugElement.query(
By.css('.mat-mdc-radio-button'),
)!.nativeElement;
let inputEl = fixture.debugElement.query(By.css('.mdc-radio__native-control'))!.nativeElement;
radioButtonEl.focus();
// Focus events don't always fire in tests, so we need to fake it.
dispatchFakeEvent(radioButtonEl, 'focus');
fixture.detectChanges();
expect(document.activeElement).toBe(inputEl);
});
it('should allow specifying an explicit tabindex for a single radio-button', () => {
const radioButtonInput = fixture.debugElement.query(By.css('.mat-mdc-radio-button input'))!
.nativeElement as HTMLInputElement;
expect(radioButtonInput.tabIndex)
.withContext('Expected the tabindex to be set to "0" by default.')
.toBe(0);
fixture.componentInstance.tabIndex = 4;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(radioButtonInput.tabIndex)
.withContext('Expected the tabindex to be set to "4".')
.toBe(4);
});
it('should remove the tabindex from the host element', () => {
const predefinedFixture = TestBed.createComponent(RadioButtonWithPredefinedTabindex);
predefinedFixture.detectChanges();
const radioButtonEl = predefinedFixture.debugElement.query(
By.css('.mat-mdc-radio-button'),
)!.nativeElement;
expect(radioButtonEl.hasAttribute('tabindex')).toBe(false);
});
it('should forward a pre-defined tabindex to the underlying input', () => {
const predefinedFixture = TestBed.createComponent(RadioButtonWithPredefinedTabindex);
predefinedFixture.detectChanges();
const radioButtonInput = predefinedFixture.debugElement.query(
By.css('.mat-mdc-radio-button input'),
)!.nativeElement as HTMLInputElement;
expect(radioButtonInput.getAttribute('tabindex')).toBe('5');
});
it('should remove the aria attributes from the host element', () => {
const predefinedFixture = TestBed.createComponent(RadioButtonWithPredefinedAriaAttributes);
predefinedFixture.detectChanges();
const radioButtonEl = predefinedFixture.debugElement.query(
By.css('.mat-mdc-radio-button'),
)!.nativeElement;
expect(radioButtonEl.hasAttribute('aria-label')).toBe(false);
expect(radioButtonEl.hasAttribute('aria-describedby')).toBe(false);
expect(radioButtonEl.hasAttribute('aria-labelledby')).toBe(false);
});
it('should remove the tabindex from the host element when disabled', () => {
const radioButton = fixture.debugElement.query(By.css('.mat-mdc-radio-button')).nativeElement;
fixture.componentInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(radioButton.hasAttribute('tabindex')).toBe(false);
});
});
describe('group interspersed with other tags', () => {
let fixture: ComponentFixture<InterleavedRadioGroup>;
let groupDebugElement: DebugElement;
let groupInstance: MatRadioGroup;
let radioDebugElements: DebugElement[];
let radioInstances: MatRadioButton[];
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(InterleavedRadioGroup);
fixture.detectChanges();
groupDebugElement = fixture.debugElement.query(By.directive(MatRadioGroup))!;
groupInstance = groupDebugElement.injector.get<MatRadioGroup>(MatRadioGroup);
radioDebugElements = fixture.debugElement.queryAll(By.directive(MatRadioButton));
radioInstances = radioDebugElements.map(debugEl => debugEl.componentInstance);
}));
it('should initialize selection of radios based on model value', () => {
expect(groupInstance.selected).toBe(radioInstances[2]);
});
});
it('should preselect a radio button with a static value and an ngIf', () => {
const fixture = TestBed.createComponent(PreselectedRadioWithStaticValueAndNgIf);
fixture.detectChanges();
expect(fixture.componentInstance.preselectedGroup.value).toBe('b');
expect(fixture.componentInstance.preselectedRadio.checked).toBe(true);
});
});
describe('MatRadioDefaultOverrides', () => {
describe('when MAT_RADIO_DEFAULT_OPTIONS overridden', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [MatRadioModule, FormsModule, DefaultRadioButton, RadioButtonWithColorBinding],
providers: [
{
provide: MAT_RADIO_DEFAULT_OPTIONS,
useValue: {color: 'primary'},
},
],
});
}));
it('should override default color in Component', () => {
const fixture: ComponentFixture<DefaultRadioButton> =
TestBed.createComponent(DefaultRadioButton);
fixture.detectChanges();
const radioDebugElement: DebugElement = fixture.debugElement.query(
By.directive(MatRadioButton),
)!;
expect(radioDebugElement.nativeElement.classList).toContain('mat-primary');
});
it('should not override explicit input bindings', () => {
const fixture: ComponentFixture<RadioButtonWithColorBinding> = TestBed.createComponent(
RadioButtonWithColorBinding,
);
fixture.detectChanges();
const radioDebugElement: DebugElement = fixture.debugElement.query(
By.directive(MatRadioButton),
)!;
expect(radioDebugElement.nativeElement.classList).not.toContain('mat-primary');
expect(radioDebugElement.nativeElement.classList).toContain('mat-warn');
});
});
});
@Component({
template: `
<mat-radio-group
[disabled]="isGroupDisabled"
[labelPosition]="labelPos"
[required]="isGroupRequired"
[value]="groupValue"
[disabledInteractive]="isGroupDisabledInteractive"
name="test-name">
@if (isFirstShown) {
<mat-radio-button value="fire" [disableRipple]="disableRipple" [disabled]="isFirstDisabled"
[color]="color">
Charmander
</mat-radio-button>
}
<mat-radio-button value="water" [disableRipple]="disableRipple" [color]="color">
Squirtle
</mat-radio-button>
<mat-radio-button value="leaf" [disableRipple]="disableRipple" [color]="color">
Bulbasaur
</mat-radio-button>
</mat-radio-group>
`,
standalone: true,
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
})
class RadiosInsideRadioGroup {
labelPos: 'before' | 'after';
isFirstDisabled = false;
isGroupDisabled = false;
isGroupRequired = false;
isGroupDisabledInteractive = false;
groupValue: string | null = null;
disableRipple = false;
color: string | null;
isFirstShown = true;
}
@Component({
template: `
<mat-radio-group name="test-name">
<mat-radio-button value="fire">Charmander</mat-radio-button>
<mat-radio-button value="water">Squirtle</mat-radio-button>
<mat-radio-button value="leaf" checked>Bulbasaur</mat-radio-button>
</mat-radio-group>
`,
standalone: true,
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
})
class RadiosInsidePreCheckedRadioGroup {}
@Component({
template: `
<mat-radio-button name="season" value="spring">Spring</mat-radio-button>
<mat-radio-button name="season" value="summer">Summer</mat-radio-button>
<mat-radio-button name="season" value="autum">Autumn</mat-radio-button>
<mat-radio-button name="weather" value="warm">Spring</mat-radio-button>
<mat-radio-button name="weather" value="hot">Summer</mat-radio-button>
<mat-radio-button name="weather" value="cool">Autumn</mat-radio-button>
<span id="xyz">Baby Banana</span>
<span id="abc">A smaller banana</span>
<mat-radio-button name="fruit"
value="banana"
[aria-label]="ariaLabel"
[aria-labelledby]="ariaLabelledby"
[aria-describedby]="ariaDescribedby">
</mat-radio-button>
<mat-radio-button name="fruit" value="raspberry">Raspberry</mat-radio-button>
<mat-radio-button id="nameless" value="no-name">No name</mat-radio-button>
`,
standalone: true,
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
})
class StandaloneRadioButtons {
ariaLabel: string = 'Banana';
ariaLabelledby: string = 'xyz';
ariaDescribedby: string = 'abc';
}
@Component({
template: `
<mat-radio-group [name]="groupName" [(ngModel)]="modelValue" (change)="lastEvent = $event">
@for (option of options; track option) {
<mat-radio-button [value]="option.value">{{option.label}}</mat-radio-button>
}
</mat-radio-group>
`,
standalone: true,
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
})
class RadioGroupWithNgModel {
modelValue: string;
groupName = 'radio-group';
options = [
{label: 'Vanilla', value: 'vanilla'},
{label: 'Chocolate', value: 'chocolate'},
{label: 'Strawberry', value: 'strawberry'},
];
lastEvent: MatRadioChange;
}
@Component({
template: `
<mat-radio-button
[disabled]="disabled"
[disabledInteractive]="disabledInteractive">One</mat-radio-button>`,
standalone: true,
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
})
class DisableableRadioButton {
disabled = false;
disabledInteractive = false;
@ViewChild(MatRadioButton) matRadioButton: MatRadioButton;
}
@Component({
template: `
<mat-radio-group [formControl]="formControl">
<mat-radio-button value="1">One</mat-radio-button>
<mat-radio-button value="2">Two</mat-radio-button>
</mat-radio-group>
`,
standalone: true,
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
})
class RadioGroupWithFormControl {
@ViewChild(MatRadioGroup) group: MatRadioGroup;
formControl = new FormControl('');
}
@Component({
template: `<mat-radio-button [disabled]="disabled" [tabIndex]="tabIndex"></mat-radio-button>`,
standalone: true,
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
})
class FocusableRadioButton {
tabIndex: number;
disabled = false;
}
@Component({
selector: 'transcluding-wrapper',
template: `
<div><ng-content></ng-content></div>
`,
standalone: true,
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
})
class TranscludingWrapper {}
@Component({
template: `
<mat-radio-group name="group" [(ngModel)]="modelValue">
@for (option of options; track option) {
<transcluding-wrapper>
<mat-radio-button [value]="option.value">{{option.label}}</mat-radio-button>
</transcluding-wrapper>
}
</mat-radio-group>
`,
standalone: true,
imports: [MatRadioModule, FormsModule, ReactiveFormsModule, TranscludingWrapper],
})
class InterleavedRadioGroup {
modelValue = 'strawberry';
options = [
{label: 'Vanilla', value: 'vanilla'},
{label: 'Chocolate', value: 'chocolate'},
{label: 'Strawberry', value: 'strawberry'},
];
}
@Component({
template: `<mat-radio-button tabindex="5"></mat-radio-button>`,
standalone: true,
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
})
class RadioButtonWithPredefinedTabindex {}
@Component({
template: `<mat-radio-button></mat-radio-button>`,
standalone: true,
imports: [MatRadioModule, FormsModule],
})
class DefaultRadioButton {}
@Component({
template: `<mat-radio-button color="warn"></mat-radio-button>`,
standalone: true,
imports: [MatRadioModule, FormsModule],
})
class RadioButtonWithColorBinding {}
@Component({
template: `
<mat-radio-button
aria-label="Radio button"
aria-describedby="something"
aria-labelledby="something-else"></mat-radio-button>`,
standalone: true,
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
})
class RadioButtonWithPredefinedAriaAttributes {}
@Component({
// Note that this is somewhat of a contrived template, but it is required to
// reproduce the issue. It was taken for a specific user report at #25831.
template: `
@if (true) {
<mat-radio-group [formControl]="controls.predecessor">
<mat-radio-button value="predecessor"></mat-radio-button>
</mat-radio-group>
}
<mat-radio-group [formControl]="controls.target" #preselectedGroup>
<mat-radio-button value="a"></mat-radio-button>
@if (true) {
<mat-radio-button value="b" #preselectedRadio></mat-radio-button>
}
</mat-radio-group>
`,
standalone: true,
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
})
class PreselectedRadioWithStaticValueAndNgIf {
@ViewChild('preselectedGroup', {read: MatRadioGroup}) preselectedGroup: MatRadioGroup;
@ViewChild('preselectedRadio', {read: MatRadioButton}) preselectedRadio: MatRadioButton;
controls = {
predecessor: new FormControl('predecessor'),
target: new FormControl('b'),
};
}