import {dispatchMouseEvent} from '@angular/cdk/testing/private'; import {Component, DebugElement, QueryList, ViewChild, ViewChildren} from '@angular/core'; import {ComponentFixture, TestBed, fakeAsync, flush, tick} from '@angular/core/testing'; import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms'; import {By} from '@angular/platform-browser'; import { MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS, MatButtonToggle, MatButtonToggleChange, MatButtonToggleGroup, MatButtonToggleModule, } from './index'; describe('MatButtonToggle with forms', () => { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ imports: [ MatButtonToggleModule, FormsModule, ReactiveFormsModule, ButtonToggleGroupWithNgModel, ButtonToggleGroupWithFormControl, ButtonToggleGroupWithIndirectDescendantToggles, ButtonToggleGroupWithFormControlAndDynamicButtons, ], }); })); describe('using FormControl', () => { let fixture: ComponentFixture; let groupDebugElement: DebugElement; let groupInstance: MatButtonToggleGroup; let testComponent: ButtonToggleGroupWithFormControl; beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(ButtonToggleGroupWithFormControl); fixture.detectChanges(); testComponent = fixture.debugElement.componentInstance; groupDebugElement = fixture.debugElement.query(By.directive(MatButtonToggleGroup))!; groupInstance = groupDebugElement.injector.get(MatButtonToggleGroup); })); it('should toggle the disabled state', () => { testComponent.control.disable(); expect(groupInstance.disabled).toBe(true); testComponent.control.enable(); expect(groupInstance.disabled).toBe(false); }); it('should set the value', () => { testComponent.control.setValue('green'); expect(groupInstance.value).toBe('green'); testComponent.control.setValue('red'); expect(groupInstance.value).toBe('red'); }); it('should register the on change callback', () => { const spy = jasmine.createSpy('onChange callback'); testComponent.control.registerOnChange(spy); testComponent.control.setValue('blue'); expect(spy).toHaveBeenCalled(); }); }); describe('button toggle group with ngModel and change event', () => { let fixture: ComponentFixture; let groupDebugElement: DebugElement; let buttonToggleDebugElements: DebugElement[]; let groupInstance: MatButtonToggleGroup; let buttonToggleInstances: MatButtonToggle[]; let testComponent: ButtonToggleGroupWithNgModel; let groupNgModel: NgModel; let innerButtons: HTMLElement[]; beforeEach(() => { fixture = TestBed.createComponent(ButtonToggleGroupWithNgModel); fixture.detectChanges(); testComponent = fixture.debugElement.componentInstance; groupDebugElement = fixture.debugElement.query(By.directive(MatButtonToggleGroup))!; groupInstance = groupDebugElement.injector.get(MatButtonToggleGroup); groupNgModel = groupDebugElement.injector.get(NgModel); buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MatButtonToggle)); buttonToggleInstances = buttonToggleDebugElements.map(debugEl => debugEl.componentInstance); innerButtons = buttonToggleDebugElements.map( debugEl => debugEl.query(By.css('button'))!.nativeElement, ); }); it('should update the model before firing change event', fakeAsync(() => { expect(testComponent.modelValue).toBeUndefined(); expect(testComponent.lastEvent).toBeUndefined(); innerButtons[0].click(); fixture.detectChanges(); tick(); expect(testComponent.modelValue).toBe('red'); expect(testComponent.lastEvent.value).toBe('red'); })); it('should set individual radio names based on the group name', () => { expect(groupInstance.name).toBeTruthy(); for (let buttonToggle of innerButtons) { expect(buttonToggle.getAttribute('name')).toBe(groupInstance.name); } groupInstance.name = 'new name'; fixture.detectChanges(); for (let buttonToggle of innerButtons) { expect(buttonToggle.getAttribute('name')).toBe(groupInstance.name); } }); it('should update the name of radio DOM elements if the name of the group changes', () => { expect(innerButtons.every(button => button.getAttribute('name') === groupInstance.name)) .withContext('Expected all buttons to have the initial name.') .toBe(true); fixture.componentInstance.groupName = 'changed-name'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(groupInstance.name).toBe('changed-name'); expect(innerButtons.every(button => button.getAttribute('name') === groupInstance.name)) .withContext('Expected all buttons to have the new name.') .toBe(true); }); it('should set the name of the buttons to the group name if the name of the button changes after init', () => { const firstButton = innerButtons[0]; expect(firstButton.getAttribute('name')).toBe(fixture.componentInstance.groupName); fixture.componentInstance.options[0].name = 'changed-name'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(firstButton.getAttribute('name')).toBe(fixture.componentInstance.groupName); }); it('should check the corresponding button toggle on a group value change', () => { expect(groupInstance.value).toBeFalsy(); for (let buttonToggle of buttonToggleInstances) { expect(buttonToggle.checked).toBeFalsy(); } groupInstance.value = 'red'; for (let buttonToggle of buttonToggleInstances) { expect(buttonToggle.checked).toBe(groupInstance.value === buttonToggle.value); } const selected = groupInstance.selected as MatButtonToggle; expect(selected.value).toBe(groupInstance.value); }); it('should have the correct FormControl state initially and after interaction', fakeAsync(() => { expect(groupNgModel.valid).toBe(true); expect(groupNgModel.pristine).toBe(true); expect(groupNgModel.touched).toBe(false); buttonToggleInstances[1].checked = true; fixture.detectChanges(); tick(); expect(groupNgModel.valid).toBe(true); expect(groupNgModel.pristine).toBe(true); expect(groupNgModel.touched).toBe(false); innerButtons[2].click(); fixture.detectChanges(); tick(); expect(groupNgModel.valid).toBe(true); expect(groupNgModel.pristine).toBe(false); expect(groupNgModel.touched).toBe(true); })); it('should update the ngModel value when selecting a button toggle', fakeAsync(() => { innerButtons[1].click(); fixture.detectChanges(); tick(); expect(testComponent.modelValue).toBe('green'); })); it('should show a ripple on label click', () => { const groupElement = groupDebugElement.nativeElement; expect(groupElement.querySelectorAll('.mat-ripple-element').length).toBe(0); dispatchMouseEvent(innerButtons[0], 'mousedown'); dispatchMouseEvent(innerButtons[0], 'mouseup'); expect(groupElement.querySelectorAll('.mat-ripple-element').length).toBe(1); }); it('should allow ripples to be disabled', () => { const groupElement = groupDebugElement.nativeElement; testComponent.disableRipple = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(groupElement.querySelectorAll('.mat-ripple-element').length).toBe(0); dispatchMouseEvent(innerButtons[0], 'mousedown'); dispatchMouseEvent(innerButtons[0], 'mouseup'); expect(groupElement.querySelectorAll('.mat-ripple-element').length).toBe(0); }); it( 'should maintain the selected value when swapping out the list of toggles with one ' + 'that still contains the value', fakeAsync(() => { expect(buttonToggleInstances[0].checked).toBe(false); expect(fixture.componentInstance.modelValue).toBeFalsy(); expect(groupInstance.value).toBeFalsy(); groupInstance.value = 'red'; fixture.detectChanges(); expect(buttonToggleInstances[0].checked).toBe(true); expect(groupInstance.value).toBe('red'); fixture.componentInstance.options = [...fixture.componentInstance.options]; fixture.detectChanges(); tick(); fixture.detectChanges(); buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MatButtonToggle)); buttonToggleInstances = buttonToggleDebugElements.map(debugEl => debugEl.componentInstance); expect(buttonToggleInstances[0].checked).toBe(true); expect(groupInstance.value).toBe('red'); }), ); }); it('should be able to pick up toggles that are not direct descendants', fakeAsync(() => { const fixture = TestBed.createComponent(ButtonToggleGroupWithIndirectDescendantToggles); fixture.detectChanges(); const button = fixture.nativeElement.querySelector('.mat-button-toggle button'); const groupDebugElement = fixture.debugElement.query(By.directive(MatButtonToggleGroup))!; const groupInstance = groupDebugElement.injector.get(MatButtonToggleGroup); button.click(); fixture.detectChanges(); tick(); expect(groupInstance.value).toBe('red'); expect(fixture.componentInstance.control.value).toBe('red'); expect(groupInstance._buttonToggles.length).toBe(3); })); it('should preserve the selection if the pre-selected option is removed and re-added', () => { const fixture = TestBed.createComponent(ButtonToggleGroupWithFormControlAndDynamicButtons); const instance = fixture.componentInstance; instance.control.setValue('a'); fixture.detectChanges(); const buttons = fixture.nativeElement.querySelectorAll('.mat-button-toggle-button'); expect(instance.toggles.map(t => t.checked)).toEqual([true, false, false]); buttons[2].click(); fixture.detectChanges(); instance.values.shift(); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(instance.toggles.map(t => t.checked)).toEqual([false, true]); instance.values.unshift('a'); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(instance.toggles.map(t => t.checked)).toEqual([false, false, true]); }); it('should preserve the pre-selected option if it is removed and re-added', () => { const fixture = TestBed.createComponent(ButtonToggleGroupWithFormControlAndDynamicButtons); const instance = fixture.componentInstance; instance.control.setValue('a'); fixture.detectChanges(); expect(instance.toggles.map(t => t.checked)).toEqual([true, false, false]); instance.values.shift(); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(instance.toggles.map(t => t.checked)).toEqual([false, false]); instance.values.unshift('a'); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(instance.toggles.map(t => t.checked)).toEqual([true, false, false]); }); it('should set the initial tabindex when using ngModel with a static list of options where none match the value', fakeAsync(() => { const fixture = TestBed.createComponent(ButtonToggleGroupWithNgModelAndStaticOptions); fixture.detectChanges(); tick(); const indexes = Array.from( fixture.nativeElement.querySelectorAll('button'), (button: HTMLElement) => button.getAttribute('tabindex'), ); expect(indexes).toEqual(['0', '-1', '-1']); })); }); describe('MatButtonToggle without forms', () => { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ imports: [ MatButtonToggleModule, ButtonTogglesInsideButtonToggleGroup, ButtonTogglesInsideButtonToggleGroupMultiple, FalsyButtonTogglesInsideButtonToggleGroupMultiple, ButtonToggleGroupWithInitialValue, StandaloneButtonToggle, ButtonToggleWithAriaLabel, ButtonToggleWithAriaLabelledby, RepeatedButtonTogglesWithPreselectedValue, ButtonToggleWithTabindex, ButtonToggleWithStaticName, ButtonToggleWithStaticChecked, ButtonToggleWithStaticAriaAttributes, ], }); })); describe('inside of an exclusive selection group', () => { let fixture: ComponentFixture; let groupDebugElement: DebugElement; let groupNativeElement: HTMLElement; let buttonToggleDebugElements: DebugElement[]; let buttonToggleNativeElements: HTMLElement[]; let innerButtons: HTMLLabelElement[]; let groupInstance: MatButtonToggleGroup; let buttonToggleInstances: MatButtonToggle[]; let testComponent: ButtonTogglesInsideButtonToggleGroup; beforeEach(() => { fixture = TestBed.createComponent(ButtonTogglesInsideButtonToggleGroup); fixture.detectChanges(); testComponent = fixture.debugElement.componentInstance; groupDebugElement = fixture.debugElement.query(By.directive(MatButtonToggleGroup))!; groupNativeElement = groupDebugElement.nativeElement; groupInstance = groupDebugElement.injector.get(MatButtonToggleGroup); buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MatButtonToggle)); buttonToggleNativeElements = buttonToggleDebugElements.map(debugEl => debugEl.nativeElement); innerButtons = fixture.debugElement .queryAll(By.css('button')) .map(debugEl => debugEl.nativeElement); buttonToggleInstances = buttonToggleDebugElements.map(debugEl => debugEl.componentInstance); }); it('should initialize the tab index correctly', () => { expect(innerButtons.map(b => b.getAttribute('tabindex'))).toEqual(['0', '-1', '-1']); }); it('should update the tab index correctly', () => { innerButtons[1].click(); fixture.detectChanges(); expect(innerButtons[0].getAttribute('tabindex')).toBe('-1'); expect(innerButtons[1].getAttribute('tabindex')).toBe('0'); }); it('should set individual button toggle names based on the group name', () => { expect(groupInstance.name).toBeTruthy(); for (let buttonToggle of innerButtons) { expect(buttonToggle.getAttribute('name')).toBe(groupInstance.name); } }); it('should disable click interactions when the group is disabled', () => { testComponent.isGroupDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); buttonToggleNativeElements[0].click(); expect(buttonToggleInstances[0].checked).toBe(false); expect(buttonToggleInstances[0].disabled).toBe(true); testComponent.isGroupDisabled = false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(buttonToggleInstances[0].disabled).toBe(false); innerButtons[0].click(); fixture.detectChanges(); expect(buttonToggleInstances[0].checked).toBe(true); }); it('should set aria-disabled based on whether the group is disabled', () => { expect(groupNativeElement.getAttribute('aria-disabled')).toBe('false'); testComponent.isGroupDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(groupNativeElement.getAttribute('aria-disabled')).toBe('true'); }); it('should disable the underlying button when the group is disabled', () => { const buttons = buttonToggleNativeElements.map(toggle => toggle.querySelector('button')!); expect(buttons.every(input => input.disabled)).toBe(false); testComponent.isGroupDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(buttons.every(input => input.disabled)).toBe(true); }); it('should be able to keep the button interactive while disabled', () => { const button = buttonToggleNativeElements[0].querySelector('button')!; testComponent.isGroupDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(button.hasAttribute('disabled')).toBe(true); expect(button.hasAttribute('aria-disabled')).toBe(false); expect(button.getAttribute('tabindex')).toBe('-1'); testComponent.disabledIntearctive = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(button.hasAttribute('disabled')).toBe(false); expect(button.getAttribute('aria-disabled')).toBe('true'); expect(button.getAttribute('tabindex')).toBe('0'); }); it('should update the group value when one of the toggles changes', () => { expect(groupInstance.value).toBeFalsy(); innerButtons[0].click(); fixture.detectChanges(); expect(groupInstance.value).toBe('test1'); expect(groupInstance.selected).toBe(buttonToggleInstances[0]); }); it('should propagate the value change back up via a two-way binding', () => { expect(groupInstance.value).toBeFalsy(); innerButtons[0].click(); fixture.detectChanges(); expect(groupInstance.value).toBe('test1'); expect(testComponent.groupValue).toBe('test1'); }); it('should update the group and toggles when one of the button toggles is clicked', () => { expect(groupInstance.value).toBeFalsy(); innerButtons[0].click(); fixture.detectChanges(); expect(groupInstance.value).toBe('test1'); expect(groupInstance.selected).toBe(buttonToggleInstances[0]); expect(buttonToggleInstances[0].checked).toBe(true); expect(buttonToggleInstances[1].checked).toBe(false); innerButtons[1].click(); fixture.detectChanges(); expect(groupInstance.value).toBe('test2'); expect(groupInstance.selected).toBe(buttonToggleInstances[1]); expect(buttonToggleInstances[0].checked).toBe(false); expect(buttonToggleInstances[1].checked).toBe(true); }); it('should check a button toggle upon interaction with underlying native radio button', () => { innerButtons[0].click(); fixture.detectChanges(); expect(buttonToggleInstances[0].checked).toBe(true); expect(groupInstance.value).toBe('test1'); }); it('should change the vertical state', () => { expect(groupNativeElement.classList).not.toContain('mat-button-toggle-vertical'); groupInstance.vertical = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(groupNativeElement.classList).toContain('mat-button-toggle-vertical'); }); it('should emit a change event from button toggles', fakeAsync(() => { expect(buttonToggleInstances[0].checked).toBe(false); const changeSpy = jasmine.createSpy('button-toggle change listener'); buttonToggleInstances[0].change.subscribe(changeSpy); innerButtons[0].click(); fixture.detectChanges(); tick(); expect(changeSpy).toHaveBeenCalledTimes(1); innerButtons[0].click(); fixture.detectChanges(); tick(); // Always emit change event when button toggle is clicked expect(changeSpy).toHaveBeenCalledTimes(2); })); it('should emit a change event from the button toggle group', fakeAsync(() => { expect(groupInstance.value).toBeFalsy(); const changeSpy = jasmine.createSpy('button-toggle-group change listener'); groupInstance.change.subscribe(changeSpy); innerButtons[0].click(); fixture.detectChanges(); tick(); expect(changeSpy).toHaveBeenCalled(); innerButtons[1].click(); fixture.detectChanges(); tick(); expect(changeSpy).toHaveBeenCalledTimes(2); })); it('should update the group and button toggles when updating the group value', () => { expect(groupInstance.value).toBeFalsy(); testComponent.groupValue = 'test1'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(groupInstance.value).toBe('test1'); expect(groupInstance.selected).toBe(buttonToggleInstances[0]); expect(buttonToggleInstances[0].checked).toBe(true); expect(buttonToggleInstances[1].checked).toBe(false); testComponent.groupValue = 'test2'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(groupInstance.value).toBe('test2'); expect(groupInstance.selected).toBe(buttonToggleInstances[1]); expect(buttonToggleInstances[0].checked).toBe(false); expect(buttonToggleInstances[1].checked).toBe(true); }); it('should deselect all of the checkboxes when the group value is cleared', () => { buttonToggleInstances[0].checked = true; expect(groupInstance.value).toBeTruthy(); groupInstance.value = null; expect(buttonToggleInstances.every(toggle => !toggle.checked)).toBe(true); }); it('should update the model if a selected toggle is removed', fakeAsync(() => { expect(groupInstance.value).toBeFalsy(); innerButtons[0].click(); fixture.detectChanges(); expect(groupInstance.value).toBe('test1'); expect(groupInstance.selected).toBe(buttonToggleInstances[0]); testComponent.renderFirstToggle = false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(); expect(groupInstance.value).toBeFalsy(); expect(groupInstance.selected).toBeFalsy(); })); it('should show checkmark indicator by default', () => { innerButtons[0].click(); fixture.detectChanges(); expect( fixture.nativeElement.querySelectorAll( '.mat-button-toggle-checked .mat-button-toggle-checkbox-wrapper', ).length, ).toBe(1); }); }); describe('with initial value and change event', () => { it('should not fire an initial change event', () => { const fixture = TestBed.createComponent(ButtonToggleGroupWithInitialValue); const testComponent = fixture.debugElement.componentInstance; const groupDebugElement = fixture.debugElement.query(By.directive(MatButtonToggleGroup))!; const groupInstance: MatButtonToggleGroup = groupDebugElement.injector.get(MatButtonToggleGroup); fixture.detectChanges(); // Note that we cast to a boolean, because the event has some circular references // which will crash the runner when Jasmine attempts to stringify them. expect(!!testComponent.lastEvent).toBe(false); expect(groupInstance.value).toBe('red'); groupInstance.value = 'green'; fixture.detectChanges(); expect(!!testComponent.lastEvent).toBe(false); expect(groupInstance.value).toBe('green'); }); }); describe('inside of a multiple selection group', () => { let fixture: ComponentFixture; let groupDebugElement: DebugElement; let groupNativeElement: HTMLElement; let buttonToggleDebugElements: DebugElement[]; let buttonToggleNativeElements: HTMLElement[]; let buttonToggleLabelElements: HTMLLabelElement[]; let groupInstance: MatButtonToggleGroup; let buttonToggleInstances: MatButtonToggle[]; let testComponent: ButtonTogglesInsideButtonToggleGroupMultiple; beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(ButtonTogglesInsideButtonToggleGroupMultiple); fixture.detectChanges(); testComponent = fixture.debugElement.componentInstance; groupDebugElement = fixture.debugElement.query(By.directive(MatButtonToggleGroup))!; groupNativeElement = groupDebugElement.nativeElement; groupInstance = groupDebugElement.injector.get(MatButtonToggleGroup); buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MatButtonToggle)); buttonToggleNativeElements = buttonToggleDebugElements.map(debugEl => debugEl.nativeElement); buttonToggleLabelElements = fixture.debugElement .queryAll(By.css('button')) .map(debugEl => debugEl.nativeElement); buttonToggleInstances = buttonToggleDebugElements.map(debugEl => debugEl.componentInstance); })); it('should disable click interactions when the group is disabled', () => { testComponent.isGroupDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); buttonToggleNativeElements[0].click(); expect(buttonToggleInstances[0].checked).toBe(false); }); it('should check a button toggle when clicked', () => { expect(buttonToggleInstances.every(buttonToggle => !buttonToggle.checked)).toBe(true); const nativeCheckboxLabel = buttonToggleDebugElements[0].query( By.css('button'), )!.nativeElement; nativeCheckboxLabel.click(); expect(groupInstance.value).toEqual(['eggs']); expect(buttonToggleInstances[0].checked).toBe(true); }); it('should allow for multiple toggles to be selected', () => { buttonToggleInstances[0].checked = true; fixture.detectChanges(); expect(groupInstance.value).toEqual(['eggs']); expect(buttonToggleInstances[0].checked).toBe(true); buttonToggleInstances[1].checked = true; fixture.detectChanges(); expect(groupInstance.value).toEqual(['eggs', 'flour']); expect(buttonToggleInstances[1].checked).toBe(true); expect(buttonToggleInstances[0].checked).toBe(true); }); it('should check a button toggle upon interaction with underlying native checkbox', () => { const nativeCheckboxButton = buttonToggleDebugElements[0].query( By.css('button'), )!.nativeElement; nativeCheckboxButton.click(); fixture.detectChanges(); expect(groupInstance.value).toEqual(['eggs']); expect(buttonToggleInstances[0].checked).toBe(true); }); it('should change the vertical state', () => { expect(groupNativeElement.classList).not.toContain('mat-button-toggle-vertical'); groupInstance.vertical = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(groupNativeElement.classList).toContain('mat-button-toggle-vertical'); }); it('should deselect a button toggle when selected twice', fakeAsync(() => { buttonToggleLabelElements[0].click(); fixture.detectChanges(); tick(); expect(buttonToggleInstances[0].checked).toBe(true); expect(groupInstance.value).toEqual(['eggs']); buttonToggleLabelElements[0].click(); fixture.detectChanges(); tick(); expect(groupInstance.value).toEqual([]); expect(buttonToggleInstances[0].checked).toBe(false); })); it('should emit a change event for state changes', fakeAsync(() => { expect(buttonToggleInstances[0].checked).toBe(false); const changeSpy = jasmine.createSpy('button-toggle change listener'); buttonToggleInstances[0].change.subscribe(changeSpy); buttonToggleLabelElements[0].click(); fixture.detectChanges(); tick(); expect(changeSpy).toHaveBeenCalled(); expect(groupInstance.value).toEqual(['eggs']); buttonToggleLabelElements[0].click(); fixture.detectChanges(); tick(); expect(groupInstance.value).toEqual([]); // The default browser behavior is to emit an event, when the value was set // to false. That's because the current input type is set to `checkbox` when // using the multiple mode. expect(changeSpy).toHaveBeenCalledTimes(2); })); it('should throw when attempting to assign a non-array value', () => { expect(() => { groupInstance.value = 'not-an-array'; }).toThrowError(/Value must be an array/); }); it('should show checkmark indicator by default', () => { buttonToggleLabelElements[0].click(); buttonToggleLabelElements[1].click(); fixture.detectChanges(); expect( fixture.nativeElement.querySelectorAll( '.mat-button-toggle-checked .mat-button-toggle-checkbox-wrapper', ).length, ).toBe(2); }); }); describe('as standalone', () => { let fixture: ComponentFixture; let buttonToggleDebugElement: DebugElement; let buttonToggleNativeElement: HTMLElement; let buttonToggleLabelElement: HTMLLabelElement; let buttonToggleInstance: MatButtonToggle; let buttonToggleButtonElement: HTMLButtonElement; beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(StandaloneButtonToggle); fixture.detectChanges(); buttonToggleDebugElement = fixture.debugElement.query(By.directive(MatButtonToggle))!; buttonToggleNativeElement = buttonToggleDebugElement.nativeElement; buttonToggleLabelElement = fixture.debugElement.query( By.css('.mat-button-toggle-label-content'), )!.nativeElement; buttonToggleInstance = buttonToggleDebugElement.componentInstance; buttonToggleButtonElement = buttonToggleNativeElement.querySelector( 'button', )! as HTMLButtonElement; })); it('should toggle when clicked', fakeAsync(() => { buttonToggleLabelElement.click(); fixture.detectChanges(); flush(); expect(buttonToggleInstance.checked).toBe(true); buttonToggleLabelElement.click(); fixture.detectChanges(); flush(); expect(buttonToggleInstance.checked).toBe(false); })); it('should emit a change event for state changes', fakeAsync(() => { expect(buttonToggleInstance.checked).toBe(false); const changeSpy = jasmine.createSpy('button-toggle change listener'); buttonToggleInstance.change.subscribe(changeSpy); buttonToggleLabelElement.click(); fixture.detectChanges(); tick(); expect(changeSpy).toHaveBeenCalled(); buttonToggleLabelElement.click(); fixture.detectChanges(); tick(); // The default browser behavior is to emit an event, when the value was set // to false. That's because the current input type is set to `checkbox`. expect(changeSpy).toHaveBeenCalledTimes(2); })); it('should focus on underlying input element when focus() is called', () => { const nativeButton = buttonToggleDebugElement.query(By.css('button'))!.nativeElement; expect(document.activeElement).not.toBe(nativeButton); buttonToggleInstance.focus(); fixture.detectChanges(); expect(document.activeElement).toBe(nativeButton); }); it('should not assign a name to the underlying input if one is not passed in', () => { expect(buttonToggleButtonElement.getAttribute('name')).toBeFalsy(); }); it('should have correct aria-pressed attribute', () => { expect(buttonToggleButtonElement.getAttribute('aria-pressed')).toBe('false'); buttonToggleLabelElement.click(); fixture.detectChanges(); expect(buttonToggleButtonElement.getAttribute('aria-pressed')).toBe('true'); }); }); describe('aria-label handling ', () => { it('should not set the aria-label attribute if none is provided', () => { const fixture = TestBed.createComponent(StandaloneButtonToggle); const checkboxDebugElement = fixture.debugElement.query(By.directive(MatButtonToggle))!; const checkboxNativeElement = checkboxDebugElement.nativeElement; const buttonElement = checkboxNativeElement.querySelector('button') as HTMLButtonElement; fixture.detectChanges(); expect(buttonElement.hasAttribute('aria-label')).toBe(false); }); it('should use the provided aria-label', () => { const fixture = TestBed.createComponent(ButtonToggleWithAriaLabel); const checkboxDebugElement = fixture.debugElement.query(By.directive(MatButtonToggle))!; const checkboxNativeElement = checkboxDebugElement.nativeElement; const buttonElement = checkboxNativeElement.querySelector('button') as HTMLButtonElement; fixture.detectChanges(); expect(buttonElement.getAttribute('aria-label')).toBe('Super effective'); }); it('should clear the static aria from the host node', () => { const fixture = TestBed.createComponent(ButtonToggleWithStaticAriaAttributes); fixture.detectChanges(); const hostNode: HTMLElement = fixture.nativeElement.querySelector('mat-button-toggle'); expect(hostNode.hasAttribute('aria-label')).toBe(false); expect(hostNode.hasAttribute('aria-labelledby')).toBe(false); }); }); describe('with provided aria-labelledby ', () => { let checkboxDebugElement: DebugElement; let checkboxNativeElement: HTMLElement; let buttonElement: HTMLButtonElement; it('should use the provided aria-labelledby', () => { const fixture = TestBed.createComponent(ButtonToggleWithAriaLabelledby); checkboxDebugElement = fixture.debugElement.query(By.directive(MatButtonToggle))!; checkboxNativeElement = checkboxDebugElement.nativeElement; buttonElement = checkboxNativeElement.querySelector('button') as HTMLButtonElement; fixture.detectChanges(); expect(buttonElement.getAttribute('aria-labelledby')).toBe('some-id'); }); it('should not assign aria-labelledby if none is provided', () => { const fixture = TestBed.createComponent(StandaloneButtonToggle); checkboxDebugElement = fixture.debugElement.query(By.directive(MatButtonToggle))!; checkboxNativeElement = checkboxDebugElement.nativeElement; buttonElement = checkboxNativeElement.querySelector('button') as HTMLButtonElement; fixture.detectChanges(); expect(buttonElement.getAttribute('aria-labelledby')).toBe(null); }); }); describe('with tabindex', () => { it('should forward the tabindex to the underlying button', () => { const fixture = TestBed.createComponent(ButtonToggleWithTabindex); fixture.detectChanges(); const button = fixture.nativeElement.querySelector('.mat-button-toggle button'); expect(button.getAttribute('tabindex')).toBe('3'); }); it('should have role "presentation"', () => { const fixture = TestBed.createComponent(ButtonToggleWithTabindex); fixture.detectChanges(); const host = fixture.nativeElement.querySelector('.mat-button-toggle'); expect(host.getAttribute('role')).toBe('presentation'); }); it('should forward focus to the underlying button when the host is focused', () => { const fixture = TestBed.createComponent(ButtonToggleWithTabindex); fixture.detectChanges(); const host = fixture.nativeElement.querySelector('.mat-button-toggle'); const button = host.querySelector('button'); expect(document.activeElement).not.toBe(button); host.focus(); expect(document.activeElement).toBe(button); }); }); describe('with tokens to hide checkmark selection indicators', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ MatButtonToggleModule, ButtonTogglesInsideButtonToggleGroup, ButtonTogglesInsideButtonToggleGroupMultiple, ], providers: [ { provide: MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS, useValue: { hideSingleSelectionIndicator: true, hideMultipleSelectionIndicator: true, }, }, ], }); }); it('should hide checkmark indicator for single selection', () => { const fixture = TestBed.createComponent(ButtonTogglesInsideButtonToggleGroup); fixture.detectChanges(); fixture.debugElement.query(By.css('button')).nativeElement.click(); fixture.detectChanges(); expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(0); }); it('should hide checkmark indicator for multiple selection', () => { const fixture = TestBed.createComponent(ButtonTogglesInsideButtonToggleGroupMultiple); fixture.detectChanges(); // Check all button toggles in the group fixture.debugElement .queryAll(By.css('button')) .forEach(toggleButton => toggleButton.nativeElement.click()); fixture.detectChanges(); expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(0); }); }); it('should not throw on init when toggles are repeated and there is an initial value', () => { const fixture = TestBed.createComponent(RepeatedButtonTogglesWithPreselectedValue); expect(() => fixture.detectChanges()).not.toThrow(); expect(fixture.componentInstance.toggleGroup.value).toBe('Two'); expect(fixture.componentInstance.toggles.toArray()[1].checked).toBe(true); }); it('should not throw on init when toggles are repeated and there is an initial value', () => { const fixture = TestBed.createComponent(ButtonToggleWithStaticName); fixture.detectChanges(); const hostNode: HTMLElement = fixture.nativeElement.querySelector('.mat-button-toggle'); expect(hostNode.hasAttribute('name')).toBe(false); expect(hostNode.querySelector('button')!.getAttribute('name')).toBe('custom-name'); }); it( 'should maintain the selected state when the value and toggles are swapped out at ' + 'the same time', () => { const fixture = TestBed.createComponent(RepeatedButtonTogglesWithPreselectedValue); fixture.detectChanges(); expect(fixture.componentInstance.toggleGroup.value).toBe('Two'); expect(fixture.componentInstance.toggles.toArray()[1].checked).toBe(true); fixture.componentInstance.possibleValues = ['Five', 'Six', 'Seven']; fixture.componentInstance.value = 'Seven'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.toggleGroup.value).toBe('Seven'); expect(fixture.componentInstance.toggles.toArray()[2].checked).toBe(true); }, ); it('should select falsy button toggle value in multiple selection', () => { const fixture = TestBed.createComponent(FalsyButtonTogglesInsideButtonToggleGroupMultiple); fixture.detectChanges(); expect(fixture.componentInstance.toggles.toArray()[0].checked).toBe(true); expect(fixture.componentInstance.toggles.toArray()[1].checked).toBe(false); expect(fixture.componentInstance.toggles.toArray()[2].checked).toBe(false); fixture.componentInstance.value = [0, false]; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.toggles.toArray()[0].checked).toBe(true); expect(fixture.componentInstance.toggles.toArray()[1].checked).toBe(false); expect(fixture.componentInstance.toggles.toArray()[2].checked).toBe(true); }); it('should not throw if initial value is set during creation', () => { const fixture = TestBed.createComponent(ButtonTogglesInsideButtonToggleGroupMultiple); // In Ivy static inputs are set during creation. We simulate this by not calling // `fixture.detectChanges` immediately, but getting a hold of the instance via the // DebugElement and setting the value ourselves. expect(() => { const toggle = fixture.debugElement.query(By.css('mat-button-toggle'))!.componentInstance; toggle.checked = true; fixture.detectChanges(); }).not.toThrow(); }); it('should have a focus indicator', () => { const fixture = TestBed.createComponent(ButtonTogglesInsideButtonToggleGroup); const buttonNativeElements = [...fixture.debugElement.nativeElement.querySelectorAll('button')]; expect( buttonNativeElements.every(element => element.classList.contains('mat-focus-indicator')), ).toBe(true); }); it('should be able to pre-check a button toggle using a static checked binding', () => { const fixture = TestBed.createComponent(ButtonToggleWithStaticChecked); fixture.detectChanges(); expect(fixture.componentInstance.toggles.map(t => t.checked)).toEqual([false, true]); expect(fixture.componentInstance.group.value).toBe('2'); }); }); @Component({ template: ` @if (renderFirstToggle) { Test1 } Test2 Test3 `, standalone: true, imports: [MatButtonToggleModule], }) class ButtonTogglesInsideButtonToggleGroup { isGroupDisabled: boolean = false; disabledIntearctive = false; isVertical: boolean = false; groupValue: string; renderFirstToggle = true; } @Component({ template: ` @for (option of options; track option) { {{option.label}} } `, standalone: true, imports: [MatButtonToggleModule, FormsModule, ReactiveFormsModule], }) class ButtonToggleGroupWithNgModel { groupName = 'group-name'; modelValue: string; options = [ {label: 'Red', value: 'red', name: ''}, {label: 'Green', value: 'green', name: ''}, {label: 'Blue', value: 'blue', name: ''}, ]; lastEvent: MatButtonToggleChange; disableRipple = false; } @Component({ template: ` Eggs Flour Sugar `, standalone: true, imports: [MatButtonToggleModule], }) class ButtonTogglesInsideButtonToggleGroupMultiple { isGroupDisabled: boolean = false; isVertical: boolean = false; } @Component({ template: ` Eggs Flour Sugar Sugar `, standalone: true, imports: [MatButtonToggleModule], }) class FalsyButtonTogglesInsideButtonToggleGroupMultiple { value: ('' | number | null | undefined | boolean)[] = [0]; @ViewChildren(MatButtonToggle) toggles: QueryList; } @Component({ template: ` Yes `, standalone: true, imports: [MatButtonToggleModule], }) class StandaloneButtonToggle {} @Component({ template: ` Value Red Value Green `, standalone: true, imports: [MatButtonToggleModule], }) class ButtonToggleGroupWithInitialValue { lastEvent: MatButtonToggleChange; } @Component({ template: ` Value Red Value Green Value Blue `, standalone: true, imports: [MatButtonToggleModule, FormsModule, ReactiveFormsModule], }) class ButtonToggleGroupWithFormControl { control = new FormControl(''); } @Component({ // We need the `@if` so that there's a container between the group and the toggles. template: ` @if (true) { Value Red Value Green Value Blue } `, standalone: true, imports: [MatButtonToggleModule, FormsModule, ReactiveFormsModule], }) class ButtonToggleGroupWithIndirectDescendantToggles { control = new FormControl(''); } /** Simple test component with an aria-label set. */ @Component({ template: ``, standalone: true, imports: [MatButtonToggleModule], }) class ButtonToggleWithAriaLabel {} /** Simple test component with an aria-label set. */ @Component({ template: ``, standalone: true, imports: [MatButtonToggleModule], }) class ButtonToggleWithAriaLabelledby {} @Component({ template: ` @for (toggle of possibleValues; track toggle) { {{toggle}} } `, standalone: true, imports: [MatButtonToggleModule], }) class RepeatedButtonTogglesWithPreselectedValue { @ViewChild(MatButtonToggleGroup) toggleGroup: MatButtonToggleGroup; @ViewChildren(MatButtonToggle) toggles: QueryList; possibleValues = ['One', 'Two', 'Three']; value = 'Two'; } @Component({ template: ``, standalone: true, imports: [MatButtonToggleModule], }) class ButtonToggleWithTabindex {} @Component({ template: ``, standalone: true, imports: [MatButtonToggleModule], }) class ButtonToggleWithStaticName {} @Component({ template: ` One Two `, standalone: true, imports: [MatButtonToggleModule], }) class ButtonToggleWithStaticChecked { @ViewChild(MatButtonToggleGroup) group: MatButtonToggleGroup; @ViewChildren(MatButtonToggle) toggles: QueryList; } @Component({ template: ` `, standalone: true, imports: [MatButtonToggleModule], }) class ButtonToggleWithStaticAriaAttributes {} @Component({ template: ` @for (value of values; track value) { {{value}} } `, standalone: true, imports: [MatButtonToggleModule, FormsModule, ReactiveFormsModule], }) class ButtonToggleGroupWithFormControlAndDynamicButtons { @ViewChildren(MatButtonToggle) toggles: QueryList; control = new FormControl(''); values = ['a', 'b', 'c']; } @Component({ template: ` One Two Three `, standalone: true, imports: [MatButtonToggleModule, FormsModule], }) class ButtonToggleGroupWithNgModelAndStaticOptions { value = ''; }