Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(material-experimental): add test harness for mdc-slider #16978

Merged
merged 1 commit into from
Nov 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}
}