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 Sep 5, 2019
1 parent 1c74518 commit d31c78b
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 45 deletions.
1 change: 1 addition & 0 deletions src/material-experimental/mdc-slider/harness/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ ng_test_library(
":harness",
"//src/cdk-experimental/testing",
"//src/cdk-experimental/testing/testbed",
"//src/material-experimental/mdc-slider",
"//src/material/slider",
],
)
131 changes: 131 additions & 0 deletions src/material-experimental/mdc-slider/harness/mdc-slider-harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* @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 {ComponentHarness, HarnessPredicate} from '@angular/cdk-experimental/testing';
import {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion';
import {SliderHarnessFilters} from './slider-harness-filters';

/**
* 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 any;
}

/**
* 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> {
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);
}
}
103 changes: 64 additions & 39 deletions src/material-experimental/mdc-slider/harness/slider-harness.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {HarnessLoader} from '@angular/cdk-experimental/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk-experimental/testing/testbed';
import {Component} from '@angular/core';
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MatSliderModule} from '@angular/material/slider';
import {MatSliderModule as MatMdcSliderModule} from '../module';
import {MatSliderHarness as MatMdcSliderHarness} from './mdc-slider-harness';
import {MatSliderHarness} from './slider-harness';

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

runTests();
// Standard slider supports vertical and inverted sliders.
createTests(true, true);
});

describe(
'MDC-based',
() => {
// TODO: run tests for MDC based slider once implemented.
beforeEach(async () => {
await TestBed
.configureTestingModule({
imports: [MatMdcSliderModule],
declarations: [SliderHarnessTest],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
.compileComponents();

fixture = TestBed.createComponent(SliderHarnessTest);
fixture.detectChanges();
loader = TestbedHarnessEnvironment.loader(fixture);
// Public APIs are the same as "MatSliderHarness", but cast is necessary because
// of different private fields.
sliderHarness = MatMdcSliderHarness as any;
});

// MDC slider does not support vertical or inverted sliders.
createTests(false, false);
});
});

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

it('should get display value of slider', async () => {
const sliders = await loader.getAllHarnesses(sliderHarness);
expect(await sliders[0].getDisplayValue()).toBe('50');
expect(await sliders[0].getDisplayValue()).toBe(null);
expect(await sliders[1].getDisplayValue()).toBe('Null');
expect(await sliders[2].getDisplayValue()).toBe('#225');
});
Expand All @@ -93,18 +114,20 @@ function runTests() {
const sliders = await loader.getAllHarnesses(sliderHarness);
expect(await sliders[0].getOrientation()).toBe('horizontal');
expect(await sliders[1].getOrientation()).toBe('horizontal');
expect(await sliders[2].getOrientation()).toBe('vertical');
expect(await sliders[2].getOrientation()).toBe(supportsVertical ? 'vertical' : 'horizontal');
});

it('should be able to focus slider', async () => {
const [slider] = await loader.getAllHarnesses(sliderHarness);
// the first slider is disabled.
const slider = (await loader.getAllHarnesses(sliderHarness))[1];
expect(getActiveElementTagName()).not.toBe('mat-slider');
await slider.focus();
expect(getActiveElementTagName()).toBe('mat-slider');
});

it('should be able to blur slider', async () => {
const [slider] = await loader.getAllHarnesses(sliderHarness);
// the first slider is disabled.
const slider = (await loader.getAllHarnesses(sliderHarness))[1];
expect(getActiveElementTagName()).not.toBe('mat-slider');
await slider.focus();
expect(getActiveElementTagName()).toBe('mat-slider');
Expand Down Expand Up @@ -139,43 +162,45 @@ function runTests() {
expect(await sliders[1].getValue()).toBe(80);
});

it('should be able to set value of inverted slider', async () => {
it('should get disabled state of slider', async () => {
const sliders = await loader.getAllHarnesses(sliderHarness);
expect(await sliders[1].getValue()).toBe(0);
expect(await sliders[2].getValue()).toBe(225);
expect(await sliders[0].isDisabled()).toBe(true);
expect(await sliders[1].isDisabled()).toBe(false);
expect(await sliders[2].isDisabled()).toBe(false);
});

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

await sliders[1].setValue(75);
await sliders[2].setValue(210);
fixture.componentInstance.invertSliders = true;
fixture.detectChanges();

expect(await sliders[1].getValue()).toBe(75);
expect(await sliders[2].getValue()).toBe(210);
});
await sliders[1].setValue(75);
await sliders[2].setValue(210);

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

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

await sliders[1].setValue(75);
await sliders[2].setValue(210);
fixture.componentInstance.invertSliders = true;
fixture.componentInstance.dir = 'rtl';
fixture.detectChanges();

expect(await sliders[1].getValue()).toBe(75);
expect(await sliders[2].getValue()).toBe(210);
});
await sliders[1].setValue(75);
await sliders[2].setValue(210);

it('should get disabled state of slider', async () => {
const sliders = await loader.getAllHarnesses(sliderHarness);
expect(await sliders[0].isDisabled()).toBe(true);
expect(await sliders[1].isDisabled()).toBe(false);
expect(await sliders[2].isDisabled()).toBe(false);
});
expect(await sliders[1].getValue()).toBe(75);
expect(await sliders[2].getValue()).toBe(210);
});
}
}

function getActiveElementTagName() {
Expand All @@ -187,12 +212,12 @@ function getActiveElementTagName() {
<mat-slider value="50" disabled></mat-slider>
<div [dir]="dir">
<mat-slider [id]="sliderId" [displayWith]="displayFn"
[invert]="invertSliders"></mat-slider>
[invert]="invertSliders" thumbLabel></mat-slider>
</div>
<mat-slider min="200" max="250" value="225" [displayWith]="displayFn" vertical
[invert]="invertSliders">
[invert]="invertSliders" thumbLabel>
</mat-slider>
`
`,
})
class SliderHarnessTest {
sliderId = 'my-slider';
Expand Down
13 changes: 10 additions & 3 deletions src/material-experimental/mdc-slider/harness/slider-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,16 @@ export class MatSliderHarness extends ComponentHarness {
return id !== '' ? id : null;
}

/** Gets the current display value of the slider. */
async getDisplayValue(): Promise<string> {
return (await this._textLabel()).text();
/**
* Gets the current display value of the slider. Returns null if the thumb
* label is disabled.
*/
async getDisplayValue(): Promise<string|null> {
const [host, textLabel] = await Promise.all([this.host(), this._textLabel()]);
if (await host.hasClass('mat-slider-thumb-label-showing')) {
return textLabel.text();
}
return null;
}

/** Gets the current percentage value of the slider. */
Expand Down
16 changes: 13 additions & 3 deletions src/material-experimental/mdc-slider/slider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ export class MatSliderChange {
'[class.mdc-slider--discrete]': 'thumbLabel',
'[class.mat-slider-has-ticks]': '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 @@ -298,7 +301,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 @@ -335,7 +338,7 @@ export class MatSlider implements AfterViewInit, OnChanges, OnDestroy, ControlVa
// client rectangle wouldn't reflect the new directionality.
// TODO(devversion): ideally the MDC slider would just compute dimensions similarly
// to the standard Material slider on "mouseenter".
this._ngZone.runOutsideAngular(() => setTimeout(() => this._foundation.layout()));
setTimeout(() => this._foundation.layout());
});
}
}
Expand All @@ -352,10 +355,12 @@ export class MatSlider implements AfterViewInit, OnChanges, OnDestroy, ControlVa
(this._foundation as any).isDiscrete_ = true;

this._syncStep();
this._syncValue();
this._syncMax();
this._syncMin();
this._syncDisabled();
// 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();
}

ngOnChanges(changes: SimpleChanges) {
Expand Down Expand Up @@ -476,6 +481,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

0 comments on commit d31c78b

Please sign in to comment.