2233 lines
76 KiB
TypeScript
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;
|
|
}
|