Skip to content

Commit cc69ceb

Browse files
committed
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.
1 parent 1c74518 commit cc69ceb

File tree

5 files changed

+225
-44
lines changed

5 files changed

+225
-44
lines changed

src/material-experimental/mdc-slider/harness/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ ng_test_library(
2121
":harness",
2222
"//src/cdk-experimental/testing",
2323
"//src/cdk-experimental/testing/testbed",
24+
"//src/material-experimental/mdc-slider",
2425
"//src/material/slider",
2526
],
2627
)
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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 {ComponentHarness, HarnessPredicate} from '@angular/cdk-experimental/testing';
10+
import {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion';
11+
import {SliderHarnessFilters} from './slider-harness-filters';
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 any;
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+
await this._stabilizeLayout();
95+
const [sliderEl, trackContainer] =
96+
await Promise.all([this.host(), this._trackContainer()]);
97+
let percentage = await this._calculatePercentage(value);
98+
const {width} = await trackContainer.getDimensions();
99+
100+
// In case the slider is displayed in RTL mode, we need to invert the
101+
// percentage so that the proper value is set.
102+
if (await sliderEl.hasClass('mat-slider-invert-mouse-coords')) {
103+
percentage = 1 - percentage;
104+
}
105+
106+
// We need to round the new coordinates because creating fake DOM
107+
// events will cause the coordinates to be rounded down.
108+
await sliderEl.click(Math.round(width * percentage), 0);
109+
}
110+
111+
/**
112+
* Focuses the slider and returns a void promise that indicates when the
113+
* action is complete.
114+
*/
115+
async focus(): Promise<void> {
116+
return (await this.host()).focus();
117+
}
118+
119+
/**
120+
* Blurs the slider and returns a void promise that indicates when the
121+
* action is complete.
122+
*/
123+
async blur(): Promise<void> {
124+
return (await this.host()).blur();
125+
}
126+
127+
/** Calculates the percentage of the given value. */
128+
private async _calculatePercentage(value: number) {
129+
const [min, max] = await Promise.all([this.getMinValue(), this.getMaxValue()]);
130+
return (value - min) / (max - min);
131+
}
132+
133+
private async _stabilizeLayout() {
134+
return new Promise(resolve => {
135+
requestAnimationFrame(() => resolve());
136+
});
137+
}
138+
}

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

Lines changed: 64 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import {HarnessLoader} from '@angular/cdk-experimental/testing';
22
import {TestbedHarnessEnvironment} from '@angular/cdk-experimental/testing/testbed';
3-
import {Component} from '@angular/core';
3+
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
44
import {ComponentFixture, TestBed} from '@angular/core/testing';
55
import {MatSliderModule} from '@angular/material/slider';
6+
import {MatSliderModule as MatMdcSliderModule} from '../module';
7+
import {MatSliderHarness as MatMdcSliderHarness} from './mdc-slider-harness';
68
import {MatSliderHarness} from './slider-harness';
79

810
let fixture: ComponentFixture<SliderHarnessTest>;
@@ -25,18 +27,37 @@ describe('MatSliderHarness', () => {
2527
sliderHarness = MatSliderHarness;
2628
});
2729

28-
runTests();
30+
// Standard slider supports vertical and inverted sliders.
31+
createTests(true, true);
2932
});
3033

3134
describe(
3235
'MDC-based',
3336
() => {
34-
// TODO: run tests for MDC based slider once implemented.
37+
beforeEach(async () => {
38+
await TestBed
39+
.configureTestingModule({
40+
imports: [MatMdcSliderModule],
41+
declarations: [SliderHarnessTest],
42+
schemas: [CUSTOM_ELEMENTS_SCHEMA]
43+
})
44+
.compileComponents();
45+
46+
fixture = TestBed.createComponent(SliderHarnessTest);
47+
fixture.detectChanges();
48+
loader = TestbedHarnessEnvironment.loader(fixture);
49+
// Public APIs are the same as "MatSliderHarness", but cast is necessary because
50+
// of different private fields.
51+
sliderHarness = MatMdcSliderHarness as any;
52+
});
53+
54+
// MDC slider does not support vertical or inverted sliders.
55+
createTests(false, false);
3556
});
3657
});
3758

