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

2233 lines
76 KiB
TypeScript

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<Direction>};
describe('MatStepper', () => {
beforeEach(() => {
dir = {
value: 'ltr',
change: new EventEmitter(),
};
});
describe('basic stepper', () => {
let fixture: ComponentFixture<SimpleMatVerticalStepperApp>;
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<HTMLElement>(
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<SimpleMatHorizontalStepperApp>;
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<SimpleMatHorizontalStepperApp>;
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<SimpleMatHorizontalStepperApp>;
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<IconOverridesStepper>;
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<SimpleMatVerticalStepperApp>;
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<LinearMatVerticalStepperApp>;
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<HTMLElement>(
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<SimplePreselectedMatHorizontalStepperApp>;
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<SimpleStepperWithoutStepControl>;
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<SimpleStepperWithStepControlAndCompletedBinding>;
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<HTMLElement>(
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<HTMLElement>(
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<HTMLElement>(
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<HTMLElement>(
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<LinearStepperWithValidOptionalStep>;
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<StepperWithAriaInputs>;
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<MatHorizontalStepperWithErrorsApp>;
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<MatHorizontalStepperWithErrorsApp>;
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<any>,
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<any>,
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<any>,
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<void>): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
return validationTrigger.pipe(
map(() =>
control.value && control.value.length >= minLength ? null : {asyncValidation: {}},
),
take(1),
);
};
}
function createComponent<T>(
component: Type<T>,
providers: Provider[] = [],
imports: any[] = [],
encapsulation?: ViewEncapsulation,
declarations = [component],
): ComponentFixture<T> {
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<T>(component);
}
@Component({
template: `
<form [formGroup]="formGroup">
<mat-stepper>
<mat-step errorMessage="This field is required"
[stepControl]="formGroup.get('firstNameCtrl')">
<ng-template matStepLabel>Step 1</ng-template>
<mat-form-field>
<mat-label>First name</mat-label>
<input matInput formControlName="firstNameCtrl" required>
<mat-error>This field is required</mat-error>
</mat-form-field>
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button matStepperNext>Next</button>
</div>
</mat-step>
<mat-step>
<ng-template matStepLabel>Step 2</ng-template>
Content 2
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button matStepperNext>Next</button>
</div>
</mat-step>
</mat-stepper>
</form>
`,
standalone: false,
})
class MatHorizontalStepperWithErrorsApp {
private readonly _formBuilder = inject(FormBuilder);
formGroup = this._formBuilder.group({
firstNameCtrl: ['', Validators.required],
lastNameCtrl: ['', Validators.required],
});
}
@Component({
template: `
<mat-stepper
[disableRipple]="disableRipple()"
[color]="stepperTheme()"
[headerPosition]="headerPosition()">
<mat-step>
<ng-template matStepLabel>Step 1</ng-template>
Content 1
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button matStepperNext>Next</button>
</div>
</mat-step>
<mat-step [color]="secondStepTheme()">
<ng-template matStepLabel>Step 2</ng-template>
Content 2
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button matStepperNext>Next</button>
</div>
</mat-step>
<mat-step [label]="inputLabel" optional>
Content 3
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button matStepperNext>Next</button>
</div>
</mat-step>
</mat-stepper>
`,
standalone: false,
})
class SimpleMatHorizontalStepperApp {
inputLabel = 'Step 3';
disableRipple = signal(false);
stepperTheme = signal<ThemePalette>(undefined);
secondStepTheme = signal<ThemePalette>(undefined);
headerPosition = signal('');
}
@Component({
template: `
<mat-stepper orientation="vertical" [disableRipple]="disableRipple()" [color]="stepperTheme()">
<mat-step>
<ng-template matStepLabel>Step 1</ng-template>
Content 1
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button matStepperNext>Next</button>
</div>
</mat-step>
@if (showStepTwo()) {
<mat-step [color]="secondStepTheme()">
<ng-template matStepLabel>Step 2</ng-template>
Content 2
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button matStepperNext>Next</button>
</div>
</mat-step>
}
<mat-step [label]="inputLabel()">
Content 3
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button matStepperNext>Next</button>
</div>
</mat-step>
</mat-stepper>
`,
standalone: false,
})
class SimpleMatVerticalStepperApp {
inputLabel = signal('Step 3');
showStepTwo = signal(true);
disableRipple = signal(false);
stepperTheme = signal<ThemePalette>(undefined);
secondStepTheme = signal<ThemePalette>(undefined);
}
@Component({
standalone: true,
template: `
<mat-stepper orientation="vertical" linear>
<mat-step [stepControl]="oneGroup">
<form [formGroup]="oneGroup">
<ng-template matStepLabel>Step one</ng-template>
<input formControlName="oneCtrl" required>
<div>
<button matStepperPrevious>Back</button>
<button matStepperNext>Next</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="twoGroup">
<form [formGroup]="twoGroup">
<ng-template matStepLabel>Step two</ng-template>
<input formControlName="twoCtrl" required>
<div>
<button matStepperPrevious>Back</button>
<button matStepperNext>Next</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="threeGroup" optional>
<form [formGroup]="threeGroup">
<ng-template matStepLabel>Step two</ng-template>
<input formControlName="threeCtrl">
<div>
<button matStepperPrevious>Back</button>
<button matStepperNext>Next</button>
</div>
</form>
</mat-step>
<mat-step>
Done
</mat-step>
</mat-stepper>
`,
imports: [ReactiveFormsModule, MatStepperModule],
})
class LinearMatVerticalStepperApp {
validationTrigger = new Subject<void>();
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: `
<mat-stepper [linear]="true" [selectedIndex]="index">
<mat-step label="One"></mat-step>
<mat-step label="Two"></mat-step>
<mat-step label="Three"></mat-step>
</mat-stepper>
`,
standalone: false,
})
class SimplePreselectedMatHorizontalStepperApp {
index = 0;
}
@Component({
template: `
<mat-stepper linear>
@for (step of steps; track step) {
<mat-step [label]="step.label" [completed]="step.completed"></mat-step>
}
</mat-stepper>
`,
standalone: false,
})
class SimpleStepperWithoutStepControl {
steps = [
{label: 'One', completed: false},
{label: 'Two', completed: false},
{label: 'Three', completed: false},
];
}
@Component({
template: `
<mat-stepper linear>
@for (step of steps; track step) {
<mat-step
[label]="step.label"
[stepControl]="step.control"
[completed]="step.completed"></mat-step>
}
</mat-stepper>
`,
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: `
<mat-stepper>
<ng-template matStepperIcon="edit">Custom edit</ng-template>
<ng-template matStepperIcon="done">Custom done</ng-template>
<ng-template matStepperIcon="number" let-index="index">
{{getRomanNumeral(index + 1)}}
</ng-template>
<mat-step>Content 1</mat-step>
<mat-step>Content 2</mat-step>
<mat-step>Content 3</mat-step>
</mat-stepper>
`,
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: `
<mat-stepper>
@if (true) {
<ng-template matStepperIcon="edit">Custom edit</ng-template>
<ng-template matStepperIcon="done">Custom done</ng-template>
<ng-template matStepperIcon="number" let-index="index">
{{getRomanNumeral(index + 1)}}
</ng-template>
}
<mat-step>Content 1</mat-step>
<mat-step>Content 2</mat-step>
<mat-step>Content 3</mat-step>
</mat-stepper>
`,
standalone: false,
})
class IndirectDescendantIconOverridesStepper extends IconOverridesStepper {}
@Component({
template: `
<mat-stepper linear>
<mat-step label="Step 1" [stepControl]="controls[0]"></mat-step>
<mat-step label="Step 2" [stepControl]="controls[1]" [optional]="step2Optional"></mat-step>
<mat-step label="Step 3" [stepControl]="controls[2]"></mat-step>
</mat-stepper>
`,
standalone: false,
})
class LinearStepperWithValidOptionalStep {
controls = [0, 0, 0].map(() => new FormControl(''));
step2Optional = signal(false);
}
@Component({
template: `
<mat-stepper>
<mat-step [aria-label]="ariaLabel()" [aria-labelledby]="ariaLabelledby()" label="One"></mat-step>
</mat-stepper>
`,
standalone: false,
})
class StepperWithAriaInputs {
ariaLabel = signal('');
ariaLabelledby = signal('');
}
@Component({
template: `
<mat-stepper orientation="vertical">
@if (true) {
<mat-step label="Step 1">Content 1</mat-step>
<mat-step label="Step 2">Content 2</mat-step>
<mat-step label="Step 3">Content 3</mat-step>
}
</mat-stepper>
`,
standalone: false,
})
class StepperWithIndirectDescendantSteps {}
@Component({
template: `
<mat-stepper orientation="vertical">
<mat-step>
<ng-template matStepLabel>Step 1</ng-template>
</mat-step>
@if (showStep2()) {
<mat-step>
<ng-template matStepLabel>Step 2</ng-template>
</mat-step>
}
</mat-stepper>
`,
standalone: false,
})
class StepperWithNgIf {
showStep2 = signal(false);
}
@Component({
template: `
<mat-stepper orientation="vertical">
<mat-step label="Step 1">Content 1</mat-step>
<mat-step label="Step 2">Content 2</mat-step>
<mat-step label="Step 3">
<mat-stepper>
<mat-step label="Sub-Step 1">Sub-Content 1</mat-step>
<mat-step label="Sub-Step 2">Sub-Content 2</mat-step>
</mat-stepper>
</mat-step>
</mat-stepper>
`,
standalone: false,
})
class NestedSteppers {
@ViewChildren(MatStepper) steppers: QueryList<MatStepper>;
}
@Component({
template: `
<mat-stepper orientation="vertical" selectedIndex="1337">
<mat-step label="Step 1">Content 1</mat-step>
<mat-step label="Step 2">Content 2</mat-step>
<mat-step label="Step 3">Content 3</mat-step>
</mat-stepper>
`,
standalone: false,
})
class StepperWithStaticOutOfBoundsIndex {
@ViewChild(MatStepper) stepper: MatStepper;
}
@Component({
template: `
<mat-stepper orientation="vertical" [selectedIndex]="selectedIndex()">
<mat-step>
<ng-template matStepLabel>Step 1</ng-template>
<ng-template matStepContent>Step 1 content</ng-template>
</mat-step>
<mat-step>
<ng-template matStepLabel>Step 2</ng-template>
<ng-template matStepContent>Step 2 content</ng-template>
</mat-step>
<mat-step>
<ng-template matStepLabel>Step 3</ng-template>
<ng-template matStepContent>Step 3 content</ng-template>
</mat-step>
</mat-stepper>
`,
standalone: false,
})
class StepperWithLazyContent {
selectedIndex = signal(0);
}
@Component({
template: `
<mat-stepper>
<mat-step label="Step 1">Content 1</mat-step>
@if (renderSecondStep()) {
<mat-step label="Step 2">Content 2</mat-step>
}
<mat-step label="Step 3">Content 3</mat-step>
</mat-stepper>
`,
standalone: false,
})
class HorizontalStepperWithDelayedStep {
renderSecondStep = signal(false);
}
@Component({
template: `
<mat-stepper [(selectedIndex)]="index">
<mat-step label="One"></mat-step>
<mat-step label="Two"></mat-step>
<mat-step label="Three"></mat-step>
</mat-stepper>
`,
standalone: false,
})
class StepperWithTwoWayBindingOnSelectedIndex {
index: number = 0;
}