From c1dd19a6b3b7f0c8066db946f77733183ad8398a Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Tue, 22 Oct 2019 11:43:03 +0200 Subject: [PATCH] feat(material-experimental): add test harness for mdc-slider * Adds a test harness for the mdc-slider that complies with the standard Angular Material slider test harness. --- .../mdc-slider/slider.ts | 15 +- .../mdc-slider/testing/BUILD.bazel | 40 +++++ .../mdc-slider/testing/index.ts | 9 ++ .../mdc-slider/testing/public-api.ts | 9 ++ .../mdc-slider/testing/slider-harness.spec.ts | 7 + .../mdc-slider/testing/slider-harness.ts | 137 ++++++++++++++++++ src/material/slider/testing/shared.spec.ts | 86 ++++++----- .../slider/testing/slider-harness.spec.ts | 2 +- src/material/slider/testing/slider-harness.ts | 13 +- 9 files changed, 274 insertions(+), 44 deletions(-) create mode 100644 src/material-experimental/mdc-slider/testing/BUILD.bazel create mode 100644 src/material-experimental/mdc-slider/testing/index.ts create mode 100644 src/material-experimental/mdc-slider/testing/public-api.ts create mode 100644 src/material-experimental/mdc-slider/testing/slider-harness.spec.ts create mode 100644 src/material-experimental/mdc-slider/testing/slider-harness.ts diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 99d874bdb844..61064430edc9 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -87,6 +87,9 @@ export class MatSliderChange { '[class.mat-slider-has-ticks]': 'tickInterval !== 0', '[class.mdc-slider--display-markers]': 'tickInterval !== 0', '[class.mat-slider-thumb-label-showing]': 'thumbLabel', + // Class binding which is only used by the test harness as there is no other + // way for the harness to detect if mouse coordinates need to be inverted. + '[class.mat-slider-invert-mouse-coords]': '_isRtl()', '[class.mat-slider-disabled]': 'disabled', '[class.mat-primary]': 'color == "primary"', '[class.mat-accent]': 'color == "accent"', @@ -295,7 +298,7 @@ export class MatSlider implements AfterViewInit, OnChanges, OnDestroy, ControlVa this._trackMarker.nativeElement.style.setProperty( 'background', this._getTrackMarkersBackground(min, max, step)); }, - isRTL: () => this._dir && this._dir.value === 'rtl', + isRTL: () => this._isRtl(), }; /** Instance of the MDC slider foundation for this slider. */ @@ -359,9 +362,12 @@ export class MatSlider implements AfterViewInit, OnChanges, OnDestroy, ControlVa // These bindings cannot be synced in the foundation, as the foundation is not // initialized and they cause DOM globals to be accessed (to move the thumb) this._syncStep(); - this._syncValue(); this._syncMax(); this._syncMin(); + + // Note that "value" needs to be synced after "max" and "min" because otherwise + // the value will be clamped by the MDC foundation implementation. + this._syncValue(); } this._syncDisabled(); @@ -494,6 +500,11 @@ export class MatSlider implements AfterViewInit, OnChanges, OnDestroy, ControlVa this._foundation.setDisabled(this.disabled); } + /** Whether the slider is displayed in RTL-mode. */ + _isRtl(): boolean { + return this._dir && this._dir.value === 'rtl'; + } + /** * Registers a callback to be triggered when the value has changed. * Implemented as part of ControlValueAccessor. diff --git a/src/material-experimental/mdc-slider/testing/BUILD.bazel b/src/material-experimental/mdc-slider/testing/BUILD.bazel new file mode 100644 index 000000000000..cc6cc3a1d80e --- /dev/null +++ b/src/material-experimental/mdc-slider/testing/BUILD.bazel @@ -0,0 +1,40 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library") + +ts_library( + name = "testing", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + module_name = "@angular/material-experimental/mdc-slider/testing", + deps = [ + "//src/cdk/coercion", + "//src/cdk/testing", + "//src/material/slider/testing", + "@npm//rxjs", + "@npm//zone.js", + ], +) + +ng_test_library( + name = "unit_tests_lib", + srcs = glob(["**/*.spec.ts"]), + deps = [ + ":testing", + "//src/material-experimental/mdc-slider", + "//src/material/slider/testing:harness_tests_lib", + ], +) + +ng_web_test_suite( + name = "unit_tests", + static_files = [ + "@npm//:node_modules/@material/slider/dist/mdc.slider.js", + ], + deps = [ + ":unit_tests_lib", + "//src/material-experimental:mdc_require_config.js", + ], +) diff --git a/src/material-experimental/mdc-slider/testing/index.ts b/src/material-experimental/mdc-slider/testing/index.ts new file mode 100644 index 000000000000..676ca90f1ffa --- /dev/null +++ b/src/material-experimental/mdc-slider/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-experimental/mdc-slider/testing/public-api.ts b/src/material-experimental/mdc-slider/testing/public-api.ts new file mode 100644 index 000000000000..508b6117f6df --- /dev/null +++ b/src/material-experimental/mdc-slider/testing/public-api.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 './slider-harness'; diff --git a/src/material-experimental/mdc-slider/testing/slider-harness.spec.ts b/src/material-experimental/mdc-slider/testing/slider-harness.spec.ts new file mode 100644 index 000000000000..d104043a5bb3 --- /dev/null +++ b/src/material-experimental/mdc-slider/testing/slider-harness.spec.ts @@ -0,0 +1,7 @@ +import {runHarnessTests} from '@angular/material/slider/testing/shared.spec'; +import {MatSliderModule} from '../index'; +import {MatSliderHarness} from './slider-harness'; + +describe('MDC-based MatSliderHarness', () => { + runHarnessTests(MatSliderModule, MatSliderHarness as any, false, false); +}); diff --git a/src/material-experimental/mdc-slider/testing/slider-harness.ts b/src/material-experimental/mdc-slider/testing/slider-harness.ts new file mode 100644 index 000000000000..810c6a23cabc --- /dev/null +++ b/src/material-experimental/mdc-slider/testing/slider-harness.ts @@ -0,0 +1,137 @@ +/** + * @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 {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion'; +import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing'; +import {SliderHarnessFilters} from '@angular/material/slider/testing'; + +/** + * Harness for interacting with a MDC mat-slider in tests. + * @dynamic + */ +export class MatSliderHarness extends ComponentHarness { + static hostSelector = 'mat-slider'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a mat-slider with + * specific attributes. + * @param options Options for narrowing the search: + * - `selector` finds a slider whose host element matches the given selector. + * - `id` finds a slider with specific id. + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: SliderHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatSliderHarness, options); + } + + private _textLabel = this.locatorForOptional('.mdc-slider__pin-value-marker'); + private _trackContainer = this.locatorFor('.mdc-slider__track-container'); + + /** Gets the slider's id. */ + async getId(): Promise { + const id = await (await this.host()).getProperty('id'); + // In case no id has been specified, the "id" property always returns + // an empty string. To make this method more explicit, we return null. + return id !== '' ? id : null; + } + + /** + * Gets the current display value of the slider. Returns null if the thumb + * label is disabled. + */ + async getDisplayValue(): Promise { + const textLabelEl = await this._textLabel(); + return textLabelEl ? textLabelEl.text() : null; + } + + /** Gets the current percentage value of the slider. */ + async getPercentage(): Promise { + return this._calculatePercentage(await this.getValue()); + } + + /** Gets the current value of the slider. */ + async getValue(): Promise { + return coerceNumberProperty(await (await this.host()).getAttribute('aria-valuenow')); + } + + /** Gets the maximum value of the slider. */ + async getMaxValue(): Promise { + return coerceNumberProperty(await (await this.host()).getAttribute('aria-valuemax')); + } + + /** Gets the minimum value of the slider. */ + async getMinValue(): Promise { + return coerceNumberProperty(await (await this.host()).getAttribute('aria-valuemin')); + } + + /** Whether the slider is disabled. */ + async isDisabled(): Promise { + const disabled = (await this.host()).getAttribute('aria-disabled'); + return coerceBooleanProperty(await disabled); + } + + /** Gets the orientation of the slider. */ + async getOrientation(): Promise<'horizontal'> { + // "aria-orientation" will always be set to "horizontal" for the MDC + // slider as there is no vertical slider support yet. + return (await this.host()).getAttribute('aria-orientation') as any; + } + + /** + * Sets the value of the slider by clicking on the slider track. + * + * Note that in rare cases the value cannot be set to the exact specified value. This + * can happen if not every value of the slider maps to a single pixel that could be + * clicked using mouse interaction. In such cases consider using the keyboard to + * select the given value or expand the slider's size for a better user experience. + */ + async setValue(value: number): Promise { + // Need to wait for async tasks outside Angular to complete. This is necessary because + // whenever directionality changes, the slider updates the element dimensions in the next + // tick (in a timer outside of the NgZone). Since this method relies on the element + // dimensions to be updated, we wait for the delayed calculation task to complete. + await this.waitForTasksOutsideAngular(); + + const [sliderEl, trackContainer] = + await Promise.all([this.host(), this._trackContainer()]); + let percentage = await this._calculatePercentage(value); + const {width} = await trackContainer.getDimensions(); + + // In case the slider is displayed in RTL mode, we need to invert the + // percentage so that the proper value is set. + if (await sliderEl.hasClass('mat-slider-invert-mouse-coords')) { + percentage = 1 - percentage; + } + + // We need to round the new coordinates because creating fake DOM + // events will cause the coordinates to be rounded down. + await sliderEl.click(Math.round(width * percentage), 0); + } + + /** + * Focuses the slider and returns a void promise that indicates when the + * action is complete. + */ + async focus(): Promise { + return (await this.host()).focus(); + } + + /** + * Blurs the slider and returns a void promise that indicates when the + * action is complete. + */ + async blur(): Promise { + return (await this.host()).blur(); + } + + /** Calculates the percentage of the given value. */ + private async _calculatePercentage(value: number) { + const [min, max] = await Promise.all([this.getMinValue(), this.getMaxValue()]); + return (value - min) / (max - min); + } +} diff --git a/src/material/slider/testing/shared.spec.ts b/src/material/slider/testing/shared.spec.ts index 9b1f97c5b0ef..99feb6113d10 100644 --- a/src/material/slider/testing/shared.spec.ts +++ b/src/material/slider/testing/shared.spec.ts @@ -1,6 +1,6 @@ import {HarnessLoader} from '@angular/cdk/testing'; import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; -import {Component} from '@angular/core'; +import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {MatSliderModule} from '@angular/material/slider'; import {MatSliderHarness} from '@angular/material/slider/testing/slider-harness'; @@ -8,7 +8,9 @@ import {MatSliderHarness} from '@angular/material/slider/testing/slider-harness' /** Shared tests to run on both the original and MDC-based sliders. */ export function runHarnessTests( sliderModule: typeof MatSliderModule, - sliderHarness: typeof MatSliderHarness) { + sliderHarness: typeof MatSliderHarness, + supportsVertical: boolean, + supportsInvert: boolean) { let fixture: ComponentFixture; let loader: HarnessLoader; @@ -17,6 +19,10 @@ export function runHarnessTests( .configureTestingModule({ imports: [sliderModule], declarations: [SliderHarnessTest], + // Use custom element schema since some inputs (like vertical or invert) do not + // exist in the MDC implementation of the slider. Though we still want to re-use + // the same test component. + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) .compileComponents(); @@ -72,7 +78,7 @@ export function runHarnessTests( it('should get display value of slider', async () => { const sliders = await loader.getAllHarnesses(sliderHarness); - expect(await sliders[0].getDisplayValue()).toBe('50'); + expect(await sliders[0].getDisplayValue()).toBe(null); expect(await sliders[1].getDisplayValue()).toBe('Null'); expect(await sliders[2].getDisplayValue()).toBe('#225'); }); @@ -81,18 +87,20 @@ export function runHarnessTests( const sliders = await loader.getAllHarnesses(sliderHarness); expect(await sliders[0].getOrientation()).toBe('horizontal'); expect(await sliders[1].getOrientation()).toBe('horizontal'); - expect(await sliders[2].getOrientation()).toBe('vertical'); + expect(await sliders[2].getOrientation()).toBe(supportsVertical ? 'vertical' : 'horizontal'); }); it('should be able to focus slider', async () => { - const [slider] = await loader.getAllHarnesses(sliderHarness); + // the first slider is disabled. + const slider = (await loader.getAllHarnesses(sliderHarness))[1]; expect(getActiveElementTagName()).not.toBe('mat-slider'); await slider.focus(); expect(getActiveElementTagName()).toBe('mat-slider'); }); it('should be able to blur slider', async () => { - const [slider] = await loader.getAllHarnesses(sliderHarness); + // the first slider is disabled. + const slider = (await loader.getAllHarnesses(sliderHarness))[1]; expect(getActiveElementTagName()).not.toBe('mat-slider'); await slider.focus(); expect(getActiveElementTagName()).toBe('mat-slider'); @@ -113,7 +121,7 @@ export function runHarnessTests( expect(await sliders[2].getValue()).toBe(250); }); - it('should be able to set value of slider in rtl', async () => { + fit('should be able to set value of slider in rtl', async () => { const sliders = await loader.getAllHarnesses(sliderHarness); expect(await sliders[1].getValue()).toBe(0); expect(await sliders[2].getValue()).toBe(225); @@ -127,43 +135,45 @@ export function runHarnessTests( expect(await sliders[1].getValue()).toBe(80); }); - it('should be able to set value of inverted slider', async () => { + it('should get disabled state of slider', async () => { const sliders = await loader.getAllHarnesses(sliderHarness); - expect(await sliders[1].getValue()).toBe(0); - expect(await sliders[2].getValue()).toBe(225); + expect(await sliders[0].isDisabled()).toBe(true); + expect(await sliders[1].isDisabled()).toBe(false); + expect(await sliders[2].isDisabled()).toBe(false); + }); - fixture.componentInstance.invertSliders = true; - fixture.detectChanges(); + if (supportsInvert) { + it('should be able to set value of inverted slider', async () => { + const sliders = await loader.getAllHarnesses(sliderHarness); + expect(await sliders[1].getValue()).toBe(0); + expect(await sliders[2].getValue()).toBe(225); - await sliders[1].setValue(75); - await sliders[2].setValue(210); + fixture.componentInstance.invertSliders = true; + fixture.detectChanges(); - expect(await sliders[1].getValue()).toBe(75); - expect(await sliders[2].getValue()).toBe(210); - }); + await sliders[1].setValue(75); + await sliders[2].setValue(210); - it('should be able to set value of inverted slider in rtl', async () => { - const sliders = await loader.getAllHarnesses(sliderHarness); - expect(await sliders[1].getValue()).toBe(0); - expect(await sliders[2].getValue()).toBe(225); + expect(await sliders[1].getValue()).toBe(75); + expect(await sliders[2].getValue()).toBe(210); + }); - fixture.componentInstance.invertSliders = true; - fixture.componentInstance.dir = 'rtl'; - fixture.detectChanges(); + it('should be able to set value of inverted slider in rtl', async () => { + const sliders = await loader.getAllHarnesses(sliderHarness); + expect(await sliders[1].getValue()).toBe(0); + expect(await sliders[2].getValue()).toBe(225); - await sliders[1].setValue(75); - await sliders[2].setValue(210); + fixture.componentInstance.invertSliders = true; + fixture.componentInstance.dir = 'rtl'; + fixture.detectChanges(); - expect(await sliders[1].getValue()).toBe(75); - expect(await sliders[2].getValue()).toBe(210); - }); + await sliders[1].setValue(75); + await sliders[2].setValue(210); - it('should get disabled state of slider', async () => { - const sliders = await loader.getAllHarnesses(sliderHarness); - expect(await sliders[0].isDisabled()).toBe(true); - expect(await sliders[1].isDisabled()).toBe(false); - expect(await sliders[2].isDisabled()).toBe(false); - }); + expect(await sliders[1].getValue()).toBe(75); + expect(await sliders[2].getValue()).toBe(210); + }); + } } function getActiveElementTagName() { @@ -175,12 +185,12 @@ function getActiveElementTagName() {
+ [invert]="invertSliders" thumbLabel>
+ [invert]="invertSliders" thumbLabel> - ` + `, }) class SliderHarnessTest { sliderId = 'my-slider'; diff --git a/src/material/slider/testing/slider-harness.spec.ts b/src/material/slider/testing/slider-harness.spec.ts index 6ca13c8300d5..aec65931a1c4 100644 --- a/src/material/slider/testing/slider-harness.spec.ts +++ b/src/material/slider/testing/slider-harness.spec.ts @@ -3,5 +3,5 @@ import {runHarnessTests} from '@angular/material/slider/testing/shared.spec'; import {MatSliderHarness} from './slider-harness'; describe('Non-MDC-based MatSliderHarness', () => { - runHarnessTests(MatSliderModule, MatSliderHarness); + runHarnessTests(MatSliderModule, MatSliderHarness, true, true); }); diff --git a/src/material/slider/testing/slider-harness.ts b/src/material/slider/testing/slider-harness.ts index 695ccae8a9a9..95f97ad1ac1f 100644 --- a/src/material/slider/testing/slider-harness.ts +++ b/src/material/slider/testing/slider-harness.ts @@ -40,9 +40,16 @@ export class MatSliderHarness extends ComponentHarness { return id !== '' ? id : null; } - /** Gets the current display value of the slider. */ - async getDisplayValue(): Promise { - return (await this._textLabel()).text(); + /** + * Gets the current display value of the slider. Returns null if the thumb + * label is disabled. + */ + async getDisplayValue(): Promise { + const [host, textLabel] = await Promise.all([this.host(), this._textLabel()]); + if (await host.hasClass('mat-slider-thumb-label-showing')) { + return textLabel.text(); + } + return null; } /** Gets the current percentage value of the slider. */