Skip to content

Commit 7cd314c

Browse files
devversionmmalerba
authored andcommitted
feat(material-experimental): add test harness for mdc-slider (#16978)
* Adds a test harness for the mdc-slider that complies with the standard Angular Material slider test harness.
1 parent 4adb2fd commit 7cd314c

File tree

9 files changed

+279
-43
lines changed

9 files changed

+279
-43
lines changed

src/material-experimental/mdc-slider/slider.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ export class MatSliderChange {
8787
'[class.mat-slider-has-ticks]': 'tickInterval !== 0',
8888
'[class.mdc-slider--display-markers]': 'tickInterval !== 0',
8989
'[class.mat-slider-thumb-label-showing]': 'thumbLabel',
90+
// Class binding which is only used by the test harness as there is no other
91+
// way for the harness to detect if mouse coordinates need to be inverted.
92+
'[class.mat-slider-invert-mouse-coords]': '_isRtl()',
9093
'[class.mat-slider-disabled]': 'disabled',
9194
'[class.mat-primary]': 'color == "primary"',
9295
'[class.mat-accent]': 'color == "accent"',
@@ -295,7 +298,7 @@ export class MatSlider implements AfterViewInit, OnChanges, OnDestroy, ControlVa
295298
this._trackMarker.nativeElement.style.setProperty(
296299
'background', this._getTrackMarkersBackground(min, max, step));
297300
},
298-
isRTL: () => this._dir && this._dir.value === 'rtl',
301+
isRTL: () => this._isRtl(),
299302
};
300303

301304
/** Instance of the MDC slider foundation for this slider. */
@@ -359,9 +362,12 @@ export class MatSlider implements AfterViewInit, OnChanges, OnDestroy, ControlVa
359362
// These bindings cannot be synced in the foundation, as the foundation is not
360363
// initialized and they cause DOM globals to be accessed (to move the thumb)
361364
this._syncStep();
362-
this._syncValue();
363365
this._syncMax();
364366
this._syncMin();
367+
368+
// Note that "value" needs to be synced after "max" and "min" because otherwise
369+
// the value will be clamped by the MDC foundation implementation.
370+
this._syncValue();
365371
}
366372

367373
this._syncDisabled();
@@ -494,6 +500,11 @@ export class MatSlider implements AfterViewInit, OnChanges, OnDestroy, ControlVa
494500
this._foundation.setDisabled(this.disabled);
495501
}
496502

