Skip to content

Commit

Permalink
feat(material-experimental): add test harness for mdc-slider
Browse files Browse the repository at this point in the history
* Adds a test harness for the mdc-slider that complies with the
standard Angular Material slider test harness.
  • Loading branch information
devversion committed Oct 24, 2019
1 parent 646d47f commit 312a6a3
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 43 deletions.
15 changes: 13 additions & 2 deletions src/material-experimental/mdc-slider/slider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"',
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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.
Expand Down
40 changes: 40 additions & 0 deletions src/material-experimental/mdc-slider/testing/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
],
)
9 changes: 9 additions & 0 deletions src/material-experimental/mdc-slider/testing/index.ts
Original file line number Diff line number Diff line change
@@ -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';
9 changes: 9 additions & 0 deletions src/material-experimental/mdc-slider/testing/public-api.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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, {
supportsVertical: false,
supportsInvert: false,
});
});
137 changes: 137 additions & 0 deletions src/material-experimental/mdc-slider/testing/slider-harness.ts
Original file line number Diff line number Diff line change
@@ -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<MatSliderHarness> {
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<string|null> {
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<string|null> {
const textLabelEl = await this._textLabel();
return textLabelEl ? textLabelEl.text() : null;
}

/** Gets the current percentage value of the slider. */
async getPercentage(): Promise<number> {
return this._calculatePercentage(await this.getValue());
}

/** Gets the current value of the slider. */
async getValue(): Promise<number> {
return coerceNumberProperty(await (await this.host()).getAttribute('aria-valuenow'));
}

/** Gets the maximum value of the slider. */
async getMaxValue(): Promise<number> {
return coerceNumberProperty(await (await this.host()).getAttribute('aria-valuemax'));
}

/** Gets the minimum value of the slider. */
async getMinValue(): Promise<number> {
return coerceNumberProperty(await (await this.host()).getAttribute('aria-valuemin'));
}

/** Whether the slider is disabled. */
async isDisabled(): Promise<boolean> {
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 Promise<'horizontal'>;
}

/**
* 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<void> {
// 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<void> {
return (await this.host()).focus();
}

/**
* Blurs the slider and returns a void promise that indicates when the
* action is complete.
*/
async blur(): Promise<void> {
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);
}
}

0 comments on commit 312a6a3

Please sign in to comment.