3859
/** Shared tests to run on both the original and MDC-based sliders. */
39-
function runTests() {
60+
function createTests(supportsVertical: boolean, supportsInvert: boolean) {
4061
it('should load all slider harnesses', async () => {
4162
const sliders = await loader.getAllHarnesses(sliderHarness);
4263
expect(sliders.length).toBe(3);
@@ -84,7 +105,7 @@ function runTests() {
84105

85106
it('should get display value of slider', async () => {
86107
const sliders = await loader.getAllHarnesses(sliderHarness);
87-
expect(await sliders[0].getDisplayValue()).toBe('50');
108+
expect(await sliders[0].getDisplayValue()).toBe(null);
88109
expect(await sliders[1].getDisplayValue()).toBe('Null');
89110
expect(await sliders[2].getDisplayValue()).toBe('#225');
90111
});
@@ -93,18 +114,20 @@ function runTests() {
93114
const sliders = await loader.getAllHarnesses(sliderHarness);
94115
expect(await sliders[0].getOrientation()).toBe('horizontal');
95116
expect(await sliders[1].getOrientation()).toBe('horizontal');
96-
expect(await sliders[2].getOrientation()).toBe('vertical');
117+
expect(await sliders[2].getOrientation()).toBe(supportsVertical ? 'vertical' : 'horizontal');
97118
});
98119

99120
it('should be able to focus slider', async () => {
100-
const [slider] = await loader.getAllHarnesses(sliderHarness);
121+
// the first slider is disabled.
122+
const slider = (await loader.getAllHarnesses(sliderHarness))[1];
101123
expect(getActiveElementTagName()).not.toBe('mat-slider');
102124
await slider.focus();
103125
expect(getActiveElementTagName()).toBe('mat-slider');
104126
});
105127

106128
it('should be able to blur slider', async () => {
107-
const [slider] = await loader.getAllHarnesses(sliderHarness);
129+
// the first slider is disabled.
130+
const slider = (await loader.getAllHarnesses(sliderHarness))[1];
108131
expect(getActiveElementTagName()).not.toBe('mat-slider');
109132
await slider.focus();
110133
expect(getActiveElementTagName()).toBe('mat-slider');
@@ -139,43 +162,45 @@ function runTests() {
139162
expect(await sliders[1].getValue()).toBe(80);
140163
});
141164

142-
it('should be able to set value of inverted slider', async () => {
165+
it('should get disabled state of slider', async () => {
143166
const sliders = await loader.getAllHarnesses(sliderHarness);
144-
expect(await sliders[1].getValue()).toBe(0);
145-
expect(await sliders[2].getValue()).toBe(225);
167+
expect(await sliders[0].isDisabled()).toBe(true);
168+
expect(await sliders[1].isDisabled()).toBe(false);
169+
expect(await sliders[2].isDisabled()).toBe(false);
170+
});
146171

147-
fixture.componentInstance.invertSliders = true;
148-
fixture.detectChanges();
172+
if (supportsInvert) {
173+
it('should be able to set value of inverted slider', async () => {
174+
const sliders = await loader.getAllHarnesses(sliderHarness);
175+
expect(await sliders[1].getValue()).toBe(0);
176+
expect(await sliders[2].getValue()).toBe(225);
149177

150-
await sliders[1].setValue(75);
151-
await sliders[2].setValue(210);
178+
fixture.componentInstance.invertSliders = true;
179+
fixture.detectChanges();
152180

153-
expect(await sliders[1].getValue()).toBe(75);
154-
expect(await sliders[2].getValue()).toBe(210);
155-
});
181+
await sliders[1].setValue(75);
182+
await sliders[2].setValue(210);
156183

157-
it('should be able to set value of inverted slider in rtl', async () => {
158-
const sliders = await loader.getAllHarnesses(sliderHarness);
159-
expect(await sliders[1].getValue()).toBe(0);
160-
expect(await sliders[2].getValue()).toBe(225);
184+
expect(await sliders[1].getValue()).toBe(75);
185+
expect(await sliders[2].getValue()).toBe(210);
186+
});
161187

162-
fixture.componentInstance.invertSliders = true;
163-
fixture.componentInstance.dir = 'rtl';
164-
fixture.detectChanges();
188+
it('should be able to set value of inverted slider in rtl', async () => {
189+
const sliders = await loader.getAllHarnesses(sliderHarness);
190+
expect(await sliders[1].getValue()).toBe(0);
191+
expect(await sliders[2].getValue()).toBe(225);
165192

166-
await sliders[1].setValue(75);
167-
await sliders[2].setValue(210);
193+
fixture.componentInstance.invertSliders = true;
194+
fixture.componentInstance.dir = 'rtl';
195+
fixture.detectChanges();
168196

169-
expect(await sliders[1].getValue()).toBe(75);
170-
expect(await sliders[2].getValue()).toBe(210);
171-
});
197+
await sliders[1].setValue(75);
198+
await sliders[2].setValue(210);
172199

173-
it('should get disabled state of slider', async () => {
174-
const sliders = await loader.getAllHarnesses(sliderHarness);
175-
expect(await sliders[0].isDisabled()).toBe(true);
176-
expect(await sliders[1].isDisabled()).toBe(false);
177-
expect(await sliders[2].isDisabled()).toBe(false);
178-
});
200+
expect(await sliders[1].getValue()).toBe(75);
201+
expect(await sliders[2].getValue()).toBe(210);
202+
});
203+
}
179204
}
180205

181206
function getActiveElementTagName() {
@@ -187,12 +212,12 @@ function getActiveElementTagName() {
187212
<mat-slider value="50" disabled></mat-slider>
188213
<div [dir]="dir">
189214
<mat-slider [id]="sliderId" [displayWith]="displayFn"
190-
[invert]="invertSliders"></mat-slider>
215+
[invert]="invertSliders" thumbLabel></mat-slider>
191216
</div>
192217
<mat-slider min="200" max="250" value="225" [displayWith]="displayFn" vertical
193-
[invert]="invertSliders">
218+
[invert]="invertSliders" thumbLabel>
194219
</mat-slider>
195-
`
220+
`,
196221
})
197222
class SliderHarnessTest {
198223
sliderId = 'my-slider';

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,16 @@ export class MatSliderHarness extends ComponentHarness {
4040
return id !== '' ? id : null;
4141
}
4242

43-
/** Gets the current display value of the slider. */
44-
async getDisplayValue(): Promise<string> {
45-
return (await this._textLabel()).text();
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 [host, textLabel] = await Promise.all([this.host(), this._textLabel()]);
49+
if (await host.hasClass('mat-slider-thumb-label-showing')) {
50+
return textLabel.text();
51+
}
52+
return null;
4653
}
4754

4855
/** Gets the current percentage value of the slider. */

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ export class MatSliderChange {
9090
'[class.mdc-slider--discrete]': 'thumbLabel',
9191
'[class.mat-slider-has-ticks]': 'tickInterval !== 0',
9292
'[class.mat-slider-thumb-label-showing]': 'thumbLabel',
93+
// Class binding which is only used by the test harness as there is no other
94+
// way for the harness to detect if mouse coordinates need to be inverted.
95+
'[class.mat-slider-invert-mouse-coords]': '_isRtl()',
9396
'[class.mat-slider-disabled]': 'disabled',
9497
'[class.mat-primary]': 'color == "primary"',
9598
'[class.mat-accent]': 'color == "accent"',
@@ -298,7 +301,7 @@ export class MatSlider implements AfterViewInit, OnChanges, OnDestroy, ControlVa
298301
this._trackMarker.nativeElement.style.setProperty(
299302
'background', this._getTrackMarkersBackground(min, max, step));
300303
},
301-
isRTL: () => this._dir && this._dir.value === 'rtl',
304+
isRTL: () => this._isRtl(),
302305
};
303306

304307
/** Instance of the MDC slider foundation for this slider. */
@@ -352,10 +355,12 @@ export class MatSlider implements AfterViewInit, OnChanges, OnDestroy, ControlVa
352355
(this._foundation as any).isDiscrete_ = true;
353356

354357
this._syncStep();
355-
this._syncValue();
356358
this._syncMax();
357359
this._syncMin();
358360
this._syncDisabled();
361+
// Note that "value" needs to be synced after "max" and "min" because otherwise
362+
// the value will be clamped by the MDC foundation implementation.
363+
this._syncValue();
359364
}
360365

361366
ngOnChanges(changes: SimpleChanges) {
@@ -476,6 +481,11 @@ export class MatSlider implements AfterViewInit, OnChanges, OnDestroy, ControlVa
476481
this._foundation.setDisabled(this.disabled);
477482
}
478483

484+
/** Whether the slider is displayed in RTL-mode. */
485+
_isRtl(): boolean {
486+
return this._dir && this._dir.value === 'rtl';
487+
}
488+
479489
/**
480490
* Registers a callback to be triggered when the value has changed.
481491
* Implemented as part of ControlValueAccessor.

0 commit comments

Comments
 (0)