import {Direction, Directionality} from '@angular/cdk/bidi'; import { DOWN_ARROW, END, ENTER, HOME, LEFT_ARROW, RIGHT_ARROW, SPACE, UP_ARROW, } from '@angular/cdk/keycodes'; import {_supportsShadowDom} from '@angular/cdk/platform'; import { CdkStep, STEPPER_GLOBAL_OPTIONS, STEP_STATE, StepperOrientation, } from '@angular/cdk/stepper'; import { createKeyboardEvent, dispatchEvent, dispatchKeyboardEvent, } from '@angular/cdk/testing/private'; import { Component, DebugElement, EventEmitter, Provider, QueryList, Type, ViewChild, ViewChildren, ViewEncapsulation, inject, signal, } from '@angular/core'; import {ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing'; import { AbstractControl, AsyncValidatorFn, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, ValidationErrors, Validators, } from '@angular/forms'; import {MatRipple, ThemePalette} from '@angular/material/core'; import {MatFormFieldModule} from '@angular/material/form-field'; import {MatInputModule} from '@angular/material/input'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {Observable, Subject, merge} from 'rxjs'; import {map, take} from 'rxjs/operators'; import {MatStepHeader, MatStepperModule} from './index'; import {MatStep, MatStepper} from './stepper'; import {MatStepperNext, MatStepperPrevious} from './stepper-button'; import {MatStepperIntl} from './stepper-intl'; const VALID_REGEX = /valid/; let dir: {value: Direction; readonly change: EventEmitter}; describe('MatStepper', () => { beforeEach(() => { dir = { value: 'ltr', change: new EventEmitter(), }; }); describe('basic stepper', () => { let fixture: ComponentFixture; beforeEach(() => { fixture = createComponent(SimpleMatVerticalStepperApp); fixture.detectChanges(); }); it('should default to the first step', () => { const stepperComponent: MatStepper = fixture.debugElement.query( By.css('mat-stepper'), )!.componentInstance; expect(stepperComponent.selectedIndex).toBe(0); }); it('should throw when a negative `selectedIndex` is assigned', () => { const stepperComponent: MatStepper = fixture.debugElement.query( By.css('mat-stepper'), )!.componentInstance; expect(() => { stepperComponent.selectedIndex = -10; fixture.detectChanges(); }).toThrowError(/Cannot assign out-of-bounds/); }); it('should throw when an out-of-bounds `selectedIndex` is assigned', () => { const stepperComponent: MatStepper = fixture.debugElement.query( By.css('mat-stepper'), )!.componentInstance; expect(() => { stepperComponent.selectedIndex = 1337; fixture.detectChanges(); }).toThrowError(/Cannot assign out-of-bounds/); }); it('should change selected index on header click', () => { const stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header')); const stepperComponent = fixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; expect(stepperComponent.selectedIndex).toBe(0); expect(stepperComponent.selected instanceof MatStep).toBe(true); // select the second step let stepHeaderEl = stepHeaders[1].nativeElement; stepHeaderEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(1); expect(stepperComponent.selected instanceof MatStep).toBe(true); // select the third step stepHeaderEl = stepHeaders[2].nativeElement; stepHeaderEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(2); expect(stepperComponent.selected instanceof MatStep).toBe(true); }); it('should set the "tablist" role on stepper', () => { const stepperEl = fixture.debugElement.query(By.css('mat-stepper'))!.nativeElement; expect(stepperEl.getAttribute('role')).toBe('tablist'); }); it('should display the correct label', () => { const stepperComponent = fixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; let selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]'); expect(selectedLabel.textContent).toMatch('Step 1'); stepperComponent.selectedIndex = 2; fixture.detectChanges(); selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]'); expect(selectedLabel.textContent).toMatch('Step 3'); fixture.componentInstance.inputLabel.set('New Label'); fixture.detectChanges(); selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]'); expect(selectedLabel.textContent).toMatch('New Label'); }); it('should go to next available step when the next button is clicked', () => { const stepperComponent = fixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; expect(stepperComponent.selectedIndex).toBe(0); let nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[0] .nativeElement; nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(1); nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[1] .nativeElement; nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(2); nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[2] .nativeElement; nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(2); }); it('should set the next stepper button type to "submit"', () => { const button = fixture.debugElement.query(By.directive(MatStepperNext))!.nativeElement; expect(button.type) .withContext(`Expected the button to have "submit" set as type.`) .toBe('submit'); }); it('should go to previous available step when the previous button is clicked', () => { const stepperComponent = fixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; expect(stepperComponent.selectedIndex).toBe(0); stepperComponent.selectedIndex = 2; let previousButtonNativeEl = fixture.debugElement.queryAll( By.directive(MatStepperPrevious), )[2].nativeElement; previousButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(1); previousButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperPrevious))[1] .nativeElement; previousButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(0); previousButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperPrevious))[0] .nativeElement; previousButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(0); }); it('should set the previous stepper button type to "button"', () => { const button = fixture.debugElement.query(By.directive(MatStepperPrevious))!.nativeElement; expect(button.type) .withContext(`Expected the button to have "button" set as type.`) .toBe('button'); }); it('should set the correct step position for animation', () => { const stepperComponent = fixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; expect(stepperComponent._getAnimationDirection(0)).toBe('current'); expect(stepperComponent._getAnimationDirection(1)).toBe('next'); expect(stepperComponent._getAnimationDirection(2)).toBe('next'); stepperComponent.selectedIndex = 1; fixture.detectChanges(); expect(stepperComponent._getAnimationDirection(0)).toBe('previous'); expect(stepperComponent._getAnimationDirection(2)).toBe('next'); expect(stepperComponent._getAnimationDirection(1)).toBe('current'); stepperComponent.selectedIndex = 2; fixture.detectChanges(); expect(stepperComponent._getAnimationDirection(0)).toBe('previous'); expect(stepperComponent._getAnimationDirection(1)).toBe('previous'); expect(stepperComponent._getAnimationDirection(2)).toBe('current'); }); it('should not set focus on header of selected step if header is not clicked', () => { const stepperComponent = fixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; const stepHeaderEl = fixture.debugElement.queryAll(By.css('mat-step-header'))[1] .nativeElement; const nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[0] .nativeElement; spyOn(stepHeaderEl, 'focus'); nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(1); expect(stepHeaderEl.focus).not.toHaveBeenCalled(); }); it('should focus next step header if focus is inside the stepper', () => { const stepperComponent = fixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; const stepHeaderEl = fixture.debugElement.queryAll(By.css('mat-step-header'))[1] .nativeElement; const nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[0] .nativeElement; spyOn(stepHeaderEl, 'focus'); nextButtonNativeEl.focus(); nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(1); expect(stepHeaderEl.focus).toHaveBeenCalled(); }); it('should focus next step header if focus is inside the stepper with shadow DOM', () => { if (!_supportsShadowDom()) { return; } fixture.destroy(); TestBed.resetTestingModule(); fixture = createComponent(SimpleMatVerticalStepperApp, [], [], ViewEncapsulation.ShadowDom); fixture.detectChanges(); const stepperComponent = fixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; const stepHeaderEl = fixture.debugElement.queryAll(By.css('mat-step-header'))[1] .nativeElement; const nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[0] .nativeElement; spyOn(stepHeaderEl, 'focus'); nextButtonNativeEl.focus(); nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(1); expect(stepHeaderEl.focus).toHaveBeenCalled(); }); it('should only be able to return to a previous step if it is editable', () => { const stepperComponent = fixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; stepperComponent.selectedIndex = 1; stepperComponent.steps.toArray()[0].editable = false; const previousButtonNativeEl = fixture.debugElement.queryAll( By.directive(MatStepperPrevious), )[1].nativeElement; previousButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(1); stepperComponent.steps.toArray()[0].editable = true; previousButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(0); }); it('should set create icon if step is editable and completed', () => { const stepperComponent = fixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; const nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[0] .nativeElement; expect(stepperComponent._getIndicatorType(0)).toBe('number'); stepperComponent.steps.toArray()[0].editable = true; nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent._getIndicatorType(0)).toBe('edit'); }); it('should set done icon if step is not editable and is completed', () => { const stepperComponent = fixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; const nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[0] .nativeElement; expect(stepperComponent._getIndicatorType(0)).toBe('number'); stepperComponent.steps.toArray()[0].editable = false; nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent._getIndicatorType(0)).toBe('done'); }); it('should emit an event when the enter animation is done', fakeAsync(() => { const stepper = fixture.debugElement.query(By.directive(MatStepper))!.componentInstance; const selectionChangeSpy = jasmine.createSpy('selectionChange spy'); const animationDoneSpy = jasmine.createSpy('animationDone spy'); const selectionChangeSubscription = stepper.selectionChange.subscribe(selectionChangeSpy); const animationDoneSubscription = stepper.animationDone.subscribe(animationDoneSpy); stepper.selectedIndex = 1; fixture.detectChanges(); expect(selectionChangeSpy).toHaveBeenCalledTimes(1); expect(animationDoneSpy).not.toHaveBeenCalled(); flush(); expect(selectionChangeSpy).toHaveBeenCalledTimes(1); expect(animationDoneSpy).toHaveBeenCalledTimes(1); selectionChangeSubscription.unsubscribe(); animationDoneSubscription.unsubscribe(); })); it('should set the correct aria-posinset and aria-setsize', () => { const headers = Array.from( fixture.nativeElement.querySelectorAll('.mat-step-header'), ); expect(headers.map(header => header.getAttribute('aria-posinset'))).toEqual(['1', '2', '3']); expect(headers.every(header => header.getAttribute('aria-setsize') === '3')).toBe(true); }); it('should adjust the index when removing a step before the current one', () => { const stepperComponent: MatStepper = fixture.debugElement.query( By.css('mat-stepper'), )!.componentInstance; stepperComponent.selectedIndex = 2; fixture.detectChanges(); // Re-assert since the setter has some extra logic. expect(stepperComponent.selectedIndex).toBe(2); expect(() => { fixture.componentInstance.showStepTwo.set(false); fixture.detectChanges(); }).not.toThrow(); expect(stepperComponent.selectedIndex).toBe(1); }); it('should not do anything when pressing the ENTER key with a modifier', () => { const stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header')); assertSelectKeyWithModifierInteraction(fixture, stepHeaders, 'vertical', ENTER); }); it('should not do anything when pressing the SPACE key with a modifier', () => { const stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header')); assertSelectKeyWithModifierInteraction(fixture, stepHeaders, 'vertical', SPACE); }); it('should have a focus indicator', () => { const stepHeaderNativeElements = [ ...fixture.debugElement.nativeElement.querySelectorAll('.mat-vertical-stepper-header'), ]; expect( stepHeaderNativeElements.every(element => element.querySelector('.mat-focus-indicator')), ).toBe(true); }); it('should hide the header icons from assistive technology', () => { const icon = fixture.nativeElement.querySelector('.mat-step-icon span'); expect(icon.getAttribute('aria-hidden')).toBe('true'); }); it('should add units to unit-less values passed in to animationDuration', () => { const stepperComponent: MatStepper = fixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; stepperComponent.animationDuration = '1337'; expect(stepperComponent.animationDuration).toBe('1337ms'); }); }); describe('basic stepper when attempting to set the selected step too early', () => { it('should not throw', () => { const fixture = createComponent(SimpleMatVerticalStepperApp); const stepperComponent: MatStepper = fixture.debugElement.query( By.css('mat-stepper'), )!.componentInstance; expect(() => stepperComponent.selected).not.toThrow(); }); }); describe('basic stepper when attempting to set the selected step too early', () => { it('should not throw', () => { const fixture = createComponent(SimpleMatVerticalStepperApp); const stepperComponent: MatStepper = fixture.debugElement.query( By.css('mat-stepper'), )!.componentInstance; expect(() => (stepperComponent.selected = null!)).not.toThrow(); expect(stepperComponent.selectedIndex).toBe(-1); }); }); describe('basic stepper with i18n label change', () => { let i18nFixture: ComponentFixture; beforeEach(() => { i18nFixture = createComponent(SimpleMatHorizontalStepperApp); i18nFixture.detectChanges(); }); it('should re-render when the i18n labels change', () => { const intl = TestBed.inject(MatStepperIntl); const header = i18nFixture.debugElement.queryAll(By.css('mat-step-header'))[2].nativeElement; const optionalLabel = header.querySelector('.mat-step-optional'); expect(optionalLabel).toBeTruthy(); expect(optionalLabel.textContent).toBe('Optional'); intl.optionalLabel = 'Valgfri'; intl.changes.next(); i18nFixture.detectChanges(); expect(optionalLabel.textContent).toBe('Valgfri'); }); }); describe('basic stepper with completed label change', () => { let fixture: ComponentFixture; beforeEach(() => { fixture = createComponent(SimpleMatHorizontalStepperApp); fixture.detectChanges(); }); it('should re-render when the completed labels change', () => { const intl = TestBed.inject(MatStepperIntl); const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper))!; const stepperComponent: MatStepper = stepperDebugElement.componentInstance; stepperComponent.steps.toArray()[0].editable = false; stepperComponent.next(); fixture.detectChanges(); const header = stepperDebugElement.nativeElement.querySelector('mat-step-header'); const completedLabel = header.querySelector('.cdk-visually-hidden'); expect(completedLabel).toBeTruthy(); expect(completedLabel.textContent).toBe('Completed'); intl.completedLabel = 'Completada'; intl.changes.next(); fixture.detectChanges(); expect(completedLabel.textContent).toBe('Completada'); }); }); describe('basic stepper with editable label change', () => { let fixture: ComponentFixture; beforeEach(() => { fixture = createComponent(SimpleMatHorizontalStepperApp); fixture.detectChanges(); }); it('should re-render when the editable label changes', () => { const intl = TestBed.inject(MatStepperIntl); const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper))!; const stepperComponent: MatStepper = stepperDebugElement.componentInstance; stepperComponent.steps.toArray()[0].editable = true; stepperComponent.next(); fixture.detectChanges(); const header = stepperDebugElement.nativeElement.querySelector('mat-step-header'); const editableLabel = header.querySelector('.cdk-visually-hidden'); expect(editableLabel).toBeTruthy(); expect(editableLabel.textContent).toBe('Editable'); intl.editableLabel = 'Modificabile'; intl.changes.next(); fixture.detectChanges(); expect(editableLabel.textContent).toBe('Modificabile'); }); }); describe('icon overrides', () => { let fixture: ComponentFixture; beforeEach(() => { fixture = createComponent(IconOverridesStepper); fixture.detectChanges(); }); it('should allow for the `edit` icon to be overridden', () => { const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper))!; const stepperComponent: MatStepper = stepperDebugElement.componentInstance; stepperComponent.steps.toArray()[0].editable = true; stepperComponent.next(); fixture.detectChanges(); const header = stepperDebugElement.nativeElement.querySelector('mat-step-header'); expect(header.textContent).toContain('Custom edit'); }); it('should allow for the `done` icon to be overridden', () => { const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper))!; const stepperComponent: MatStepper = stepperDebugElement.componentInstance; stepperComponent.steps.toArray()[0].editable = false; stepperComponent.next(); fixture.detectChanges(); const header = stepperDebugElement.nativeElement.querySelector('mat-step-header'); expect(header.textContent).toContain('Custom done'); }); it('should allow for the `number` icon to be overridden with context', () => { const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper))!; const headers = stepperDebugElement.nativeElement.querySelectorAll('mat-step-header'); expect(headers[2].textContent).toContain('III'); }); }); describe('RTL', () => { let fixture: ComponentFixture; beforeEach(() => { dir.value = 'rtl'; fixture = createComponent(SimpleMatVerticalStepperApp); fixture.detectChanges(); }); it('should reverse animation in RTL mode', () => { const stepperComponent = fixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; expect(stepperComponent._getAnimationDirection(0)).toBe('current'); expect(stepperComponent._getAnimationDirection(1)).toBe('previous'); expect(stepperComponent._getAnimationDirection(2)).toBe('previous'); stepperComponent.selectedIndex = 1; fixture.detectChanges(); expect(stepperComponent._getAnimationDirection(0)).toBe('next'); expect(stepperComponent._getAnimationDirection(2)).toBe('previous'); expect(stepperComponent._getAnimationDirection(1)).toBe('current'); stepperComponent.selectedIndex = 2; fixture.detectChanges(); expect(stepperComponent._getAnimationDirection(0)).toBe('next'); expect(stepperComponent._getAnimationDirection(1)).toBe('next'); expect(stepperComponent._getAnimationDirection(2)).toBe('current'); }); }); describe('linear stepper', () => { let fixture: ComponentFixture; let testComponent: LinearMatVerticalStepperApp; let stepperComponent: MatStepper; beforeEach(() => { fixture = createComponent(LinearMatVerticalStepperApp, [], [], undefined, []); fixture.detectChanges(); testComponent = fixture.componentInstance; stepperComponent = fixture.debugElement.query(By.css('mat-stepper'))!.componentInstance; }); it('should have true linear attribute', () => { expect(stepperComponent.linear).toBe(true); }); it('should not move to next step if current step is invalid', () => { expect(testComponent.oneGroup.get('oneCtrl')!.value).toBe(''); expect(testComponent.oneGroup.get('oneCtrl')!.valid).toBe(false); expect(testComponent.oneGroup.valid).toBe(false); expect(testComponent.oneGroup.invalid).toBe(true); expect(stepperComponent.selectedIndex).toBe(0); const stepHeaderEl = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header'))[1] .nativeElement; stepHeaderEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(0); const nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[0] .nativeElement; nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(0); testComponent.oneGroup.get('oneCtrl')!.setValue('answer'); stepHeaderEl.click(); fixture.detectChanges(); expect(testComponent.oneGroup.valid).toBe(true); expect(stepperComponent.selectedIndex).toBe(1); }); it('should not move to next step if current step is pending', () => { const stepHeaderEl = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header'))[2] .nativeElement; const nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[1] .nativeElement; testComponent.oneGroup.get('oneCtrl')!.setValue('input'); testComponent.twoGroup.get('twoCtrl')!.setValue('input'); stepperComponent.selectedIndex = 1; fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(1); // Step status = PENDING // Assert that linear stepper does not allow step selection change expect(testComponent.twoGroup.pending).toBe(true); stepHeaderEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(1); nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(1); // Trigger asynchronous validation testComponent.validationTrigger.next(); // Asynchronous validation completed: // Step status = VALID expect(testComponent.twoGroup.pending).toBe(false); expect(testComponent.twoGroup.valid).toBe(true); stepHeaderEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(2); stepperComponent.selectedIndex = 1; fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(1); nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(2); }); it('should be able to focus step header upon click if it is unable to be selected', () => { const stepHeaderEl = fixture.debugElement.queryAll(By.css('mat-step-header'))[1] .nativeElement; fixture.detectChanges(); expect(stepHeaderEl.getAttribute('tabindex')).toBe('-1'); }); it('should be able to move to next step even when invalid if current step is optional', () => { testComponent.oneGroup.get('oneCtrl')!.setValue('input'); testComponent.twoGroup.get('twoCtrl')!.setValue('input'); testComponent.validationTrigger.next(); stepperComponent.selectedIndex = 1; fixture.detectChanges(); stepperComponent.selectedIndex = 2; fixture.detectChanges(); expect(stepperComponent.steps.toArray()[2].optional).toBe(true); expect(stepperComponent.selectedIndex).toBe(2); expect(testComponent.threeGroup.get('threeCtrl')!.valid).toBe(true); const nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[2] .nativeElement; nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex) .withContext('Expected selectedIndex to change when optional step input is empty.') .toBe(3); stepperComponent.selectedIndex = 2; testComponent.threeGroup.get('threeCtrl')!.setValue('input'); nextButtonNativeEl.click(); fixture.detectChanges(); expect(testComponent.threeGroup.get('threeCtrl')!.valid).toBe(false); expect(stepperComponent.selectedIndex) .withContext('Expected selectedIndex to change when optional step input is invalid.') .toBe(3); }); it('should be able to reset the stepper to its initial state', () => { const steps = stepperComponent.steps.toArray(); testComponent.oneGroup.get('oneCtrl')!.setValue('value'); fixture.detectChanges(); stepperComponent.next(); fixture.detectChanges(); stepperComponent.next(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(1); expect(steps[0].interacted).toBe(true); expect(steps[0].completed).toBe(true); expect(testComponent.oneGroup.get('oneCtrl')!.valid).toBe(true); expect(testComponent.oneGroup.get('oneCtrl')!.value).toBe('value'); expect(steps[1].interacted).toBe(true); expect(steps[1].completed).toBe(false); expect(testComponent.twoGroup.get('twoCtrl')!.valid).toBe(false); stepperComponent.reset(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(0); expect(steps[0].interacted).toBe(false); expect(steps[0].completed).toBe(false); expect(testComponent.oneGroup.get('oneCtrl')!.valid).toBe(false); expect(testComponent.oneGroup.get('oneCtrl')!.value).toBeFalsy(); expect(steps[1].interacted).toBe(false); expect(steps[1].completed).toBe(false); expect(testComponent.twoGroup.get('twoCtrl')!.valid).toBe(false); }); it('should reset back to the first step when some of the steps are not editable', () => { const steps = stepperComponent.steps.toArray(); steps[0].editable = false; testComponent.oneGroup.get('oneCtrl')!.setValue('value'); fixture.detectChanges(); stepperComponent.next(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(1); stepperComponent.reset(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(0); }); it('should not clobber the `complete` binding when resetting', () => { const steps: CdkStep[] = stepperComponent.steps.toArray(); const fillOutStepper = () => { testComponent.oneGroup.get('oneCtrl')!.setValue('input'); testComponent.twoGroup.get('twoCtrl')!.setValue('input'); testComponent.threeGroup.get('threeCtrl')!.setValue('valid'); testComponent.validationTrigger.next(); stepperComponent.selectedIndex = 1; fixture.detectChanges(); stepperComponent.selectedIndex = 2; fixture.detectChanges(); stepperComponent.selectedIndex = 3; fixture.detectChanges(); }; fillOutStepper(); expect(steps[2].completed) .withContext('Expected third step to be considered complete after the first run through.') .toBe(true); stepperComponent.reset(); fixture.detectChanges(); fillOutStepper(); expect(steps[2].completed) .withContext( 'Expected third step to be considered complete when doing a run after ' + 'a reset.', ) .toBe(true); }); it('should be able to skip past the current step if a custom `completed` value is set', () => { expect(testComponent.oneGroup.get('oneCtrl')!.value).toBe(''); expect(testComponent.oneGroup.get('oneCtrl')!.valid).toBe(false); expect(testComponent.oneGroup.valid).toBe(false); expect(stepperComponent.selectedIndex).toBe(0); const nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[0] .nativeElement; nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(0); stepperComponent.steps.first.completed = true; nextButtonNativeEl.click(); fixture.detectChanges(); expect(testComponent.oneGroup.valid).toBe(false); expect(stepperComponent.selectedIndex).toBe(1); }); it('should set aria-disabled if the user is not able to navigate to a step', () => { const stepHeaders = Array.from( fixture.nativeElement.querySelectorAll('.mat-vertical-stepper-header'), ); expect(stepHeaders.map(step => step.getAttribute('aria-disabled'))).toEqual([ null, 'true', 'true', 'true', ]); }); }); describe('linear stepper with a pre-defined selectedIndex', () => { let preselectedFixture: ComponentFixture; let stepper: MatStepper; beforeEach(() => { preselectedFixture = createComponent(SimplePreselectedMatHorizontalStepperApp); preselectedFixture.detectChanges(); stepper = preselectedFixture.debugElement.query(By.directive(MatStepper))!.componentInstance; }); it('should not throw', () => { expect(() => preselectedFixture.detectChanges()).not.toThrow(); }); it('selectedIndex should be typeof number', () => { expect(typeof stepper.selectedIndex).toBe('number'); }); it('value of selectedIndex should be the pre-defined value', () => { expect(stepper.selectedIndex).toBe(0); }); }); describe('linear stepper with no `stepControl`', () => { let noStepControlFixture: ComponentFixture; beforeEach(() => { noStepControlFixture = createComponent(SimpleStepperWithoutStepControl); noStepControlFixture.detectChanges(); }); it('should not move to the next step if the current one is not completed ', () => { const stepper: MatStepper = noStepControlFixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; const headers = noStepControlFixture.debugElement.queryAll( By.css('.mat-horizontal-stepper-header'), ); expect(stepper.selectedIndex).toBe(0); headers[1].nativeElement.click(); noStepControlFixture.detectChanges(); expect(stepper.selectedIndex).toBe(0); }); }); describe('linear stepper with `stepControl`', () => { let controlAndBindingFixture: ComponentFixture; beforeEach(() => { controlAndBindingFixture = createComponent(SimpleStepperWithStepControlAndCompletedBinding); controlAndBindingFixture.detectChanges(); }); it('should have the `stepControl` take precedence when `completed` is set', () => { expect(controlAndBindingFixture.componentInstance.steps[0].control.valid).toBe(true); expect(controlAndBindingFixture.componentInstance.steps[0].completed).toBe(false); const stepper: MatStepper = controlAndBindingFixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; const headers = controlAndBindingFixture.debugElement.queryAll( By.css('.mat-horizontal-stepper-header'), ); expect(stepper.selectedIndex).toBe(0); headers[1].nativeElement.click(); controlAndBindingFixture.detectChanges(); expect(stepper.selectedIndex).toBe(1); }); }); describe('vertical stepper', () => { it('should set the aria-orientation to "vertical"', () => { const fixture = createComponent(SimpleMatVerticalStepperApp); fixture.detectChanges(); const stepperEl = fixture.debugElement.query(By.css('mat-stepper'))!.nativeElement; expect(stepperEl.getAttribute('aria-orientation')).toBe('vertical'); }); it('should support using the left/right arrows to move focus', () => { const fixture = createComponent(SimpleMatVerticalStepperApp); fixture.detectChanges(); const stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header')); assertCorrectKeyboardInteraction(fixture, stepHeaders, 'horizontal'); }); it('should support using the up/down arrows to move focus', () => { const fixture = createComponent(SimpleMatVerticalStepperApp); fixture.detectChanges(); const stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header')); assertCorrectKeyboardInteraction(fixture, stepHeaders, 'vertical'); }); it('should reverse arrow key focus in RTL mode', () => { dir.value = 'rtl'; const fixture = createComponent(SimpleMatVerticalStepperApp); fixture.detectChanges(); const stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header')); assertArrowKeyInteractionInRtl(fixture, stepHeaders); }); it('should be able to disable ripples', () => { const fixture = createComponent(SimpleMatVerticalStepperApp); fixture.detectChanges(); const stepHeaders = fixture.debugElement.queryAll(By.directive(MatStepHeader)); const headerRipples = stepHeaders.map(headerDebugEl => headerDebugEl.query(By.directive(MatRipple))!.injector.get(MatRipple), ); expect(headerRipples.every(ripple => ripple.disabled)).toBe(false); fixture.componentInstance.disableRipple.set(true); fixture.detectChanges(); expect(headerRipples.every(ripple => ripple.disabled)).toBe(true); }); it('should be able to disable ripples', () => { const fixture = createComponent(SimpleMatVerticalStepperApp); fixture.detectChanges(); const stepHeaders = fixture.debugElement.queryAll(By.directive(MatStepHeader)); stepHeaders[0].componentInstance.focus('mouse'); stepHeaders[1].componentInstance.focus(); expect(stepHeaders[1].nativeElement.classList).toContain('cdk-focused'); expect(stepHeaders[1].nativeElement.classList).toContain('cdk-mouse-focused'); }); it('should be able to set the theme for all steps', () => { const fixture = createComponent(SimpleMatVerticalStepperApp); fixture.detectChanges(); const headers = Array.from( fixture.nativeElement.querySelectorAll('.mat-step-header'), ); expect(headers.every(element => element.classList.contains('mat-primary'))).toBe(true); expect(headers.some(element => element.classList.contains('mat-accent'))).toBe(false); expect(headers.some(element => element.classList.contains('mat-warn'))).toBe(false); fixture.componentInstance.stepperTheme.set('accent'); fixture.detectChanges(); expect(headers.some(element => element.classList.contains('mat-accent'))).toBe(true); expect(headers.some(element => element.classList.contains('mat-primary'))).toBe(false); expect(headers.some(element => element.classList.contains('mat-warn'))).toBe(false); }); it('should be able to set the theme for a specific step', () => { const fixture = createComponent(SimpleMatVerticalStepperApp); fixture.detectChanges(); const headers = Array.from( fixture.nativeElement.querySelectorAll('.mat-step-header'), ); expect(headers.every(element => element.classList.contains('mat-primary'))).toBe(true); fixture.componentInstance.secondStepTheme.set('accent'); fixture.detectChanges(); expect(headers[0].classList.contains('mat-primary')).toBe(true); expect(headers[1].classList.contains('mat-primary')).toBe(false); expect(headers[2].classList.contains('mat-primary')).toBe(true); expect(headers[1].classList.contains('mat-accent')).toBe(true); }); }); describe('horizontal stepper', () => { it('should set the aria-orientation to "horizontal"', () => { const fixture = createComponent(SimpleMatHorizontalStepperApp); fixture.detectChanges(); const stepperEl = fixture.debugElement.query(By.css('mat-stepper'))!.nativeElement; expect(stepperEl.getAttribute('aria-orientation')).toBe('horizontal'); }); it('should support using the left/right arrows to move focus', () => { const fixture = createComponent(SimpleMatHorizontalStepperApp); fixture.detectChanges(); const stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header')); assertCorrectKeyboardInteraction(fixture, stepHeaders, 'horizontal'); }); it('should reverse arrow key focus in RTL mode', () => { dir.value = 'rtl'; const fixture = createComponent(SimpleMatHorizontalStepperApp); fixture.detectChanges(); const stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header')); assertArrowKeyInteractionInRtl(fixture, stepHeaders); }); it('should maintain the correct navigation order when a step is added later on', () => { const fixture = createComponent(HorizontalStepperWithDelayedStep); fixture.detectChanges(); fixture.componentInstance.renderSecondStep.set(true); fixture.detectChanges(); const stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header')); assertCorrectKeyboardInteraction(fixture, stepHeaders, 'horizontal'); }); it('should reverse arrow key focus when switching into RTL after init', () => { const fixture = createComponent(SimpleMatHorizontalStepperApp); fixture.detectChanges(); const stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header')); assertCorrectKeyboardInteraction(fixture, stepHeaders, 'horizontal'); dir.value = 'rtl'; dir.change.emit('rtl'); fixture.detectChanges(); assertArrowKeyInteractionInRtl(fixture, stepHeaders); }); it('should be able to disable ripples', () => { const fixture = createComponent(SimpleMatHorizontalStepperApp); fixture.detectChanges(); const stepHeaders = fixture.debugElement.queryAll(By.directive(MatStepHeader)); const headerRipples = stepHeaders.map(headerDebugEl => headerDebugEl.query(By.directive(MatRipple))!.injector.get(MatRipple), ); expect(headerRipples.every(ripple => ripple.disabled)).toBe(false); fixture.componentInstance.disableRipple.set(true); fixture.detectChanges(); expect(headerRipples.every(ripple => ripple.disabled)).toBe(true); }); it('should be able to set the theme for all steps', () => { const fixture = createComponent(SimpleMatHorizontalStepperApp); fixture.detectChanges(); const headers = Array.from( fixture.nativeElement.querySelectorAll('.mat-step-header'), ); expect(headers.every(element => element.classList.contains('mat-primary'))).toBe(true); expect(headers.some(element => element.classList.contains('mat-accent'))).toBe(false); expect(headers.some(element => element.classList.contains('mat-warn'))).toBe(false); fixture.componentInstance.stepperTheme.set('accent'); fixture.detectChanges(); expect(headers.some(element => element.classList.contains('mat-accent'))).toBe(true); expect(headers.some(element => element.classList.contains('mat-primary'))).toBe(false); expect(headers.some(element => element.classList.contains('mat-warn'))).toBe(false); }); it('should be able to set the theme for a specific step', () => { const fixture = createComponent(SimpleMatHorizontalStepperApp); fixture.detectChanges(); const headers = Array.from( fixture.nativeElement.querySelectorAll('.mat-step-header'), ); expect(headers.every(element => element.classList.contains('mat-primary'))).toBe(true); fixture.componentInstance.secondStepTheme.set('accent'); fixture.detectChanges(); expect(headers[0].classList.contains('mat-primary')).toBe(true); expect(headers[1].classList.contains('mat-primary')).toBe(false); expect(headers[2].classList.contains('mat-primary')).toBe(true); expect(headers[1].classList.contains('mat-accent')).toBe(true); }); it('should be able to mark all steps as interacted', () => { const fixture = createComponent(SimpleMatHorizontalStepperApp); fixture.detectChanges(); const stepper: MatStepper = fixture.debugElement.query( By.directive(MatStepper), ).componentInstance; expect(stepper.steps.map(step => step.interacted)).toEqual([false, false, false]); stepper.next(); fixture.detectChanges(); expect(stepper.steps.map(step => step.interacted)).toEqual([true, false, false]); stepper.next(); fixture.detectChanges(); expect(stepper.steps.map(step => step.interacted)).toEqual([true, true, false]); stepper.next(); fixture.detectChanges(); expect(stepper.steps.map(step => step.interacted)).toEqual([true, true, true]); }); it('should emit when the user has interacted with a step', () => { const fixture = createComponent(SimpleMatHorizontalStepperApp); fixture.detectChanges(); const stepper: MatStepper = fixture.debugElement.query( By.directive(MatStepper), ).componentInstance; const interactedSteps: number[] = []; const subscription = merge(...stepper.steps.map(step => step.interactedStream)).subscribe( step => interactedSteps.push(stepper.steps.toArray().indexOf(step as MatStep)), ); expect(interactedSteps).toEqual([]); stepper.next(); fixture.detectChanges(); expect(interactedSteps).toEqual([0]); stepper.next(); fixture.detectChanges(); expect(interactedSteps).toEqual([0, 1]); stepper.next(); fixture.detectChanges(); expect(interactedSteps).toEqual([0, 1, 2]); subscription.unsubscribe(); }); it('should set a class on the host if the header is positioned at the bottom', () => { const fixture = createComponent(SimpleMatHorizontalStepperApp); fixture.detectChanges(); const stepperHost = fixture.nativeElement.querySelector('.mat-stepper-horizontal'); expect(stepperHost.classList).not.toContain('mat-stepper-header-position-bottom'); fixture.componentInstance.headerPosition.set('bottom'); fixture.detectChanges(); expect(stepperHost.classList).toContain('mat-stepper-header-position-bottom'); }); }); describe('linear stepper with valid step', () => { let fixture: ComponentFixture; let testComponent: LinearStepperWithValidOptionalStep; let stepper: MatStepper; beforeEach(() => { fixture = createComponent(LinearStepperWithValidOptionalStep); fixture.detectChanges(); testComponent = fixture.componentInstance; stepper = fixture.debugElement.query(By.css('mat-stepper'))!.componentInstance; }); it('must be visited if not optional', () => { stepper.selectedIndex = 1; fixture.componentRef.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(stepper.selectedIndex).toBe(1); stepper.selectedIndex = 2; fixture.componentRef.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(stepper.selectedIndex).toBe(2); }); it('can be skipped entirely if optional', () => { testComponent.step2Optional.set(true); fixture.detectChanges(); stepper.selectedIndex = 2; fixture.detectChanges(); expect(stepper.selectedIndex).toBe(2); }); }); describe('aria labelling', () => { let fixture: ComponentFixture; let stepHeader: HTMLElement; beforeEach(() => { fixture = createComponent(StepperWithAriaInputs); fixture.detectChanges(); stepHeader = fixture.nativeElement.querySelector('.mat-step-header'); }); it('should not set aria-label or aria-labelledby attributes if they are not passed in', () => { expect(stepHeader.hasAttribute('aria-label')).toBe(false); expect(stepHeader.hasAttribute('aria-labelledby')).toBe(false); }); it('should set the aria-label attribute', () => { fixture.componentInstance.ariaLabel.set('First step'); fixture.detectChanges(); expect(stepHeader.getAttribute('aria-label')).toBe('First step'); }); it('should set the aria-labelledby attribute', () => { fixture.componentInstance.ariaLabelledby.set('first-step-label'); fixture.detectChanges(); expect(stepHeader.getAttribute('aria-labelledby')).toBe('first-step-label'); }); it('should not be able to set both an aria-label and aria-labelledby', () => { fixture.componentInstance.ariaLabel.set('First step'); fixture.componentInstance.ariaLabelledby.set('first-step-label'); fixture.detectChanges(); expect(stepHeader.getAttribute('aria-label')).toBe('First step'); expect(stepHeader.hasAttribute('aria-labelledby')).toBe(false); }); }); describe('stepper with error state', () => { let fixture: ComponentFixture; let stepper: MatStepper; function createFixture(showErrorByDefault: boolean | undefined) { fixture = createComponent( MatHorizontalStepperWithErrorsApp, [ { provide: STEPPER_GLOBAL_OPTIONS, useValue: {showError: showErrorByDefault}, }, ], [MatFormFieldModule, MatInputModule], ); fixture.detectChanges(); stepper = fixture.debugElement.query(By.css('mat-stepper'))!.componentInstance; } it('should show error state', () => { createFixture(true); const nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[0] .nativeElement; stepper.selectedIndex = 1; stepper.steps.first.hasError = true; nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepper._getIndicatorType(0)).toBe(STEP_STATE.ERROR); }); it('should respect a custom falsy hasError value', () => { createFixture(true); const nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[0] .nativeElement; stepper.selectedIndex = 1; nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepper._getIndicatorType(0)).toBe(STEP_STATE.ERROR); stepper.steps.first.hasError = false; fixture.detectChanges(); expect(stepper._getIndicatorType(0)).not.toBe(STEP_STATE.ERROR); }); it('should show error state if explicitly enabled, even when disabled globally', () => { createFixture(undefined); const nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[0] .nativeElement; stepper.selectedIndex = 1; stepper.steps.first.hasError = true; nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepper._getIndicatorType(0)).toBe(STEP_STATE.ERROR); }); }); describe('stepper using Material UI Guideline logic', () => { let fixture: ComponentFixture; let stepper: MatStepper; beforeEach(() => { fixture = createComponent( MatHorizontalStepperWithErrorsApp, [ { provide: STEPPER_GLOBAL_OPTIONS, useValue: {displayDefaultIndicatorType: false}, }, ], [MatFormFieldModule, MatInputModule], ); fixture.detectChanges(); stepper = fixture.debugElement.query(By.css('mat-stepper'))!.componentInstance; }); it('should show done state when step is completed and its not the current step', () => { const nextButtonNativeEl = fixture.debugElement.queryAll(By.directive(MatStepperNext))[0] .nativeElement; stepper.selectedIndex = 1; stepper.steps.first.completed = true; nextButtonNativeEl.click(); fixture.detectChanges(); expect(stepper._getIndicatorType(0)).toBe(STEP_STATE.DONE); }); it('should show edit state when step is editable and its the current step', () => { stepper.selectedIndex = 1; stepper.steps.toArray()[1].editable = true; fixture.detectChanges(); expect(stepper._getIndicatorType(1)).toBe(STEP_STATE.EDIT); }); }); describe('indirect descendants', () => { it('should be able to change steps when steps are indirect descendants', () => { const fixture = createComponent(StepperWithIndirectDescendantSteps); fixture.detectChanges(); const stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header')); const stepperComponent = fixture.debugElement.query( By.directive(MatStepper), )!.componentInstance; expect(stepperComponent.selectedIndex).toBe(0); expect(stepperComponent.selected instanceof MatStep).toBe(true); // select the second step let stepHeaderEl = stepHeaders[1].nativeElement; stepHeaderEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(1); expect(stepperComponent.selected instanceof MatStep).toBe(true); // select the third step stepHeaderEl = stepHeaders[2].nativeElement; stepHeaderEl.click(); fixture.detectChanges(); expect(stepperComponent.selectedIndex).toBe(2); expect(stepperComponent.selected instanceof MatStep).toBe(true); }); it('should allow for the `edit` icon to be overridden', () => { const fixture = createComponent(IndirectDescendantIconOverridesStepper); fixture.detectChanges(); const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper))!; const stepperComponent: MatStepper = stepperDebugElement.componentInstance; stepperComponent.steps.toArray()[0].editable = true; stepperComponent.next(); fixture.detectChanges(); const header = stepperDebugElement.nativeElement.querySelector('mat-step-header'); expect(header.textContent).toContain('Custom edit'); }); it('should allow for the `done` icon to be overridden', () => { const fixture = createComponent(IndirectDescendantIconOverridesStepper); fixture.detectChanges(); const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper))!; const stepperComponent: MatStepper = stepperDebugElement.componentInstance; stepperComponent.steps.toArray()[0].editable = false; stepperComponent.next(); fixture.detectChanges(); const header = stepperDebugElement.nativeElement.querySelector('mat-step-header'); expect(header.textContent).toContain('Custom done'); }); it('should allow for the `number` icon to be overridden with context', () => { const fixture = createComponent(IndirectDescendantIconOverridesStepper); fixture.detectChanges(); const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper))!; const headers = stepperDebugElement.nativeElement.querySelectorAll('mat-step-header'); expect(headers[2].textContent).toContain('III'); }); }); it('should be able to toggle steps via ngIf', () => { const fixture = createComponent(StepperWithNgIf); fixture.detectChanges(); expect(fixture.nativeElement.querySelectorAll('.mat-step-header').length).toBe(1); fixture.componentInstance.showStep2.set(true); fixture.detectChanges(); expect(fixture.nativeElement.querySelectorAll('.mat-step-header').length).toBe(2); }); it('should not pick up the steps from descendant steppers', () => { const fixture = createComponent(NestedSteppers); fixture.detectChanges(); const steppers = fixture.componentInstance.steppers.toArray(); expect(steppers[0].steps.length).toBe(3); expect(steppers[1].steps.length).toBe(2); }); it('should not throw when trying to change steps after initializing to an out-of-bounds index', () => { const fixture = createComponent(StepperWithStaticOutOfBoundsIndex); fixture.detectChanges(); const stepper = fixture.componentInstance.stepper; expect(stepper.selectedIndex).toBe(0); expect(stepper.selected).toBeTruthy(); expect(() => { stepper.selectedIndex = 1; fixture.detectChanges(); }).not.toThrow(); expect(stepper.selectedIndex).toBe(1); expect(stepper.selected).toBeTruthy(); }); describe('stepper with lazy content', () => { it('should render the content of the selected step on init', () => { const fixture = createComponent(StepperWithLazyContent); const element = fixture.nativeElement; fixture.componentInstance.selectedIndex.set(1); fixture.detectChanges(); expect(element.textContent).not.toContain('Step 1 content'); expect(element.textContent).toContain('Step 2 content'); expect(element.textContent).not.toContain('Step 3 content'); }); it('should render the content of steps when the user navigates to them', () => { const fixture = createComponent(StepperWithLazyContent); const element = fixture.nativeElement; fixture.componentInstance.selectedIndex.set(0); fixture.detectChanges(); expect(element.textContent).toContain('Step 1 content'); expect(element.textContent).not.toContain('Step 2 content'); expect(element.textContent).not.toContain('Step 3 content'); fixture.componentInstance.selectedIndex.set(1); fixture.detectChanges(); expect(element.textContent).toContain('Step 1 content'); expect(element.textContent).toContain('Step 2 content'); expect(element.textContent).not.toContain('Step 3 content'); fixture.componentInstance.selectedIndex.set(2); fixture.detectChanges(); expect(element.textContent).toContain('Step 1 content'); expect(element.textContent).toContain('Step 2 content'); expect(element.textContent).toContain('Step 3 content'); }); }); describe('stepper with two-way binding on selectedIndex', () => { it('should update selectedIndex in component on navigation', () => { const fixture = createComponent(StepperWithTwoWayBindingOnSelectedIndex); fixture.detectChanges(); expect(fixture.componentInstance.index).toBe(0); const stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header')); let lastStepHeaderEl = stepHeaders[2].nativeElement; lastStepHeaderEl.click(); fixture.detectChanges(); expect(fixture.componentInstance.index).toBe(2); let middleStepHeaderEl = stepHeaders[1].nativeElement; middleStepHeaderEl.click(); fixture.detectChanges(); expect(fixture.componentInstance.index).toBe(1); let firstStepHeaderEl = stepHeaders[0].nativeElement; firstStepHeaderEl.click(); fixture.detectChanges(); expect(fixture.componentInstance.index).toBe(0); }); }); }); /** Asserts that keyboard interaction works correctly. */ function assertCorrectKeyboardInteraction( fixture: ComponentFixture, stepHeaders: DebugElement[], orientation: StepperOrientation, ) { const stepperComponent = fixture.debugElement.query(By.directive(MatStepper))!.componentInstance; const nextKey = orientation === 'vertical' ? DOWN_ARROW : RIGHT_ARROW; const prevKey = orientation === 'vertical' ? UP_ARROW : LEFT_ARROW; expect(stepperComponent._getFocusIndex()).toBe(0); expect(stepperComponent.selectedIndex).toBe(0); let stepHeaderEl = stepHeaders[0].nativeElement; dispatchKeyboardEvent(stepHeaderEl, 'keydown', nextKey); fixture.detectChanges(); expect(stepperComponent._getFocusIndex()) .withContext('Expected index of focused step to increase by 1 after pressing the next key.') .toBe(1); expect(stepperComponent.selectedIndex) .withContext('Expected index of selected step to remain unchanged after pressing the next key.') .toBe(0); stepHeaderEl = stepHeaders[1].nativeElement; dispatchKeyboardEvent(stepHeaderEl, 'keydown', ENTER); fixture.detectChanges(); expect(stepperComponent._getFocusIndex()) .withContext('Expected index of focused step to remain unchanged after ENTER event.') .toBe(1); expect(stepperComponent.selectedIndex) .withContext( 'Expected index of selected step to change to index of focused step ' + 'after ENTER event.', ) .toBe(1); stepHeaderEl = stepHeaders[1].nativeElement; dispatchKeyboardEvent(stepHeaderEl, 'keydown', prevKey); fixture.detectChanges(); expect(stepperComponent._getFocusIndex()) .withContext( 'Expected index of focused step to decrease by 1 after pressing the ' + 'previous key.', ) .toBe(0); expect(stepperComponent.selectedIndex) .withContext( 'Expected index of selected step to remain unchanged after pressing the ' + 'previous key.', ) .toBe(1); // When the focus is on the last step and right arrow key is pressed, the focus should cycle // through to the first step. stepperComponent._keyManager.updateActiveItem(2); stepHeaderEl = stepHeaders[2].nativeElement; dispatchKeyboardEvent(stepHeaderEl, 'keydown', nextKey); fixture.detectChanges(); expect(stepperComponent._getFocusIndex()) .withContext( 'Expected index of focused step to cycle through to index 0 after pressing ' + 'the next key.', ) .toBe(0); expect(stepperComponent.selectedIndex) .withContext( 'Expected index of selected step to remain unchanged after pressing ' + 'the next key.', ) .toBe(1); stepHeaderEl = stepHeaders[0].nativeElement; dispatchKeyboardEvent(stepHeaderEl, 'keydown', SPACE); fixture.detectChanges(); expect(stepperComponent._getFocusIndex()) .withContext('Expected index of focused to remain unchanged after SPACE event.') .toBe(0); expect(stepperComponent.selectedIndex) .withContext( 'Expected index of selected step to change to index of focused step ' + 'after SPACE event.', ) .toBe(0); const endEvent = dispatchKeyboardEvent(stepHeaderEl, 'keydown', END); expect(stepperComponent._getFocusIndex()) .withContext('Expected last step to be focused when pressing END.') .toBe(stepHeaders.length - 1); expect(endEvent.defaultPrevented) .withContext('Expected default END action to be prevented.') .toBe(true); const homeEvent = dispatchKeyboardEvent(stepHeaderEl, 'keydown', HOME); expect(stepperComponent._getFocusIndex()) .withContext('Expected first step to be focused when pressing HOME.') .toBe(0); expect(homeEvent.defaultPrevented) .withContext('Expected default HOME action to be prevented.') .toBe(true); } /** Asserts that arrow key direction works correctly in RTL mode. */ function assertArrowKeyInteractionInRtl( fixture: ComponentFixture, stepHeaders: DebugElement[], ) { const stepperComponent = fixture.debugElement.query(By.directive(MatStepper))!.componentInstance; expect(stepperComponent._getFocusIndex()).toBe(0); let stepHeaderEl = stepHeaders[0].nativeElement; dispatchKeyboardEvent(stepHeaderEl, 'keydown', LEFT_ARROW); fixture.detectChanges(); expect(stepperComponent._getFocusIndex()).toBe(1); stepHeaderEl = stepHeaders[1].nativeElement; dispatchKeyboardEvent(stepHeaderEl, 'keydown', RIGHT_ARROW); fixture.detectChanges(); expect(stepperComponent._getFocusIndex()).toBe(0); } /** Asserts that keyboard interaction works correctly when the user is pressing a modifier key. */ function assertSelectKeyWithModifierInteraction( fixture: ComponentFixture, stepHeaders: DebugElement[], orientation: StepperOrientation, selectionKey: number, ) { const stepperComponent = fixture.debugElement.query(By.directive(MatStepper))!.componentInstance; const modifiers = ['altKey', 'shiftKey', 'ctrlKey', 'metaKey']; expect(stepperComponent._getFocusIndex()).toBe(0); expect(stepperComponent.selectedIndex).toBe(0); dispatchKeyboardEvent( stepHeaders[0].nativeElement, 'keydown', orientation === 'vertical' ? DOWN_ARROW : RIGHT_ARROW, ); fixture.detectChanges(); expect(stepperComponent._getFocusIndex()) .withContext( 'Expected index of focused step to increase by 1 after pressing ' + 'the next key.', ) .toBe(1); expect(stepperComponent.selectedIndex) .withContext( 'Expected index of selected step to remain unchanged after pressing ' + 'the next key.', ) .toBe(0); modifiers.forEach(modifier => { const event = createKeyboardEvent('keydown', selectionKey); Object.defineProperty(event, modifier, {get: () => true}); dispatchEvent(stepHeaders[1].nativeElement, event); fixture.detectChanges(); expect(stepperComponent.selectedIndex) .withContext( `Expected selected index to remain unchanged ` + `when pressing the selection key with ${modifier} modifier.`, ) .toBe(0); expect(event.defaultPrevented).toBe(false); }); } function asyncValidator(minLength: number, validationTrigger: Subject): AsyncValidatorFn { return (control: AbstractControl): Observable => { return validationTrigger.pipe( map(() => control.value && control.value.length >= minLength ? null : {asyncValidation: {}}, ), take(1), ); }; } function createComponent( component: Type, providers: Provider[] = [], imports: any[] = [], encapsulation?: ViewEncapsulation, declarations = [component], ): ComponentFixture { TestBed.configureTestingModule({ imports: [MatStepperModule, NoopAnimationsModule, ReactiveFormsModule, ...imports], providers: [{provide: Directionality, useFactory: () => dir}, ...providers], declarations, }); if (encapsulation != null) { TestBed.overrideComponent(component, { set: {encapsulation}, }); } return TestBed.createComponent(component); } @Component({ template: `
Step 1 First name This field is required
Step 2 Content 2
`, standalone: false, }) class MatHorizontalStepperWithErrorsApp { private readonly _formBuilder = inject(FormBuilder); formGroup = this._formBuilder.group({ firstNameCtrl: ['', Validators.required], lastNameCtrl: ['', Validators.required], }); } @Component({ template: ` Step 1 Content 1
Step 2 Content 2
Content 3
`, standalone: false, }) class SimpleMatHorizontalStepperApp { inputLabel = 'Step 3'; disableRipple = signal(false); stepperTheme = signal(undefined); secondStepTheme = signal(undefined); headerPosition = signal(''); } @Component({ template: ` Step 1 Content 1
@if (showStepTwo()) { Step 2 Content 2
} Content 3
`, standalone: false, }) class SimpleMatVerticalStepperApp { inputLabel = signal('Step 3'); showStepTwo = signal(true); disableRipple = signal(false); stepperTheme = signal(undefined); secondStepTheme = signal(undefined); } @Component({ standalone: true, template: `
Step one
Step two
Step two
Done
`, imports: [ReactiveFormsModule, MatStepperModule], }) class LinearMatVerticalStepperApp { validationTrigger = new Subject(); oneGroup = new FormGroup({ oneCtrl: new FormControl('', Validators.required), }); twoGroup = new FormGroup({ twoCtrl: new FormControl('', Validators.required, asyncValidator(3, this.validationTrigger)), }); threeGroup = new FormGroup({ threeCtrl: new FormControl('', Validators.pattern(VALID_REGEX)), }); } @Component({ template: ` `, standalone: false, }) class SimplePreselectedMatHorizontalStepperApp { index = 0; } @Component({ template: ` @for (step of steps; track step) { } `, standalone: false, }) class SimpleStepperWithoutStepControl { steps = [ {label: 'One', completed: false}, {label: 'Two', completed: false}, {label: 'Three', completed: false}, ]; } @Component({ template: ` @for (step of steps; track step) { } `, standalone: false, }) class SimpleStepperWithStepControlAndCompletedBinding { steps = [ {label: 'One', completed: false, control: new FormControl('')}, {label: 'Two', completed: false, control: new FormControl('')}, {label: 'Three', completed: false, control: new FormControl('')}, ]; } @Component({ template: ` Custom edit Custom done {{getRomanNumeral(index + 1)}} Content 1 Content 2 Content 3 `, standalone: false, }) class IconOverridesStepper { getRomanNumeral(value: number) { const numberMap: {[key: number]: string} = { 1: 'I', 2: 'II', 3: 'III', 4: 'IV', 5: 'V', 6: 'VI', 7: 'VII', 8: 'VIII', 9: 'IX', }; return numberMap[value]; } } @Component({ template: ` @if (true) { Custom edit Custom done {{getRomanNumeral(index + 1)}} } Content 1 Content 2 Content 3 `, standalone: false, }) class IndirectDescendantIconOverridesStepper extends IconOverridesStepper {} @Component({ template: ` `, standalone: false, }) class LinearStepperWithValidOptionalStep { controls = [0, 0, 0].map(() => new FormControl('')); step2Optional = signal(false); } @Component({ template: ` `, standalone: false, }) class StepperWithAriaInputs { ariaLabel = signal(''); ariaLabelledby = signal(''); } @Component({ template: ` @if (true) { Content 1 Content 2 Content 3 } `, standalone: false, }) class StepperWithIndirectDescendantSteps {} @Component({ template: ` Step 1 @if (showStep2()) { Step 2 } `, standalone: false, }) class StepperWithNgIf { showStep2 = signal(false); } @Component({ template: ` Content 1 Content 2 Sub-Content 1 Sub-Content 2 `, standalone: false, }) class NestedSteppers { @ViewChildren(MatStepper) steppers: QueryList; } @Component({ template: ` Content 1 Content 2 Content 3 `, standalone: false, }) class StepperWithStaticOutOfBoundsIndex { @ViewChild(MatStepper) stepper: MatStepper; } @Component({ template: ` Step 1 Step 1 content Step 2 Step 2 content Step 3 Step 3 content `, standalone: false, }) class StepperWithLazyContent { selectedIndex = signal(0); } @Component({ template: ` Content 1 @if (renderSecondStep()) { Content 2 } Content 3 `, standalone: false, }) class HorizontalStepperWithDelayedStep { renderSecondStep = signal(false); } @Component({ template: ` `, standalone: false, }) class StepperWithTwoWayBindingOnSelectedIndex { index: number = 0; }