From 69ef54a5130bfdbb70d0c8dffadb89a29d2f84d3 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Thu, 30 Jul 2020 23:38:45 +0200 Subject: [PATCH] feat(stepper): add test harness Adds test harnesses for the stepper-related components. --- src/material/config.bzl | 1 + src/material/stepper/step-header.html | 5 +- src/material/stepper/stepper-button.ts | 2 + src/material/stepper/testing/BUILD.bazel | 53 +++ src/material/stepper/testing/index.ts | 9 + src/material/stepper/testing/public-api.ts | 12 + src/material/stepper/testing/shared.spec.ts | 314 ++++++++++++++++++ .../stepper/testing/step-harness-filters.ts | 39 +++ src/material/stepper/testing/step-harness.ts | 132 ++++++++ .../testing/stepper-button-harnesses.ts | 61 ++++ .../stepper/testing/stepper-harness.spec.ts | 9 + .../stepper/testing/stepper-harness.ts | 61 ++++ .../material/stepper/testing.d.ts | 54 +++ 13 files changed, 750 insertions(+), 2 deletions(-) create mode 100644 src/material/stepper/testing/BUILD.bazel create mode 100644 src/material/stepper/testing/index.ts create mode 100644 src/material/stepper/testing/public-api.ts create mode 100644 src/material/stepper/testing/shared.spec.ts create mode 100644 src/material/stepper/testing/step-harness-filters.ts create mode 100644 src/material/stepper/testing/step-harness.ts create mode 100644 src/material/stepper/testing/stepper-button-harnesses.ts create mode 100644 src/material/stepper/testing/stepper-harness.spec.ts create mode 100644 src/material/stepper/testing/stepper-harness.ts create mode 100644 tools/public_api_guard/material/stepper/testing.d.ts diff --git a/src/material/config.bzl b/src/material/config.bzl index 2d935e0a73a4..b19db81f16fe 100644 --- a/src/material/config.bzl +++ b/src/material/config.bzl @@ -54,6 +54,7 @@ entryPoints = [ "sort", "sort/testing", "stepper", + "stepper/testing", "table", "table/testing", "tabs", diff --git a/src/material/stepper/step-header.html b/src/material/stepper/step-header.html index 4faa85eb57f3..ae06ac2fbaef 100644 --- a/src/material/stepper/step-header.html +++ b/src/material/stepper/step-header.html @@ -19,8 +19,9 @@ [class.mat-step-label-selected]="selected" [class.mat-step-label-error]="state == 'error'"> - - +
+ +
{{label}}
diff --git a/src/material/stepper/stepper-button.ts b/src/material/stepper/stepper-button.ts index 42fcfb36197f..35eef9ad4a28 100644 --- a/src/material/stepper/stepper-button.ts +++ b/src/material/stepper/stepper-button.ts @@ -13,6 +13,7 @@ import {Directive} from '@angular/core'; @Directive({ selector: 'button[matStepperNext]', host: { + 'class': 'mat-stepper-next', '[type]': 'type', }, inputs: ['type'] @@ -24,6 +25,7 @@ export class MatStepperNext extends CdkStepperNext { @Directive({ selector: 'button[matStepperPrevious]', host: { + 'class': 'mat-stepper-previous', '[type]': 'type', }, inputs: ['type'] diff --git a/src/material/stepper/testing/BUILD.bazel b/src/material/stepper/testing/BUILD.bazel new file mode 100644 index 000000000000..f323ef9a2af9 --- /dev/null +++ b/src/material/stepper/testing/BUILD.bazel @@ -0,0 +1,53 @@ +load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library") + +package(default_visibility = ["//visibility:public"]) + +ts_library( + name = "testing", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + module_name = "@angular/material/stepper/testing", + deps = [ + "//src/cdk/testing", + ], +) + +filegroup( + name = "source-files", + srcs = glob(["**/*.ts"]), +) + +ng_test_library( + name = "harness_tests_lib", + srcs = ["shared.spec.ts"], + deps = [ + ":testing", + "//src/cdk/stepper", + "//src/cdk/testing", + "//src/cdk/testing/private", + "//src/cdk/testing/testbed", + "//src/material/stepper", + "@npm//@angular/forms", + "@npm//@angular/platform-browser", + ], +) + +ng_test_library( + name = "unit_tests_lib", + srcs = glob( + ["**/*.spec.ts"], + exclude = ["shared.spec.ts"], + ), + deps = [ + ":harness_tests_lib", + ":testing", + "//src/material/stepper", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = [":unit_tests_lib"], +) diff --git a/src/material/stepper/testing/index.ts b/src/material/stepper/testing/index.ts new file mode 100644 index 000000000000..676ca90f1ffa --- /dev/null +++ b/src/material/stepper/testing/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './public-api'; diff --git a/src/material/stepper/testing/public-api.ts b/src/material/stepper/testing/public-api.ts new file mode 100644 index 000000000000..b2e8803cb6dd --- /dev/null +++ b/src/material/stepper/testing/public-api.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './stepper-harness'; +export * from './step-harness'; +export * from './step-harness-filters'; +export * from './stepper-button-harnesses'; diff --git a/src/material/stepper/testing/shared.spec.ts b/src/material/stepper/testing/shared.spec.ts new file mode 100644 index 000000000000..dbf3230c401b --- /dev/null +++ b/src/material/stepper/testing/shared.spec.ts @@ -0,0 +1,314 @@ +import {Component} from '@angular/core'; +import {ReactiveFormsModule, FormGroup, FormControl, Validators} from '@angular/forms'; +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatStepperModule} from '@angular/material/stepper'; +import {STEPPER_GLOBAL_OPTIONS} from '@angular/cdk/stepper'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {MatStepperHarness} from './stepper-harness'; +import {MatStepperNextHarness, MatStepperPreviousHarness} from './stepper-button-harnesses'; +import {StepperOrientation} from './step-harness-filters'; + +/** Shared tests to run on both the original and MDC-based steppers. */ +export function runHarnessTests( + stepperModule: typeof MatStepperModule, + stepperHarness: typeof MatStepperHarness, + stepperNextHarness: typeof MatStepperNextHarness, + stepperPreviousHarness: typeof MatStepperPreviousHarness) { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [stepperModule, NoopAnimationsModule, ReactiveFormsModule], + declarations: [StepperHarnessTest], + providers: [{ + provide: STEPPER_GLOBAL_OPTIONS, + useValue: {showError: true} // Required so the error state shows up in tests. + }] + }).compileComponents(); + + fixture = TestBed.createComponent(StepperHarnessTest); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('should load all stepper harnesses', async () => { + const steppers = await loader.getAllHarnesses(stepperHarness); + expect(steppers.length).toBe(3); + }); + + it('should filter steppers by their orientation', async () => { + const [verticalSteppers, horizontalSteppers] = await Promise.all([ + loader.getAllHarnesses(stepperHarness.with({orientation: StepperOrientation.VERTICAL})), + loader.getAllHarnesses(stepperHarness.with({orientation: StepperOrientation.HORIZONTAL})) + ]); + + expect(verticalSteppers.length).toBe(2); + expect(horizontalSteppers.length).toBe(1); + }); + + it('should get the orientation of a stepper', async () => { + const steppers = await loader.getAllHarnesses(stepperHarness); + + expect(await Promise.all(steppers.map(stepper => stepper.getOrientation()))).toEqual([ + StepperOrientation.VERTICAL, + StepperOrientation.HORIZONTAL, + StepperOrientation.VERTICAL + ]); + }); + + it('should get the steps of a stepper', async () => { + const steppers = await loader.getAllHarnesses(stepperHarness); + const steps = await Promise.all(steppers.map(stepper => stepper.getSteps())); + expect(steps.map(current => current.length)).toEqual([4, 3, 2]); + }); + + it('should filter the steps of a stepper', async () => { + const stepper = await loader.getHarness(stepperHarness.with({selector: '#one-stepper'})); + const steps = await stepper.getSteps({label: /Two|Four/}); + expect(await Promise.all(steps.map(step => step.getLabel()))).toEqual(['Two', 'Four']); + }); + + it('should be able to select a particular step that matches a filter', async () => { + const stepper = await loader.getHarness(stepperHarness.with({selector: '#one-stepper'})); + const steps = await stepper.getSteps(); + + expect(await Promise.all(steps.map(step => step.isSelected()))).toEqual([ + true, + false, + false, + false + ]); + + await stepper.selectStep({label: 'Three'}); + + expect(await Promise.all(steps.map(step => step.isSelected()))).toEqual([ + false, + false, + true, + false + ]); + }); + + it('should be able to get the text-based label of a step', async () => { + const stepper = await loader.getHarness(stepperHarness.with({selector: '#one-stepper'})); + const steps = await stepper.getSteps(); + + expect(await Promise.all(steps.map(step => step.getLabel()))).toEqual([ + 'One', + 'Two', + 'Three', + 'Four' + ]); + }); + + it('should be able to get the template-based label of a step', async () => { + const stepper = await loader.getHarness(stepperHarness.with({selector: '#two-stepper'})); + const steps = await stepper.getSteps(); + expect(await Promise.all(steps.map(step => step.getLabel()))).toEqual(['One', 'Two', 'Three']); + }); + + it('should be able to get the aria-label of a step', async () => { + const stepper = await loader.getHarness(stepperHarness.with({selector: '#one-stepper'})); + const steps = await stepper.getSteps(); + expect(await Promise.all(steps.map(step => step.getAriaLabel()))).toEqual([ + null, + null, + null, + 'Fourth step' + ]); + }); + + it('should be able to get the aria-labelledby of a step', async () => { + const stepper = await loader.getHarness(stepperHarness.with({selector: '#one-stepper'})); + const steps = await stepper.getSteps(); + expect(await Promise.all(steps.map(step => step.getAriaLabelledby()))).toEqual([ + null, + null, + 'some-label', + null + ]); + }); + + it('should get the selected state of a step', async () => { + const stepper = await loader.getHarness(stepperHarness.with({selector: '#one-stepper'})); + const steps = await stepper.getSteps(); + + expect(await Promise.all(steps.map(step => step.isSelected()))).toEqual([ + true, + false, + false, + false + ]); + }); + + it('should be able to select a step', async () => { + const stepper = await loader.getHarness(stepperHarness.with({selector: '#one-stepper'})); + const steps = await stepper.getSteps(); + + expect(await Promise.all(steps.map(step => step.isSelected()))).toEqual([ + true, + false, + false, + false + ]); + + await steps[2].select(); + + expect(await Promise.all(steps.map(step => step.isSelected()))).toEqual([ + false, + false, + true, + false + ]); + }); + + it('should get whether a step is optional', async () => { + const stepper = await loader.getHarness(stepperHarness.with({selector: '#two-stepper'})); + const steps = await stepper.getSteps(); + expect(await Promise.all(steps.map(step => step.isOptional()))).toEqual([false, true, true]); + }); + + it('should be able to get harness loader for an element inside a tab', async () => { + const stepper = await loader.getHarness(stepperHarness.with({selector: '#one-stepper'})); + const [step] = await stepper.getSteps({label: 'Two'}); + const [nextButton, previousButton] = await Promise.all([ + step.getHarness(stepperNextHarness), + step.getHarness(stepperPreviousHarness) + ]); + + expect(await nextButton.getText()).toBe('Next'); + expect(await previousButton.getText()).toBe('Previous'); + }); + + it('should go forward when pressing the next button', async () => { + const stepper = await loader.getHarness(stepperHarness.with({selector: '#one-stepper'})); + const steps = await stepper.getSteps(); + const secondStep = steps[1]; + const nextButton = await secondStep.getHarness(stepperNextHarness); + + await secondStep.select(); + + expect(await Promise.all(steps.map(step => step.isSelected()))).toEqual([ + false, + true, + false, + false + ]); + + await nextButton.click(); + + expect(await Promise.all(steps.map(step => step.isSelected()))).toEqual([ + false, + false, + true, + false + ]); + }); + + it('should go backward when pressing the previous button', async () => { + const stepper = await loader.getHarness(stepperHarness.with({selector: '#one-stepper'})); + const steps = await stepper.getSteps(); + const secondStep = steps[1]; + const previousButton = await secondStep.getHarness(stepperPreviousHarness); + + await secondStep.select(); + + expect(await Promise.all(steps.map(step => step.isSelected()))).toEqual([ + false, + true, + false, + false + ]); + + await previousButton.click(); + + expect(await Promise.all(steps.map(step => step.isSelected()))).toEqual([ + true, + false, + false, + false + ]); + }); + + it('should get whether a step has errors', async () => { + const stepper = await loader.getHarness(stepperHarness.with({selector: '#three-stepper'})); + const steps = await stepper.getSteps(); + + expect(await Promise.all(steps.map(step => step.hasErrors()))).toEqual([false, false]); + + await steps[1].select(); + + expect(await Promise.all(steps.map(step => step.hasErrors()))).toEqual([true, false]); + }); + + it('should get whether a step has been completed', async () => { + const stepper = await loader.getHarness(stepperHarness.with({selector: '#three-stepper'})); + const steps = await stepper.getSteps(); + + expect(await Promise.all(steps.map(step => step.isCompleted()))).toEqual([false, false]); + + fixture.componentInstance.oneGroup.setValue({oneCtrl: 'done'}); + await steps[1].select(); + + expect(await Promise.all(steps.map(step => step.isCompleted()))).toEqual([true, false]); + }); + +} + +@Component({ + template: ` + + + + + + + + + + + + + + + + + + + + One + + + Two + + + Three + + + + + +
+ +
+
+ +
+ +
+
+
+ ` +}) +class StepperHarnessTest { + oneGroup = new FormGroup({ + oneCtrl: new FormControl('', Validators.required) + }); + + twoGroup = new FormGroup({ + twoCtrl: new FormControl('', Validators.required) + }); +} diff --git a/src/material/stepper/testing/step-harness-filters.ts b/src/material/stepper/testing/step-harness-filters.ts new file mode 100644 index 000000000000..7e3c395a5b1b --- /dev/null +++ b/src/material/stepper/testing/step-harness-filters.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {BaseHarnessFilters} from '@angular/cdk/testing'; + +/** Possible orientations for a stepper. */ +export const enum StepperOrientation {HORIZONTAL, VERTICAL} + +/** A set of criteria that can be used to filter a list of `MatStepHarness` instances. */ +export interface StepHarnessFilters extends BaseHarnessFilters { + /** Only find instances whose label matches the given value. */ + label?: string | RegExp; + /** Only find steps with the given selected state. */ + selected?: boolean; + /** Only find completed steps. */ + completed?: boolean; + /** Only find steps that have errors. */ + invalid?: boolean; +} + +/** A set of criteria that can be used to filter a list of `MatStepperHarness` instances. */ +export interface StepperHarnessFilters extends BaseHarnessFilters { + /** Only find instances whose orientation matches the given value. */ + orientation?: StepperOrientation; +} + +/** + * A set of criteria that can be used to filter a list of + * `MatStepperNextHarness` and `MatStepperPreviousHarness` instances. + */ +export interface StepperButtonHarnessFilters extends BaseHarnessFilters { + /** Only find instances whose text matches the given value. */ + text?: string | RegExp; +} + diff --git a/src/material/stepper/testing/step-harness.ts b/src/material/stepper/testing/step-harness.ts new file mode 100644 index 000000000000..2a0c1d740ea8 --- /dev/null +++ b/src/material/stepper/testing/step-harness.ts @@ -0,0 +1,132 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { + ContentContainerComponentHarness, + HarnessPredicate, + HarnessLoader, + ComponentHarness, + HarnessQuery, +} from '@angular/cdk/testing'; +import {StepHarnessFilters} from './step-harness-filters'; + +/** Harness for interacting with a standard Angular Material step in tests. */ +export class MatStepHarness extends ContentContainerComponentHarness { + /** The selector for the host element of a `MatStep` instance. */ + static hostSelector = '.mat-step-header'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a `MatStepHarness` that meets + * certain criteria. + * @param options Options for filtering which steps are considered a match. + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: StepHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatStepHarness, options) + .addOption('label', options.label, + (harness, label) => HarnessPredicate.stringMatches(harness.getLabel(), label)) + .addOption('selected', options.selected, + async (harness, selected) => (await harness.isSelected()) === selected) + .addOption('completed', options.completed, + async (harness, completed) => (await harness.isCompleted()) === completed) + .addOption('invalid', options.invalid, + async (harness, invalid) => (await harness.hasErrors()) === invalid); + } + + /** Gets the label of the step. */ + async getLabel(): Promise { + return (await this.locatorFor('.mat-step-text-label')()).text(); + } + + /** Gets the `aria-label` of the step. */ + async getAriaLabel(): Promise { + return (await this.host()).getAttribute('aria-label'); + } + + /** Gets the value of the `aria-labelledby` attribute. */ + async getAriaLabelledby(): Promise { + return (await this.host()).getAttribute('aria-labelledby'); + } + + /** Whether the step is selected. */ + async isSelected(): Promise { + const host = await this.host(); + return (await host.getAttribute('aria-selected')) === 'true'; + } + + /** Whether the step has been filled out. */ + async isCompleted(): Promise { + const state = await this._getIconState(); + return state === 'done' || (state === 'edit' && !(await this.isSelected())); + } + + /** + * Whether the step is currently showing its error state. Note that this doesn't mean that there + * are or aren't any invalid form controls inside the step, but that the step is showing its + * error-specific styling which depends on there being invalid controls, as well as the + * `ErrorStateMatcher` determining that an error should be shown and that the `showErrors` + * option was enabled through the `STEPPER_GLOBAL_OPTIONS` injection token. + */ + async hasErrors(): Promise { + return (await this._getIconState()) === 'error'; + } + + /** Whether the step is optional. */ + async isOptional(): Promise { + // If the node with the optional text is present, it means that the step is optional. + const optionalNode = await this.locatorForOptional('.mat-step-optional')(); + return !!optionalNode; + } + + /** + * Selects the given step by clicking on the label. The step may not be selected + * if the stepper doesn't allow it (e.g. if there are validation errors). + */ + async select(): Promise { + await (await this.host()).click(); + } + + async getChildLoader(selector: string): Promise { + return (await this._getContentLoader()).getChildLoader(selector); + } + + async getAllChildLoaders(selector: string): Promise { + return (await this._getContentLoader()).getAllChildLoaders(selector); + } + + async getHarness(query: HarnessQuery): Promise { + return (await this._getContentLoader()).getHarness(query); + } + + async getAllHarnesses(query: HarnessQuery): Promise { + return (await this._getContentLoader()).getAllHarnesses(query); + } + + /** Gets the element id for the content of the current step. */ + private async _getContentLoader(): Promise { + const contentId = await (await this.host()).getAttribute('aria-controls'); + return this.documentRootLocatorFactory().harnessLoaderFor(`#${contentId}`); + } + + /** + * Gets the state of the step. Note that we have a `StepState` which we could use to type the + * return value, but it's basically the same as `string`, because the type has `| string`. + */ + private async _getIconState(): Promise { + // The state is exposed on the icon with a class that looks like `mat-step-icon-state-{{state}}` + const icon = await this.locatorFor('.mat-step-icon')(); + const classes = (await icon.getAttribute('class'))!; + const match = classes.match(/mat-step-icon-state-([a-z]+)/); + + if (!match) { + throw Error(`Could not determine step state from "${classes}".`); + } + + return match[1]; + } +} diff --git a/src/material/stepper/testing/stepper-button-harnesses.ts b/src/material/stepper/testing/stepper-button-harnesses.ts new file mode 100644 index 000000000000..cdac75a2317c --- /dev/null +++ b/src/material/stepper/testing/stepper-button-harnesses.ts @@ -0,0 +1,61 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing'; +import {StepperButtonHarnessFilters} from './step-harness-filters'; + +/** Base class for stepper button harnesses. */ +abstract class StepperButtonHarness extends ComponentHarness { + /** Gets the text of the button. */ + async getText(): Promise { + return (await this.host()).text(); + } + + /** Clicks the button. */ + async click(): Promise { + return (await this.host()).click(); + } +} + +/** Harness for interacting with a standard Angular Material stepper next button in tests. */ +export class MatStepperNextHarness extends StepperButtonHarness { + /** The selector for the host element of a `MatStep` instance. */ + static hostSelector = '.mat-stepper-next'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a `MatStepperNextHarness` that meets + * certain criteria. + * @param options Options for filtering which steps are considered a match. + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: StepperButtonHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatStepperNextHarness, options) + .addOption('text', options.text, + (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text)); + } +} + + +/** Harness for interacting with a standard Angular Material stepper previous button in tests. */ +export class MatStepperPreviousHarness extends StepperButtonHarness { + /** The selector for the host element of a `MatStep` instance. */ + static hostSelector = '.mat-stepper-previous'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a `MatStepperPreviousHarness` + * that meets certain criteria. + * @param options Options for filtering which steps are considered a match. + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: StepperButtonHarnessFilters = {}): + HarnessPredicate { + return new HarnessPredicate(MatStepperPreviousHarness, options) + .addOption('text', options.text, + (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text)); + } +} diff --git a/src/material/stepper/testing/stepper-harness.spec.ts b/src/material/stepper/testing/stepper-harness.spec.ts new file mode 100644 index 000000000000..2d613ae4a058 --- /dev/null +++ b/src/material/stepper/testing/stepper-harness.spec.ts @@ -0,0 +1,9 @@ +import {MatStepperModule} from '@angular/material/stepper'; +import {runHarnessTests} from '@angular/material/stepper/testing/shared.spec'; +import {MatStepperHarness} from './stepper-harness'; +import {MatStepperNextHarness, MatStepperPreviousHarness} from './stepper-button-harnesses'; + +describe('Non-MDC-based MatStepperHarness', () => { + runHarnessTests(MatStepperModule, MatStepperHarness, MatStepperNextHarness, + MatStepperPreviousHarness); +}); diff --git a/src/material/stepper/testing/stepper-harness.ts b/src/material/stepper/testing/stepper-harness.ts new file mode 100644 index 000000000000..5b50da9080cf --- /dev/null +++ b/src/material/stepper/testing/stepper-harness.ts @@ -0,0 +1,61 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing'; +import {MatStepHarness} from './step-harness'; +import { + StepperHarnessFilters, + StepHarnessFilters, + StepperOrientation, +} from './step-harness-filters'; + +/** Harness for interacting with a standard Material stepper in tests. */ +export class MatStepperHarness extends ComponentHarness { + /** The selector for the host element of a `MatStepper` instance. */ + static hostSelector = '.mat-stepper-horizontal, .mat-stepper-vertical'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a `MatStepperHarness` that meets + * certain criteria. + * @param options Options for filtering which stepper instances are considered a match. + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: StepperHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatStepperHarness, options) + .addOption('orientation', options.orientation, + async (harness, orientation) => (await harness.getOrientation()) === orientation); + } + + /** + * Gets the list of steps in the stepper. + * @param filter Optionally filters which steps are included. + */ + async getSteps(filter: StepHarnessFilters = {}): Promise { + return this.locatorForAll(MatStepHarness.with(filter))(); + } + + /** Gets the orientation of the stepper. */ + async getOrientation(): Promise { + const host = await this.host(); + return (await host.hasClass('mat-stepper-horizontal')) ? + StepperOrientation.HORIZONTAL : StepperOrientation.VERTICAL; + } + + /** + * Selects a step in this stepper. + * @param filter An optional filter to apply to the child steps. The first step matching the + * filter will be selected. + */ + async selectStep(filter: StepHarnessFilters = {}): Promise { + const steps = await this.getSteps(filter); + if (!steps.length) { + throw Error(`Cannot find mat-step matching filter ${JSON.stringify(filter)}`); + } + await steps[0].select(); + } +} diff --git a/tools/public_api_guard/material/stepper/testing.d.ts b/tools/public_api_guard/material/stepper/testing.d.ts new file mode 100644 index 000000000000..9b90af85241e --- /dev/null +++ b/tools/public_api_guard/material/stepper/testing.d.ts @@ -0,0 +1,54 @@ +export declare class MatStepHarness extends ContentContainerComponentHarness { + getAllChildLoaders(selector: string): Promise; + getAllHarnesses(query: HarnessQuery): Promise; + getAriaLabel(): Promise; + getAriaLabelledby(): Promise; + getChildLoader(selector: string): Promise; + getHarness(query: HarnessQuery): Promise; + getLabel(): Promise; + hasErrors(): Promise; + isCompleted(): Promise; + isOptional(): Promise; + isSelected(): Promise; + select(): Promise; + static hostSelector: string; + static with(options?: StepHarnessFilters): HarnessPredicate; +} + +export declare class MatStepperHarness extends ComponentHarness { + getOrientation(): Promise; + getSteps(filter?: StepHarnessFilters): Promise; + selectStep(filter?: StepHarnessFilters): Promise; + static hostSelector: string; + static with(options?: StepperHarnessFilters): HarnessPredicate; +} + +export declare class MatStepperNextHarness extends StepperButtonHarness { + static hostSelector: string; + static with(options?: StepperButtonHarnessFilters): HarnessPredicate; +} + +export declare class MatStepperPreviousHarness extends StepperButtonHarness { + static hostSelector: string; + static with(options?: StepperButtonHarnessFilters): HarnessPredicate; +} + +export interface StepHarnessFilters extends BaseHarnessFilters { + completed?: boolean; + invalid?: boolean; + label?: string | RegExp; + selected?: boolean; +} + +export interface StepperButtonHarnessFilters extends BaseHarnessFilters { + text?: string | RegExp; +} + +export interface StepperHarnessFilters extends BaseHarnessFilters { + orientation?: StepperOrientation; +} + +export declare const enum StepperOrientation { + HORIZONTAL = 0, + VERTICAL = 1 +}