503+
/** Whether the slider is displayed in RTL-mode. */
504+
_isRtl(): boolean {
505+
return this._dir && this._dir.value === 'rtl';
506+
}
507+
497508
/**
498509
* Registers a callback to be triggered when the value has changed.
499510
* Implemented as part of ControlValueAccessor.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library")
4+
5+
ts_library(
6+
name = "testing",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
module_name = "@angular/material-experimental/mdc-slider/testing",
12+
deps = [
13+
"//src/cdk/coercion",
14+
"//src/cdk/testing",
15+
"//src/material/slider/testing",
16+
"@npm//rxjs",
17+
"@npm//zone.js",
18+
],
19+
)
20+
21+
ng_test_library(
22+
name = "unit_tests_lib",
23+
srcs = glob(["**/*.spec.ts"]),
24+
deps = [
25+
":testing",
26+
"//src/material-experimental/mdc-slider",
27+
"//src/material/slider/testing:harness_tests_lib",
28+
],
29+
)
30+
31+
ng_web_test_suite(
32+
name = "unit_tests",
33+
static_files = [
34+
"@npm//:node_modules/@material/slider/dist/mdc.slider.js",
35+
],
36+
deps = [
37+
":unit_tests_lib",
38+
"//src/material-experimental:mdc_require_config.js",
39+
],
40+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export * from './public-api';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export * from './slider-harness';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {runHarnessTests} from '@angular/material/slider/testing/shared.spec';
2+
import {MatSliderModule} from '../index';
3+
import {MatSliderHarness} from './slider-harness';
4+
5+
describe('MDC-based MatSliderHarness', () => {
6+
runHarnessTests(MatSliderModule, MatSliderHarness as any, {
7+
supportsVertical: false,
8+
supportsInvert: false,
9+
});
10+
});
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion';
10+
import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
11+
import {SliderHarnessFilters} from '@angular/material/slider/testing';
12+
13+
/**
14+
* Harness for interacting with a MDC mat-slider in tests.
15+
* @dynamic
16+
*/
17+
export class MatSliderHarness extends ComponentHarness {
18+
static hostSelector = 'mat-slider';
19+
20+
/**
21+
* Gets a `HarnessPredicate` that can be used to search for a mat-slider with
22+
* specific attributes.
23+
* @param options Options for narrowing the search:
24+
* - `selector` finds a slider whose host element matches the given selector.
25+
* - `id` finds a slider with specific id.
26+
* @return a `HarnessPredicate` configured with the given options.
27+
*/
28+
static with(options: SliderHarnessFilters = {}): HarnessPredicate<MatSliderHarness> {
29+
return new HarnessPredicate(MatSliderHarness, options);
30+
}
31+
32+
private _textLabel = this.locatorForOptional('.mdc-slider__pin-value-marker');
33+
private _trackContainer = this.locatorFor('.mdc-slider__track-container');
34+
35+
/** Gets the slider's id. */
36+
async getId(): Promise<string|null> {
37+
const id = await (await this.host()).getProperty('id');
38+
// In case no id has been specified, the "id" property always returns
39+
// an empty string. To make this method more explicit, we return null.
40+
return id !== '' ? id : null;
41+
}
42+
43+
/**
44+
* Gets the current display value of the slider. Returns null if the thumb
45+
* label is disabled.
46+
*/
47+
async getDisplayValue(): Promise<string|null> {
48+
const textLabelEl = await this._textLabel();
49+
return textLabelEl ? textLabelEl.text() : null;
50+
}
51+
52+
/** Gets the current percentage value of the slider. */
53+
async getPercentage(): Promise<number> {
54+
return this._calculatePercentage(await this.getValue());
55+
}
56+
57+
/** Gets the current value of the slider. */
58+
async getValue(): Promise<number> {
59+
return coerceNumberProperty(await (await this.host()).getAttribute('aria-valuenow'));
60+
}
61+
62+
/** Gets the maximum value of the slider. */
63+
async getMaxValue(): Promise<number> {
64+
return coerceNumberProperty(await (await this.host()).getAttribute('aria-valuemax'));
65+
}
66+
67+
/** Gets the minimum value of the slider. */
68+
async getMinValue(): Promise<number> {
69+
return coerceNumberProperty(await (await this.host()).getAttribute('aria-valuemin'));
70+
}
71+
72+
/** Whether the slider is disabled. */
73+
async isDisabled(): Promise<boolean> {
74+
const disabled = (await this.host()).getAttribute('aria-disabled');
75+
return coerceBooleanProperty(await disabled);
76+
}
77+
78+
/** Gets the orientation of the slider. */
79+
async getOrientation(): Promise<'horizontal'> {
80+
// "aria-orientation" will always be set to "horizontal" for the MDC
81+
// slider as there is no vertical slider support yet.
82+
return (await this.host()).getAttribute('aria-orientation') as Promise<'horizontal'>;
83+
}
84+
85+
/**
86+
* Sets the value of the slider by clicking on the slider track.
87+
*
88+
* Note that in rare cases the value cannot be set to the exact specified value. This
89+
* can happen if not every value of the slider maps to a single pixel that could be
90+
* clicked using mouse interaction. In such cases consider using the keyboard to
91+
* select the given value or expand the slider's size for a better user experience.
92+
*/
93+
async setValue(value: number): Promise<void> {
94+
// Need to wait for async tasks outside Angular to complete. This is necessary because
95+
// whenever directionality changes, the slider updates the element dimensions in the next
96+
// tick (in a timer outside of the NgZone). Since this method relies on the element
97+
// dimensions to be updated, we wait for the delayed calculation task to complete.
98+
await this.waitForTasksOutsideAngular();
99+
100+
const [sliderEl, trackContainer] =
101+
await Promise.all([this.host(), this._trackContainer()]);
102+
let percentage = await this._calculatePercentage(value);
103+
const {width} = await trackContainer.getDimensions();
104+
105+
// In case the slider is displayed in RTL mode, we need to invert the
106+
// percentage so that the proper value is set.
107+
if (await sliderEl.hasClass('mat-slider-invert-mouse-coords')) {
108+
percentage = 1 - percentage;
109+
}
110+
111+
// We need to round the new coordinates because creating fake DOM
112+
// events will cause the coordinates to be rounded down.
113+
await sliderEl.click(Math.round(width * percentage), 0);
114+
}
115+
116+
/**
117+
* Focuses the slider and returns a void promise that indicates when the
118+
* action is complete.
119+
*/
120+
async focus(): Promise<void> {
121+
return (await this.host()).focus();
122+
}
123+
124+
/**
125+
* Blurs the slider and returns a void promise that indicates when the
126+
* action is complete.
127+
*/
128+
async blur(): Promise<void> {
129+
return (await this.host()).blur();
130+
}
131+
132+
/** Calculates the percentage of the given value. */
133+
private async _calculatePercentage(value: number) {
134+
const [min, max] = await Promise.all([this.getMinValue(), this.getMaxValue()]);
135+
return (value - min) / (max - min);
136+
}
137+
}

0 commit comments

Comments
 (0)