Skip to content

Commit d0c8d50

Browse files
committed
fixup! feat(material-experimental): add test harness for slider
Support setting value
1 parent 7b9354f commit d0c8d50

File tree

13 files changed

+146
-21
lines changed

13 files changed

+146
-21
lines changed

WORKSPACE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,4 @@ rbe_autoconfig(
107107
# We can't use the default "ubuntu16_04" RBE image provided by the autoconfig because we need
108108
# a specific Linux kernel that comes with "libx11" in order to run headless browser tests.
109109
repository = "google/rbe-ubuntu16-04-webtest",
110-
)
110+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
/**
10+
* Dimensions for element size and its position relative to the viewport.
11+
*/
12+
export interface ElementDimensions {
13+
top: number;
14+
left: number;
15+
width: number;
16+
height: number;
17+
}

src/cdk-experimental/testing/protractor/protractor-element.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {browser, ElementFinder} from 'protractor';
1010
import {TestElement} from '../test-element';
11+
import {ElementDimensions} from '../element-dimensions';
1112

1213
/** A `TestElement` implementation for Protractor. */
1314
export class ProtractorElement implements TestElement {
@@ -21,8 +22,11 @@ export class ProtractorElement implements TestElement {
2122
return this.element.clear();
2223
}
2324

24-
async click(): Promise<void> {
25-
return this.element.click();
25+
async click(relativeX = 0, relativeY = 0): Promise<void> {
26+
await browser.actions()
27+
.mouseMove(await this.element.getWebElement(), {x: relativeX, y: relativeY})
28+
.click()
29+
.perform();
2630
}
2731

2832
async focus(): Promise<void> {
@@ -55,4 +59,10 @@ export class ProtractorElement implements TestElement {
5559
const classes = (await this.getAttribute('class')) || '';
5660
return new Set(classes.split(/\s+/).filter(c => c)).has(name);
5761
}
62+
63+
async getDimensions(): Promise<ElementDimensions> {
64+
const {width, height} = await this.element.getSize();
65+
const {x: left, y: top} = await this.element.getLocation();
66+
return {width, height, left, top};
67+
}
5868
}

src/cdk-experimental/testing/test-element.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {ElementDimensions} from './element-dimensions';
10+
911
/**
1012
* This acts as a common interface for DOM elements across both unit and e2e tests. It is the
1113
* interface through which the ComponentHarness interacts with the component's DOM.
@@ -18,7 +20,7 @@ export interface TestElement {
1820
clear(): Promise<void>;
1921

2022
/** Click the element. */
21-
click(): Promise<void>;
23+
click(relativeX?: number, relativeY?: number): Promise<void>;
2224

2325
/** Focus the element. */
2426
focus(): Promise<void>;
@@ -46,4 +48,7 @@ export interface TestElement {
4648

4749
/** Checks whether the element has the given class. */
4850
hasClass(name: string): Promise<boolean>;
51+
52+
/** Gets the dimensions of the element. */
53+
getDimensions(): Promise<ElementDimensions>;
4954
}

src/cdk-experimental/testing/testbed/unit-test-element.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
triggerFocus
1515
} from '@angular/cdk/testing';
1616
import {TestElement} from '../test-element';
17+
import {ElementDimensions} from '../element-dimensions';
1718

1819
function isTextInput(element: Element): element is HTMLInputElement | HTMLTextAreaElement {
1920
return element.nodeName.toLowerCase() === 'input' ||
@@ -41,9 +42,14 @@ export class UnitTestElement implements TestElement {
4142
await this._stabilize();
4243
}
4344

44-
async click(): Promise<void> {
45+
async click(relativeX = 0, relativeY = 0): Promise<void> {
4546
await this._stabilize();
46-
dispatchMouseEvent(this.element, 'click');
47+
const {left, top} = this.element.getBoundingClientRect();
48+
const pageX = left + relativeX;
49+
const pageY = top + relativeY;
50+
dispatchMouseEvent(this.element, 'mousedown', pageX, pageY);
51+
dispatchMouseEvent(this.element, 'mouseup', pageX, pageY);
52+
dispatchMouseEvent(this.element, 'click', pageX, pageY);
4753
await this._stabilize();
4854
}
4955

@@ -105,4 +111,9 @@ export class UnitTestElement implements TestElement {
105111
await this._stabilize();
106112
return this.element.classList.contains(name);
107113
}
114+
115+
async getDimensions(): Promise<ElementDimensions> {
116+
await this._stabilize();
117+
return this.element.getBoundingClientRect();
118+
}
108119
}

src/cdk-experimental/testing/tests/protractor.e2e.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,11 @@ describe('ProtractorHarnessEnvironment', () => {
196196
.toBe(await browser.driver.switchTo().activeElement().getAttribute('id'));
197197
});
198198

199+
it('should be able to retrieve dimensions', async () => {
200+
const dimensions = await (await harness.title()).getDimensions();
201+
expect(dimensions).toEqual(jasmine.objectContaining({height: 100, width: 200}));
202+
});
203+
199204
it('should be able to hover', async () => {
200205
const host = await harness.host();
201206
let classAttr = await host.getAttribute('class');
@@ -217,7 +222,7 @@ describe('ProtractorHarnessEnvironment', () => {
217222

218223
it('should be able to getCssValue', async () => {
219224
const title = await harness.title();
220-
expect(await title.getCssValue('height')).toBe('50px');
225+
expect(await title.getCssValue('height')).toBe('100px');
221226
});
222227

223228
it('should focus and blur element', async () => {

src/cdk-experimental/testing/tests/test-main-component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<h1 style="height: 50px">Main Component</h1>
1+
<h1 style="height: 100px; width: 200px;">Main Component</h1>
22
<div id="username">Hello {{username}} from Angular 2!</div>
33
<div class="counters">
44
<button (click)="click()">Up</button><br>

src/cdk-experimental/testing/tests/testbed.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ describe('TestbedHarnessEnvironment', () => {
215215
expect(await input.getAttribute('id')).toBe(document.activeElement!.id);
216216
});
217217

218+
it('should be able to retrieve dimensions', async () => {
219+
const dimensions = await (await harness.title()).getDimensions();
220+
expect(dimensions).toEqual(jasmine.objectContaining({height: 100, width: 200}));
221+
});
222+
218223
it('should be able to hover', async () => {
219224
const host = await harness.host();
220225
let classAttr = await host.getAttribute('class');
@@ -236,7 +241,7 @@ describe('TestbedHarnessEnvironment', () => {
236241

237242
it('should be able to getCssValue', async () => {
238243
const title = await harness.title();
239-
expect(await title.getCssValue('height')).toBe('50px');
244+
expect(await title.getCssValue('height')).toBe('100px');
240245
});
241246

242247
it('should focus and blur element', async () => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ ng_test_library(
2828
ng_web_test_suite(
2929
name = "tests",
3030
deps = [":harness_tests"],
31-
)
31+
)

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

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {HarnessLoader} from '@angular/cdk-experimental/testing';
22
import {TestbedHarnessEnvironment} from '@angular/cdk-experimental/testing/testbed';
33
import {Component} from '@angular/core';
44
import {ComponentFixture, TestBed} from '@angular/core/testing';
5-
import {ReactiveFormsModule} from '@angular/forms';
65
import {MatSliderModule} from '@angular/material/slider';
76
import {MatSliderHarness} from './slider-harness';
87

@@ -15,7 +14,7 @@ describe('MatSliderHarness', () => {
1514
beforeEach(async () => {
1615
await TestBed
1716
.configureTestingModule({
18-
imports: [MatSliderModule, ReactiveFormsModule],
17+
imports: [MatSliderModule],
1918
declarations: [SliderHarnessTest],
2019
})
2120
.compileComponents();
@@ -113,6 +112,37 @@ function runTests() {
113112
expect(getActiveElementTagName()).not.toBe('mat-slider');
114113
});
115114

115+
it('should be able to set value of slider', async () => {
116+
const sliders = await loader.getAllHarnesses(sliderHarness);
117+
expect(await sliders[1].getValue()).toBe(0);
118+
expect(await sliders[2].getValue()).toBe(225);
119+
120+
await sliders[1].setValue(33);
121+
await sliders[2].setValue(300);
122+
123+
expect(await sliders[1].getValue()).toBe(33);
124+
// value should be clamped to the maximum.
125+
expect(await sliders[2].getValue()).toBe(250);
126+
127+
// should not retrieve incorrect values in case slider is inverted
128+
// due to RTL page layout.
129+
fixture.componentInstance.dir = 'rtl';
130+
fixture.detectChanges();
131+
132+
await sliders[1].setValue(80);
133+
expect(await sliders[1].getValue()).toBe(80);
134+
135+
// should not retrieve incorrect values in case sliders are inverted.
136+
fixture.componentInstance.invertSliders = true;
137+
fixture.detectChanges();
138+
139+
await sliders[1].setValue(75);
140+
await sliders[2].setValue(210);
141+
142+
expect(await sliders[1].getValue()).toBe(75);
143+
expect(await sliders[2].getValue()).toBe(210);
144+
});
145+
116146
it('should get disabled state of slider', async () => {
117147
const sliders = await loader.getAllHarnesses(sliderHarness);
118148
expect(await sliders[0].isDisabled()).toBe(true);
@@ -128,12 +158,19 @@ function getActiveElementTagName() {
128158
@Component({
129159
template: `
130160
<mat-slider value="50" disabled></mat-slider>
131-
<mat-slider [id]="sliderId" [displayWith]="displayFn"></mat-slider>
132-
<mat-slider min="200" max="250" value="225" [displayWith]="displayFn" vertical></mat-slider>
133-
`
161+
<div [dir]="dir">
162+
<mat-slider [id]="sliderId" [displayWith]="displayFn"
163+
[invert]="invertSliders"></mat-slider>
164+
</div>
165+
<mat-slider min="200" max="250" value="225" [displayWith]="displayFn" vertical
166+
[invert]="invertSliders">
167+
</mat-slider>
168+
`
134169
})
135170
class SliderHarnessTest {
136171
sliderId = 'my-slider';
172+
invertSliders = false;
173+
dir = 'ltr';
137174

138175
displayFn(value: number|null) {
139176
if (!value) {

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

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export class MatSliderHarness extends ComponentHarness {
3030
}
3131

3232
private _textLabel = this.locatorFor('.mat-slider-thumb-label-text');
33+
private _wrapper = this.locatorFor('.mat-slider-wrapper');
3334

3435
/** Gets the slider's id. */
3536
async getId(): Promise<string|null> {
@@ -46,9 +47,7 @@ export class MatSliderHarness extends ComponentHarness {
4647

4748
/** Gets the current percentage value of the slider. */
4849
async getPercentage(): Promise<number> {
49-
const min = await this.getMinValue();
50-
const max = await this.getMaxValue();
51-
return ((await this.getValue() || 0) - min) / (max - min);
50+
return this._calculatePercentage(await this.getValue());
5251
}
5352

5453
/** Gets the current value of the slider. */
@@ -78,6 +77,35 @@ export class MatSliderHarness extends ComponentHarness {
7877
return (await this.host()).getAttribute('aria-orientation') as any;
7978
}
8079

80+
/**
81+
* Sets the value of the slide by clicking on the slider track.
82+
*
83+
* Note that in rare cases the value cannot be set to the exact specified value. This
84+
* can happen if not every value of the slider maps to a single pixel that could be
85+
* clicked using mouse interaction. In such cases consider using the keyboard to
86+
* select the given value or expand the slider's size for a better user experience.
87+
*/
88+
async setValue(value: number): Promise<void> {
89+
const sliderEl = await this.host();
90+
const wrapperEl = await this._wrapper();
91+
const {height, width} = await wrapperEl.getDimensions();
92+
const isVertical = await this.getOrientation() === 'vertical';
93+
let percentage = await this._calculatePercentage(value);
94+
95+
// In case the slider is inverted in LTR mode or not inverted in RTL mode,
96+
// we need to invert the percentage so that the proper value is set.
97+
if (await sliderEl.hasClass('mat-slider-invert-mouse-coords')) {
98+
percentage = 1 - percentage;
99+
}
100+
101+
// We need to round the new coordinates because creating fake DOM
102+
// events will cause the coordinates to be rounded down.
103+
const relativeX = isVertical ? 0 : Math.round(width * percentage);
104+
const relativeY = isVertical ? Math.round(height * percentage) : 0;
105+
106+
await wrapperEl.click(relativeX, relativeY);
107+
}
108+
81109
/**
82110
* Focuses the slider and returns a void promise that indicates when the
83111
* action is complete.
@@ -93,4 +121,11 @@ export class MatSliderHarness extends ComponentHarness {
93121
async blur(): Promise<void> {
94122
return (await this.host()).blur();
95123
}
124+
125+
/** Calculates the percentage of the given value. */
126+
private async _calculatePercentage(value: number) {
127+
const min = await this.getMinValue();
128+
const max = await this.getMaxValue();
129+
return (value - min) / (max - min);
130+
}
96131
}

src/material/slider/slider.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ const _MatSliderMixinBase:
132132
'[class.mat-slider-has-ticks]': 'tickInterval',
133133
'[class.mat-slider-horizontal]': '!vertical',
134134
'[class.mat-slider-axis-inverted]': '_invertAxis',
135+
'[class.mat-slider-invert-mouse-coords]': '_shouldInvertMouseCoords()',
135136
'[class.mat-slider-sliding]': '_isSliding',
136137
'[class.mat-slider-thumb-label-showing]': 'thumbLabel',
137138
'[class.mat-slider-vertical]': 'vertical',
@@ -451,7 +452,7 @@ export class MatSlider extends _MatSliderMixinBase
451452
* Whether mouse events should be converted to a slider position by calculating their distance
452453
* from the right or bottom edge of the slider as opposed to the top or left.
453454
*/
454-
private _shouldInvertMouseCoords() {
455+
_shouldInvertMouseCoords() {
455456
return (this._getDirection() == 'rtl' && !this.vertical) ? !this._invertAxis : this._invertAxis;
456457
}
457458

@@ -656,8 +657,6 @@ export class MatSlider extends _MatSliderMixinBase
656657
let size = this.vertical ? this._sliderDimensions.height : this._sliderDimensions.width;
657658
let posComponent = this.vertical ? pos.y : pos.x;
658659

659-
console.error(posComponent, offset, size, (posComponent - offset) / size);
660-
661660
// The exact value is calculated from the event and used to find the closest snap value.
662661
let percent = this._clamp((posComponent - offset) / size);
663662

tools/public_api_guard/material/slider.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export declare class MatSlider extends _MatSliderMixinBase implements ControlVal
4747
_onSlide(event: HammerInput): void;
4848
_onSlideEnd(): void;
4949
_onSlideStart(event: HammerInput | null): void;
50+
_shouldInvertMouseCoords(): boolean;
5051
blur(): void;
5152
focus(): void;
5253
ngOnDestroy(): void;

0 commit comments

Comments
 (0)