/** * @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.dev/license */ import {BidiModule, Directionality} from '@angular/cdk/bidi'; import {Platform} from '@angular/cdk/platform'; import {dispatchEvent, dispatchFakeEvent, dispatchPointerEvent} from '@angular/cdk/testing/private'; import {Component, Provider, QueryList, Type, ViewChild, ViewChildren} from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync, flush, tick, waitForAsync, } from '@angular/core/testing'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {By} from '@angular/platform-browser'; import {of} from 'rxjs'; import {MatSliderModule} from './module'; import {MatSlider} from './slider'; import {MatSliderRangeThumb, MatSliderThumb} from './slider-input'; import {_MatThumb} from './slider-interface'; import {MatSliderVisualThumb} from './slider-thumb'; interface Point { x: number; y: number; } describe('MatSlider', () => { let platform: Platform; function createComponent(component: Type, providers: Provider[] = []): ComponentFixture { TestBed.configureTestingModule({ imports: [FormsModule, MatSliderModule, ReactiveFormsModule, BidiModule], providers: [...providers], declarations: [component], }); platform = TestBed.inject(Platform); return TestBed.createComponent(component); } function checkInput( input: MatSliderThumb, { min, max, value, translateX, width, step, }: {min: number; max: number; value: number; translateX: number; width?: number; step?: number}, ): void { expect(input.min).withContext('min').toBe(min); expect(input.max).withContext('max').toBe(max); expect(input.value).withContext('value').toBe(value); // The discrepancy between the "ideal" and "actual" translateX comes from // the 3px offset from the start & end of the slider track to the first // and last tick marks. // // The "actual" translateX is calculated based on a slider that is 6px // smaller than the width of the slider. Using this "actual" translateX in // tests would make it even more difficult than it already is to tell if // the translateX is off, so we abstract things in here so tests can be // more intuitive. // // The most clear way to compare the two tx's is to just turn them into // percentages by dividing by their (total height) / 100. const idealTXPercentage = Math.round(translateX / 3); const actualTXPercentage = Math.round((input.translateX - 3) / 2.94); expect(actualTXPercentage) .withContext(`translateX: ${input.translateX} should be close to ${translateX}`) .toBe(idealTXPercentage); if (step !== undefined) { expect(input.step).withContext('step').toBe(step); } if (width !== undefined) { const realWidth = parseInt( (input as MatSliderRangeThumb)._hostElement.style.width.match(/\d+/)![0], 10, ); expect(realWidth) .withContext('width') .toBeCloseTo((300 * width) / 100 + 16, 0); } } // Note that this test is outside of the other `describe` blocks, because they all run // `detectChanges` in the `beforeEach` and we're testing specifically what happens if it // is destroyed before change detection has run. it('should not throw if a slider is destroyed before the first change detection run', () => { expect(() => { const fixture = createComponent(StandardSlider); fixture.destroy(); }).not.toThrow(); }); describe('standard slider', () => { let fixture: ComponentFixture; let slider: MatSlider; let input: MatSliderThumb; beforeEach(waitForAsync(() => { fixture = createComponent(StandardSlider); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; input = slider._getInput(_MatThumb.END) as MatSliderThumb; })); it('should set the default values', () => { expect(slider.min).toBe(0); expect(slider.max).toBe(100); expect(slider.step).toBe(1); checkInput(input, {min: 0, max: 100, value: 0, step: 1, translateX: 0}); }); it('should update by click', fakeAsync(() => { setValueByClick(slider, input, 25); checkInput(input, {min: 0, max: 100, value: 25, step: 1, translateX: 75}); setValueByClick(slider, input, 50); checkInput(input, {min: 0, max: 100, value: 50, step: 1, translateX: 150}); setValueByClick(slider, input, 75); checkInput(input, {min: 0, max: 100, value: 75, step: 1, translateX: 225}); setValueByClick(slider, input, 100); checkInput(input, {min: 0, max: 100, value: 100, step: 1, translateX: 300}); })); it('should update by slide', fakeAsync(() => { slideToValue(slider, input, 25); checkInput(input, {min: 0, max: 100, value: 25, step: 1, translateX: 75}); slideToValue(slider, input, 50); checkInput(input, {min: 0, max: 100, value: 50, step: 1, translateX: 150}); slideToValue(slider, input, 75); checkInput(input, {min: 0, max: 100, value: 75, step: 1, translateX: 225}); slideToValue(slider, input, 100); checkInput(input, {min: 0, max: 100, value: 100, step: 1, translateX: 300}); })); it('should not slide before the track', fakeAsync(() => { slideToValue(slider, input, -10); expect(input.value).toBe(0); checkInput(input, {min: 0, max: 100, value: 0, step: 1, translateX: 0}); })); it('should not slide past the track', fakeAsync(() => { slideToValue(slider, input, 110); expect(input.value).toBe(100); checkInput(input, {min: 0, max: 100, value: 100, step: 1, translateX: 300}); })); // TODO(wagnermaciel): Fix this test case (behavior works as intended in browser). // it('should not break when the page layout changes', fakeAsync(async () => { // slider._elementRef.nativeElement.style.marginLeft = '100px'; // tick(200); // fixture.detectChanges(); // setValueByClick(slider, input, 25); // checkInput(input, {min: 0, max: 100, value: 25, step: 1, translateX: 75}); // slider._elementRef.nativeElement.style.marginLeft = 'initial'; // })); }); describe('standard range slider', () => { let fixture: ComponentFixture; let slider: MatSlider; let endInput: MatSliderRangeThumb; let startInput: MatSliderRangeThumb; beforeEach(waitForAsync(() => { fixture = createComponent(StandardRangeSlider); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; endInput = slider._getInput(_MatThumb.END) as MatSliderRangeThumb; startInput = slider._getInput(_MatThumb.START) as MatSliderRangeThumb; })); it('should set the default values', () => { checkInput(startInput, {min: 0, max: 100, value: 0, step: 1, translateX: 0}); checkInput(endInput, {min: 0, max: 100, value: 100, step: 1, translateX: 300}); expect(slider.min).toBe(0); expect(slider.max).toBe(100); expect(slider.step).toBe(1); }); it('should update by start input click', fakeAsync(() => { setValueByClick(slider, startInput, 25); checkInput(startInput, {min: 0, max: 100, value: 25, translateX: 75}); checkInput(endInput, {min: 25, max: 100, value: 100, translateX: 300}); })); it('should update by end input click', fakeAsync(() => { setValueByClick(slider, endInput, 75); checkInput(startInput, {min: 0, max: 75, value: 0, translateX: 0}); checkInput(endInput, {min: 0, max: 100, value: 75, translateX: 225}); })); it('should update by start thumb slide', fakeAsync(() => { slideToValue(slider, startInput, 75); checkInput(startInput, {min: 0, max: 100, value: 75, translateX: 225}); checkInput(endInput, {min: 75, max: 100, value: 100, translateX: 300}); })); it('should update by end thumb slide', fakeAsync(() => { slideToValue(slider, endInput, 25); checkInput(startInput, {min: 0, max: 25, value: 0, translateX: 0}); checkInput(endInput, {min: 0, max: 100, value: 25, translateX: 75}); })); it('should not allow start thumb to slide before the track', fakeAsync(() => { slideToValue(slider, startInput, -10); checkInput(startInput, {min: 0, max: 100, value: 0, translateX: 0}); checkInput(endInput, {min: 0, max: 100, value: 100, translateX: 300}); })); it('should not allow end thumb to slide past the track', fakeAsync(() => { slideToValue(slider, endInput, 110); checkInput(startInput, {min: 0, max: 100, value: 0, translateX: 0}); checkInput(endInput, {min: 0, max: 100, value: 100, translateX: 300}); })); it('should not allow start thumb to slide past the end thumb', fakeAsync(() => { slideToValue(slider, endInput, 50); slideToValue(slider, startInput, 55); checkInput(startInput, {min: 0, max: 50, value: 50, translateX: 150}); checkInput(endInput, {min: 50, max: 100, value: 50, translateX: 150}); })); it('should not allow end thumb to slide past the start thumb', fakeAsync(() => { slideToValue(slider, startInput, 50); slideToValue(slider, endInput, 45); checkInput(startInput, {min: 0, max: 50, value: 50, translateX: 150}); checkInput(endInput, {min: 50, max: 100, value: 50, translateX: 150}); })); // TODO(wagnermaciel): Fix this test case (behavior works as intended in browser). // it('should not break when the page layout changes', fakeAsync(() => { // slider._elementRef.nativeElement.style.marginLeft = '100px'; // setValueByClick(slider, startInput, 25); // checkInput(startInput, {min: 0, max: 100, value: 25, translateX: 75}); // checkInput(endInput, {min: 25, max: 100, value: 100, translateX: 300}); // setValueByClick(slider, endInput, 75); // checkInput(startInput, {min: 0, max: 75, value: 25, translateX: 75}); // checkInput(endInput, {min: 25, max: 100, value: 75, translateX: 225}); // slider._elementRef.nativeElement.style.marginLeft = 'initial'; // })); }); describe('slider with min/max bindings', () => { let fixture: ComponentFixture; let slider: MatSlider; let input: MatSliderThumb; beforeEach(waitForAsync(() => { fixture = createComponent(SliderWithMinAndMax); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; input = slider._getInput(_MatThumb.END) as MatSliderThumb; })); it('should have the correct initial values', () => { checkInput(input, {min: 25, max: 75, value: 25, translateX: 0}); }); it('should update the min when the bound value changes', () => { fixture.componentInstance.min = 0; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(input, {min: 0, max: 75, value: 25, translateX: 100}); }); it('should update the max when the bound value changes', () => { fixture.componentInstance.max = 90; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(input, {min: 25, max: 90, value: 25, translateX: 0}); }); it('should update the value if the min increases past it', () => { fixture.componentInstance.min = 50; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(input, {min: 50, max: 75, value: 50, translateX: 0}); }); it('should update the value if the max decreases below it', () => { input.value = 75; fixture.componentInstance.max = 50; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(input, {min: 25, max: 50, value: 50, translateX: 300}); }); it('should allow the min increase above the max', () => { fixture.componentInstance.min = 80; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(input, {min: 80, max: 75, value: 80, translateX: 0}); }); it('should allow the max to decrease below the min', () => { fixture.componentInstance.max = -10; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(input, {min: 25, max: -10, value: 25, translateX: 0}); }); it('should update the thumb translateX when the min changes', () => { checkInput(input, {min: 25, max: 75, value: 25, translateX: 0}); fixture.componentInstance.min = -25; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(input, {min: -25, max: 75, value: 25, translateX: 150}); }); it('should update the thumb translateX when the max changes', fakeAsync(() => { setValueByClick(slider, input, 50); checkInput(input, {min: 25, max: 75, value: 50, translateX: 150}); fixture.componentInstance.max = 125; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(input, {min: 25, max: 125, value: 50, translateX: 75}); })); }); describe('range slider with min/max bindings', () => { let fixture: ComponentFixture; let slider: MatSlider; let endInput: MatSliderRangeThumb; let startInput: MatSliderRangeThumb; beforeEach(waitForAsync(() => { fixture = createComponent(RangeSliderWithMinAndMax); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; endInput = slider._getInput(_MatThumb.END) as MatSliderRangeThumb; startInput = slider._getInput(_MatThumb.START) as MatSliderRangeThumb; })); it('should have the correct initial values', () => { checkInput(startInput, {min: 25, max: 75, value: 25, translateX: 0}); checkInput(endInput, {min: 25, max: 75, value: 75, translateX: 300}); }); describe('should handle min changes', () => { it('that do not affect values', () => { checkInput(startInput, {min: 25, max: 75, value: 25, translateX: 0}); checkInput(endInput, {min: 25, max: 75, value: 75, translateX: 300}); fixture.componentInstance.min = -25; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(startInput, {min: -25, max: 75, value: 25, translateX: 150}); checkInput(endInput, {min: 25, max: 75, value: 75, translateX: 300}); }); it('that affect the start value', () => { fixture.componentInstance.min = 50; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(startInput, {min: 50, max: 75, value: 50, translateX: 0}); checkInput(endInput, {min: 50, max: 75, value: 75, translateX: 300}); }); it('that affect both values', () => { endInput.value = 50; fixture.componentInstance.min = 60; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(startInput, {min: 60, max: 60, value: 60, translateX: 0}); checkInput(endInput, {min: 60, max: 75, value: 60, translateX: 0}); }); it('where the new start tx is greater than the old end tx', fakeAsync(() => { fixture.componentInstance.min = 0; fixture.componentInstance.max = 100; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); slideToValue(slider, startInput, 10); slideToValue(slider, endInput, 20); checkInput(startInput, {min: 0, max: 20, value: 10, translateX: 30}); checkInput(endInput, {min: 10, max: 100, value: 20, translateX: 60}); fixture.componentInstance.min = -1000; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(startInput, {min: -1000, max: 20, value: 10, translateX: 275.5}); checkInput(endInput, {min: 10, max: 100, value: 20, translateX: 278}); })); it('where the new end tx is less than the old start tx', fakeAsync(() => { fixture.componentInstance.min = 0; fixture.componentInstance.max = 100; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); slideToValue(slider, endInput, 92); slideToValue(slider, startInput, 91); checkInput(startInput, {min: 0, max: 92, value: 91, translateX: 273}); checkInput(endInput, {min: 91, max: 100, value: 92, translateX: 276}); fixture.componentInstance.min = 90; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(startInput, {min: 90, max: 92, value: 91, translateX: 30}); checkInput(endInput, {min: 91, max: 100, value: 92, translateX: 60}); })); it('that make min and max equal', () => { fixture.componentInstance.min = 75; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(startInput, {min: 75, max: 75, value: 75, translateX: 0}); checkInput(endInput, {min: 75, max: 75, value: 75, translateX: 0}); }); it('that increase above the max', () => { fixture.componentInstance.min = 80; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(startInput, {min: 80, max: 75, value: 80, translateX: 0}); checkInput(endInput, {min: 80, max: 75, value: 80, translateX: 0}); }); }); describe('should handle max changes', () => { it('that do not affect values', () => { checkInput(startInput, {min: 25, max: 75, value: 25, translateX: 0}); checkInput(endInput, {min: 25, max: 75, value: 75, translateX: 300}); fixture.componentInstance.max = 125; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(startInput, {min: 25, max: 75, value: 25, translateX: 0}); checkInput(endInput, {min: 25, max: 125, value: 75, translateX: 150}); }); it('that affect the end value', () => { fixture.componentInstance.max = 50; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(endInput, {min: 25, max: 50, value: 50, translateX: 300}); checkInput(startInput, {min: 25, max: 50, value: 25, translateX: 0}); }); it('that affect both values', () => { startInput.value = 60; fixture.componentInstance.max = 50; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(endInput, {min: 50, max: 50, value: 50, translateX: 300}); checkInput(startInput, {min: 25, max: 50, value: 50, translateX: 300}); }); it('where the new start tx is greater than the old end tx', fakeAsync(() => { fixture.componentInstance.min = 0; fixture.componentInstance.max = 100; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); slideToValue(slider, startInput, 1); slideToValue(slider, endInput, 2); checkInput(startInput, {min: 0, max: 2, value: 1, translateX: 3}); checkInput(endInput, {min: 1, max: 100, value: 2, translateX: 6}); fixture.componentInstance.max = 10; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(startInput, {min: 0, max: 2, value: 1, translateX: 30}); checkInput(endInput, {min: 1, max: 10, value: 2, translateX: 60}); })); it('where the new end tx is less than the old start tx', fakeAsync(() => { fixture.componentInstance.min = 0; fixture.componentInstance.max = 100; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); slideToValue(slider, endInput, 95); slideToValue(slider, startInput, 90); checkInput(startInput, {min: 0, max: 95, value: 90, translateX: 270}); checkInput(endInput, {min: 90, max: 100, value: 95, translateX: 285}); fixture.componentInstance.max = 1000; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(startInput, {min: 0, max: 95, value: 90, translateX: 27}); checkInput(endInput, {min: 90, max: 1000, value: 95, translateX: 28.5}); })); it('that make min and max equal', () => { fixture.componentInstance.max = 25; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(startInput, {min: 25, max: 25, value: 25, translateX: 0}); checkInput(endInput, {min: 25, max: 25, value: 25, translateX: 0}); }); it('that decrease below the min', () => { fixture.componentInstance.max = 0; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // For some reason there was a bug with Safari 15.3. // Manually testing on version 16.0 shows that this issue no longer exists. if (!platform.SAFARI) { checkInput(startInput, {min: 25, max: 0, value: 25, translateX: 0}); checkInput(endInput, {min: 25, max: 0, value: 25, translateX: 0}); } }); }); }); describe('disabled slider', () => { let slider: MatSlider; let input: MatSliderThumb; beforeEach(waitForAsync(() => { const fixture = createComponent(DisabledSlider); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; input = slider._getInput(_MatThumb.END) as MatSliderThumb; })); it('should be disabled', () => { expect(slider.disabled).toBeTrue(); }); it('should have the disabled class on the root element', () => { expect(slider._elementRef.nativeElement.classList).toContain('mdc-slider--disabled'); }); it('should set the disabled attribute on the input element', () => { expect(input._hostElement.disabled).toBeTrue(); }); }); describe('disabled range slider', () => { let slider: MatSlider; let startInput: MatSliderThumb; let endInput: MatSliderThumb; beforeEach(waitForAsync(() => { const fixture = createComponent(DisabledRangeSlider); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; startInput = slider._getInput(_MatThumb.START) as MatSliderRangeThumb; endInput = slider._getInput(_MatThumb.END) as MatSliderRangeThumb; })); it('should be disabled', () => { expect(slider.disabled).toBeTrue(); }); it('should have the disabled class on the root element', () => { expect(slider._elementRef.nativeElement.classList).toContain('mdc-slider--disabled'); }); it('should set the disabled attribute on the input elements', () => { expect(startInput._hostElement.disabled).toBeTrue(); expect(endInput._hostElement.disabled).toBeTrue(); }); }); describe('ripple states', () => { let input: MatSliderThumb; let thumbInstance: MatSliderVisualThumb; let thumbElement: HTMLElement; let thumbX: number; let thumbY: number; beforeEach(waitForAsync(() => { const fixture = createComponent(StandardSlider); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); const slider = sliderDebugElement.componentInstance; input = slider._getInput(_MatThumb.END) as MatSliderThumb; thumbInstance = slider._getThumb(_MatThumb.END); thumbElement = thumbInstance._hostElement; const thumbDimensions = thumbElement.getBoundingClientRect(); thumbX = thumbDimensions.left + thumbDimensions.width / 2; thumbY = thumbDimensions.top + thumbDimensions.height / 2; })); function isRippleVisible(selector: string) { flushRippleTransitions(); return thumbElement.querySelector(`.mat-mdc-slider-${selector}-ripple`) !== null; } function flushRippleTransitions() { thumbElement.querySelectorAll('.mat-ripple-element').forEach(el => { dispatchFakeEvent(el, 'transitionend'); }); } function blur() { input._hostElement.blur(); } function pointerenter() { dispatchPointerEvent(input._hostElement, 'pointermove', thumbX, thumbY); } function pointerleave() { dispatchPointerEvent(input._hostElement, 'pointermove', thumbX + 1000, thumbY); } function pointerdown() { dispatchPointerEvent(input._hostElement, 'pointerdown', thumbX, thumbY); input.focus(); } function pointerup() { dispatchPointerEvent(input._hostElement, 'pointerup', thumbX, thumbY); } it('should show the hover ripple on pointerenter', fakeAsync(() => { // Doesn't make sense to test for pointerenter events on touch devices. expect(isRippleVisible('hover')).toBeFalse(); pointerenter(); expect(isRippleVisible('hover')).toBeTrue(); })); it('should hide the hover ripple on pointerleave', fakeAsync(() => { // Doesn't make sense to test for pointerleave events on touch devices. pointerenter(); pointerleave(); expect(isRippleVisible('hover')).toBeFalse(); })); it('should show the focus ripple on pointerdown', fakeAsync(() => { expect(isRippleVisible('focus')).toBeFalse(); pointerdown(); flush(); expect(isRippleVisible('focus')).toBeTrue(); })); it('should continue to show the focus ripple on pointerup', fakeAsync(() => { pointerdown(); pointerup(); flush(); // The slider immediately loses focus on pointerup for Safari. if (platform.SAFARI) { expect(isRippleVisible('hover')).toBeTrue(); } else { expect(isRippleVisible('focus')).toBeTrue(); } })); it('should hide the focus ripple on blur', fakeAsync(() => { pointerdown(); pointerup(); blur(); flush(); expect(isRippleVisible('focus')).toBeFalse(); })); it('should show the active ripple on pointerdown', fakeAsync(() => { expect(isRippleVisible('active')).toBeFalse(); pointerdown(); expect(isRippleVisible('active')).toBeTrue(); flush(); })); it('should hide the active ripple on pointerup', fakeAsync(() => { pointerdown(); pointerup(); flush(); expect(isRippleVisible('active')).toBeFalse(); })); // Edge cases. it('should not show the hover ripple if the thumb is already focused', fakeAsync(() => { pointerdown(); pointerenter(); flush(); expect(isRippleVisible('hover')).toBeFalse(); })); it('should hide the hover ripple if the thumb is focused', fakeAsync(() => { pointerenter(); pointerdown(); flush(); expect(isRippleVisible('hover')).toBeFalse(); })); it('should not hide the focus ripple if the thumb is pressed', fakeAsync(() => { pointerdown(); blur(); flush(); expect(isRippleVisible('focus')).toBeTrue(); })); it('should not hide the hover ripple on blur if the thumb is hovered', fakeAsync(() => { pointerenter(); pointerdown(); pointerup(); blur(); flush(); expect(isRippleVisible('hover')).toBeTrue(); })); it('should hide the focus ripple on drag end if the thumb already lost focus', fakeAsync(() => { pointerdown(); blur(); pointerup(); flush(); expect(isRippleVisible('focus')).toBeFalse(); })); }); describe('slider with set value', () => { let slider: MatSlider; let input: MatSliderThumb; beforeEach(waitForAsync(() => { const fixture = createComponent(SliderWithValue); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; input = slider._getInput(_MatThumb.END) as MatSliderThumb; })); it('should set the default value from the attribute', () => { checkInput(input, {min: 0, max: 100, value: 50, translateX: 150}); }); it('should update the value', fakeAsync(() => { slideToValue(slider, input, 75); checkInput(input, {min: 0, max: 100, value: 75, translateX: 225}); })); }); describe('range slider with set value', () => { let slider: MatSlider; let startInput: MatSliderThumb; let endInput: MatSliderThumb; beforeEach(waitForAsync(() => { const fixture = createComponent(RangeSliderWithValue); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; startInput = slider._getInput(_MatThumb.START) as MatSliderRangeThumb; endInput = slider._getInput(_MatThumb.END) as MatSliderRangeThumb; })); it('should set the correct initial values', fakeAsync(() => { checkInput(startInput, {min: 0, max: 75, value: 25, translateX: 75}); checkInput(endInput, {min: 25, max: 100, value: 75, translateX: 225}); })); it('should update the start value', fakeAsync(() => { checkInput(startInput, {min: 0, max: 75, value: 25, translateX: 75}); checkInput(endInput, {min: 25, max: 100, value: 75, translateX: 225}); slideToValue(slider, startInput, 30); checkInput(startInput, {min: 0, max: 75, value: 30, translateX: 90}); checkInput(endInput, {min: 30, max: 100, value: 75, translateX: 225}); })); it('should update the end value', fakeAsync(() => { slideToValue(slider, endInput, 77); checkInput(startInput, {min: 0, max: 77, value: 25, translateX: 75}); checkInput(endInput, {min: 25, max: 100, value: 77, translateX: 231}); })); }); describe('slider with set step', () => { let fixture: ComponentFixture; let slider: MatSlider; let input: MatSliderThumb; beforeEach(waitForAsync(() => { fixture = createComponent(SliderWithStep); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; input = slider._getInput(_MatThumb.END) as MatSliderThumb; })); it('should update to the value based on the step', fakeAsync(() => { slideToValue(slider, input, 30); expect(input.value).toBe(25); })); it('should not add decimals to the value if it is a whole number', fakeAsync(() => { fixture.componentInstance.step = 0.1; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); slideToValue(slider, input, 11); expect(input.value).toBe(11); })); it('should truncate long decimal values when using a decimal step', fakeAsync(() => { fixture.componentInstance.step = 0.5; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); slideToValue(slider, input, 55.555); expect(input.value).toBe(55.5); })); it('should update the value on step change', fakeAsync(() => { slideToValue(slider, input, 30); fixture.componentInstance.step = 50; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(input.value).toBe(50); })); }); describe('range slider with set step', () => { let fixture: ComponentFixture; let slider: MatSlider; let startInput: MatSliderThumb; let endInput: MatSliderThumb; beforeEach(waitForAsync(() => { fixture = createComponent(RangeSliderWithStep); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; startInput = slider._getInput(_MatThumb.START) as MatSliderRangeThumb; endInput = slider._getInput(_MatThumb.END) as MatSliderRangeThumb; })); it('should set the correct start value on slide', fakeAsync(() => { slideToValue(slider, startInput, 30); expect(startInput.value).toBe(25); })); it('should set the correct end value on slide', fakeAsync(() => { slideToValue(slider, endInput, 45); expect(endInput.value).toBe(50); })); it('should not add decimals to the end value if it is a whole number', fakeAsync(() => { fixture.componentInstance.step = 0.1; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); slideToValue(slider, endInput, 11); expect(endInput.value).toBe(11); })); it('should not add decimals to the start value if it is a whole number', fakeAsync(() => { fixture.componentInstance.step = 0.1; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); slideToValue(slider, startInput, 11); expect(startInput.value).toBe(11); })); it('should truncate long decimal start values when using a decimal step', fakeAsync(() => { fixture.componentInstance.step = 0.1; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); slideToValue(slider, startInput, 33.666); expect(startInput.value).toBe(33.7); })); it('should truncate long decimal end values when using a decimal step', fakeAsync(() => { fixture.componentInstance.step = 0.1; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); slideToValue(slider, endInput, 33.6666); expect(endInput.value).toBe(33.7); })); describe('should handle step changes', () => { it('where the new start tx is greater than the old end tx', fakeAsync(() => { fixture.componentInstance.step = 0; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); slideToValue(slider, startInput, 45); slideToValue(slider, endInput, 46); checkInput(startInput, {min: 0, max: 46, value: 45, translateX: 135}); checkInput(endInput, {min: 45, max: 100, value: 46, translateX: 138}); fixture.componentInstance.step = 50; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(startInput, {min: 0, max: 50, value: 50, translateX: 150}); checkInput(endInput, {min: 50, max: 100, value: 50, translateX: 150}); })); it('where the new end tx is less than the old start tx', fakeAsync(() => { fixture.componentInstance.step = 0; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); slideToValue(slider, startInput, 21); slideToValue(slider, endInput, 22); checkInput(startInput, {min: 0, max: 22, value: 21, translateX: 63}); checkInput(endInput, {min: 21, max: 100, value: 22, translateX: 66}); fixture.componentInstance.step = 50; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkInput(startInput, {min: 0, max: 0, value: 0, translateX: 0}); checkInput(endInput, {min: 0, max: 100, value: 0, translateX: 0}); })); }); }); describe('slider with custom thumb label formatting', () => { let fixture: ComponentFixture; let slider: MatSlider; let input: MatSliderThumb; let valueIndicatorTextElement: Element; beforeEach(() => { fixture = createComponent(DiscreteSliderWithDisplayWith); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider))!; const sliderNativeElement = sliderDebugElement.nativeElement; slider = sliderDebugElement.componentInstance; valueIndicatorTextElement = sliderNativeElement.querySelector( '.mdc-slider__value-indicator-text', )!; input = slider._getInput(_MatThumb.END) as MatSliderThumb; }); it('should set the aria-valuetext attribute with the given `displayWith` function', fakeAsync(() => { expect(input._hostElement.getAttribute('aria-valuetext')).toBe('$1'); setValueByClick(slider, input, 199); fixture.detectChanges(); flush(); expect(input._hostElement.getAttribute('aria-valuetext')).toBe('$199'); })); it('should invoke the passed-in `displayWith` function with the value', fakeAsync(() => { spyOn(slider, 'displayWith').and.callThrough(); setValueByClick(slider, input, 199); expect(slider.displayWith).toHaveBeenCalledWith(199); })); it('should format the thumb label based on the passed-in `displayWith` function', fakeAsync(() => { setValueByClick(slider, input, 149); fixture.detectChanges(); expect(valueIndicatorTextElement.textContent).toBe('$149'); })); }); describe('range slider with custom thumb label formatting', () => { let fixture: ComponentFixture; let slider: MatSlider; let startValueIndicatorTextElement: Element; let endValueIndicatorTextElement: Element; let startInput: MatSliderThumb; let endInput: MatSliderThumb; beforeEach(() => { fixture = createComponent(DiscreteRangeSliderWithDisplayWith); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider))!; slider = sliderDebugElement.componentInstance; startInput = slider._getInput(_MatThumb.START) as MatSliderRangeThumb; endInput = slider._getInput(_MatThumb.END) as MatSliderRangeThumb; const startThumbElement = slider._getThumb(_MatThumb.START)._hostElement; const endThumbElement = slider._getThumb(_MatThumb.END)._hostElement; startValueIndicatorTextElement = startThumbElement.querySelector( '.mdc-slider__value-indicator-text', )!; endValueIndicatorTextElement = endThumbElement.querySelector( '.mdc-slider__value-indicator-text', )!; }); it('should set the aria-valuetext attribute with the given `displayWith` function', fakeAsync(() => { expect(startInput._hostElement.getAttribute('aria-valuetext')).toBe('$1'); expect(endInput._hostElement.getAttribute('aria-valuetext')).toBe('$200'); setValueByClick(slider, startInput, 25); setValueByClick(slider, endInput, 81); expect(startInput._hostElement.getAttribute('aria-valuetext')).toBe('$25'); expect(endInput._hostElement.getAttribute('aria-valuetext')).toBe('$81'); })); it('should invoke the passed-in `displayWith` function with the start value', fakeAsync(() => { spyOn(slider, 'displayWith').and.callThrough(); setValueByClick(slider, startInput, 197); expect(slider.displayWith).toHaveBeenCalledWith(197); })); it('should invoke the passed-in `displayWith` function with the end value', fakeAsync(() => { spyOn(slider, 'displayWith').and.callThrough(); setValueByClick(slider, endInput, 72); expect(slider.displayWith).toHaveBeenCalledWith(72); })); it('should format the start thumb label based on the passed-in `displayWith` function', fakeAsync(() => { setValueByClick(slider, startInput, 120); fixture.detectChanges(); expect(startValueIndicatorTextElement.textContent).toBe('$120'); })); it('should format the end thumb label based on the passed-in `displayWith` function', fakeAsync(() => { setValueByClick(slider, endInput, 70); fixture.detectChanges(); expect(endValueIndicatorTextElement.textContent).toBe('$70'); })); }); describe('slider with value property binding', () => { let fixture: ComponentFixture; let input: MatSliderThumb; beforeEach(waitForAsync(() => { fixture = createComponent(SliderWithOneWayBinding); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); const slider = sliderDebugElement.componentInstance; input = slider._getInput(_MatThumb.END) as MatSliderThumb; })); it('should update when bound value changes', () => { fixture.componentInstance.value = 75; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(input.value).toBe(75); }); }); describe('range slider with value property binding', () => { let fixture: ComponentFixture; let startInput: MatSliderThumb; let endInput: MatSliderThumb; beforeEach(waitForAsync(() => { fixture = createComponent(RangeSliderWithOneWayBinding); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); const slider = sliderDebugElement.componentInstance; startInput = slider._getInput(_MatThumb.START) as MatSliderRangeThumb; endInput = slider._getInput(_MatThumb.END) as MatSliderRangeThumb; })); it('should update when bound start value changes', () => { fixture.componentInstance.startValue = 30; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(startInput.value).toBe(30); }); it('should update when bound end value changes', () => { fixture.componentInstance.endValue = 70; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(endInput.value).toBe(70); }); it('should update the input width when the start value changes', () => { const startInputEl = startInput._elementRef.nativeElement; const endInputEl = endInput._elementRef.nativeElement; const startInputWidthBefore = startInputEl.getBoundingClientRect().width; const endInputWidthBefore = endInputEl.getBoundingClientRect().width; fixture.componentInstance.startValue = 10; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const startInputWidthAfter = startInputEl.getBoundingClientRect().width; const endInputWidthAfter = endInputEl.getBoundingClientRect().width; expect(startInputWidthBefore).not.toBe(startInputWidthAfter); expect(endInputWidthBefore).not.toBe(endInputWidthAfter); }); it('should update the input width when the end value changes', () => { const startInputEl = startInput._elementRef.nativeElement; const endInputEl = endInput._elementRef.nativeElement; const startInputWidthBefore = startInputEl.getBoundingClientRect().width; const endInputWidthBefore = endInputEl.getBoundingClientRect().width; fixture.componentInstance.endValue = 90; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const startInputWidthAfter = startInputEl.getBoundingClientRect().width; const endInputWidthAfter = endInputEl.getBoundingClientRect().width; expect(startInputWidthBefore).not.toBe(startInputWidthAfter); expect(endInputWidthBefore).not.toBe(endInputWidthAfter); }); }); describe('slider with direction', () => { let slider: MatSlider; let input: MatSliderThumb; beforeEach(waitForAsync(() => { const fixture = createComponent(StandardSlider, [ { provide: Directionality, useValue: {value: 'rtl', change: of()}, }, ]); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; input = slider._getInput(_MatThumb.END) as MatSliderThumb; })); it('works in RTL languages', fakeAsync(() => { setValueByClick(slider, input, 25, true); checkInput(input, {min: 0, max: 100, value: 75, translateX: 75}); })); }); describe('range slider with direction', () => { let slider: MatSlider; let startInput: MatSliderThumb; let endInput: MatSliderThumb; beforeEach(waitForAsync(() => { const fixture = createComponent(StandardRangeSlider, [ { provide: Directionality, useValue: {value: 'rtl', change: of()}, }, ]); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; startInput = slider._getInput(_MatThumb.START) as MatSliderRangeThumb; endInput = slider._getInput(_MatThumb.END) as MatSliderRangeThumb; })); it('works in RTL languages', fakeAsync(() => { setValueByClick(slider, startInput, 90, true); checkInput(startInput, {min: 0, max: 100, value: 10, translateX: 270}); setValueByClick(slider, endInput, 10, true); checkInput(endInput, {min: 10, max: 100, value: 90, translateX: 30}); })); }); describe('slider with ngModel', () => { let fixture: ComponentFixture; let slider: MatSlider; let input: MatSliderThumb; beforeEach(waitForAsync(() => { fixture = createComponent(SliderWithNgModel); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; input = slider._getInput(_MatThumb.END) as MatSliderThumb; })); it('should update the model', fakeAsync(() => { slideToValue(slider, input, 19); fixture.detectChanges(); expect(fixture.componentInstance.val).toBe(19); checkInput(input, {min: 0, max: 100, value: 19, translateX: 57}); })); it('should update the slider', fakeAsync(() => { fixture.componentInstance.val = 20; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); checkInput(input, {min: 0, max: 100, value: 20, translateX: 60}); })); it('should be able to reset a slider by setting the model back to undefined', fakeAsync(() => { fixture.componentInstance.val = 5; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); checkInput(input, {min: 0, max: 100, value: 5, translateX: 15}); fixture.componentInstance.val = undefined; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); checkInput(input, {min: 0, max: 100, value: 0, translateX: 0}); })); }); describe('range slider with ngModel', () => { let slider: MatSlider; let fixture: ComponentFixture; let startInput: MatSliderThumb; let endInput: MatSliderThumb; beforeEach(waitForAsync(() => { fixture = createComponent(RangeSliderWithNgModel); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; startInput = slider._getInput(_MatThumb.START) as MatSliderRangeThumb; endInput = slider._getInput(_MatThumb.END) as MatSliderRangeThumb; })); it('should update the models on input value changes', fakeAsync(() => { slideToValue(slider, startInput, 25); fixture.detectChanges(); flush(); checkInput(startInput, {min: 0, max: 100, value: 25, translateX: 75}); slideToValue(slider, endInput, 75); fixture.detectChanges(); flush(); checkInput(endInput, {min: 25, max: 100, value: 75, translateX: 225}); })); it('should update the thumbs on ngModel value change', fakeAsync(() => { fixture.componentInstance.startVal = 50; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); checkInput(startInput, {min: 0, max: 100, value: 50, translateX: 150}); fixture.componentInstance.endVal = 75; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); checkInput(endInput, {min: 50, max: 100, value: 75, translateX: 225}); })); it('should be able to reset a start input', fakeAsync(() => { fixture.componentInstance.startVal = 5; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); checkInput(startInput, {min: 0, max: 100, value: 5, translateX: 15}); fixture.componentInstance.startVal = undefined; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); checkInput(startInput, {min: 0, max: 100, value: 0, translateX: 0}); })); it('should be able to reset an end input', fakeAsync(() => { fixture.componentInstance.endVal = 99; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); checkInput(endInput, {min: 0, max: 100, value: 99, translateX: 297}); fixture.componentInstance.endVal = undefined; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); checkInput(endInput, {min: 0, max: 100, value: 0, translateX: 0}); })); }); describe('range slider w/ NgModel edge case', () => { it('should initialize correctly despite NgModel `null` bug', fakeAsync(() => { const fixture = createComponent(RangeSliderWithNgModelEdgeCase); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); const slider = sliderDebugElement.componentInstance; const startInput = slider._getInput(_MatThumb.START) as MatSliderRangeThumb; const endInput = slider._getInput(_MatThumb.END) as MatSliderRangeThumb; flush(); checkInput(startInput, {min: -1, max: -0.3, value: -0.7, translateX: 90}); checkInput(endInput, {min: -0.7, max: 0, value: -0.3, translateX: 210}); })); }); describe('slider as a custom form control', () => { let fixture: ComponentFixture; let slider: MatSlider; let input: MatSliderThumb; beforeEach(waitForAsync(() => { fixture = createComponent(SliderWithFormControl); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; input = slider._getInput(_MatThumb.END) as MatSliderThumb; })); it('should update the control on slide', fakeAsync(() => { expect(fixture.componentInstance.control.value).toBe(0); slideToValue(slider, input, 19); expect(fixture.componentInstance.control.value).toBe(19); })); it('should update the value when the control is set', () => { expect(input.value).toBe(0); fixture.componentInstance.control.setValue(7); checkInput(input, {min: 0, max: 100, value: 7, translateX: 21}); }); it('should update the disabled state when control is disabled', () => { expect(slider.disabled).toBe(false); fixture.componentInstance.control.disable(); expect(slider.disabled).toBe(true); }); it('should update the disabled state when the control is enabled', () => { slider.disabled = true; fixture.componentInstance.control.enable(); expect(slider.disabled).toBe(false); }); it('should have the correct control state initially and after interaction', fakeAsync(() => { let sliderControl = fixture.componentInstance.control; // The control should start off valid, pristine, and untouched. expect(sliderControl.valid).toBe(true); expect(sliderControl.pristine).toBe(true); expect(sliderControl.touched).toBe(false); // After changing the value, the control should become dirty (not pristine), // but remain untouched. setValueByClick(slider, input, 50); expect(sliderControl.valid).toBe(true); expect(sliderControl.pristine).toBe(false); expect(sliderControl.touched).toBe(false); // If the control has been visited due to interaction, the control should remain // dirty and now also be touched. input.blur(); fixture.detectChanges(); expect(sliderControl.valid).toBe(true); expect(sliderControl.pristine).toBe(false); expect(sliderControl.touched).toBe(true); })); }); describe('slider as a custom form control', () => { let fixture: ComponentFixture; let slider: MatSlider; let startInput: MatSliderThumb; let endInput: MatSliderThumb; beforeEach(waitForAsync(() => { fixture = createComponent(RangeSliderWithFormControl); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; startInput = slider._getInput(_MatThumb.START) as MatSliderRangeThumb; endInput = slider._getInput(_MatThumb.END) as MatSliderRangeThumb; })); it('should update the start input control on slide', fakeAsync(() => { expect(fixture.componentInstance.startInputControl.value).toBe(0); slideToValue(slider, startInput, 20); expect(fixture.componentInstance.startInputControl.value).toBe(20); })); it('should update the end input control on slide', fakeAsync(() => { expect(fixture.componentInstance.endInputControl.value).toBe(100); slideToValue(slider, endInput, 80); expect(fixture.componentInstance.endInputControl.value).toBe(80); })); it('should update the start input value when the start input control is set', () => { expect(startInput.value).toBe(0); fixture.componentInstance.startInputControl.setValue(10); checkInput(startInput, {min: 0, max: 100, value: 10, translateX: 30}); }); it('should update the end input value when the end input control is set', () => { expect(endInput.value).toBe(100); fixture.componentInstance.endInputControl.setValue(90); checkInput(endInput, {min: 0, max: 100, value: 90, translateX: 270}); }); it('should update the disabled state if the start input control is disabled', () => { expect(slider.disabled).toBe(false); fixture.componentInstance.startInputControl.disable(); expect(slider.disabled).toBe(true); }); it('should update the disabled state if the end input control is disabled', () => { expect(slider.disabled).toBe(false); fixture.componentInstance.endInputControl.disable(); expect(slider.disabled).toBe(true); }); it('should update the disabled state when both input controls are enabled', () => { slider.disabled = true; fixture.componentInstance.startInputControl.enable(); expect(slider.disabled).toBe(false); fixture.componentInstance.endInputControl.enable(); expect(slider.disabled).toBe(false); }); it('should have the correct start input control state initially and after interaction', fakeAsync(() => { let sliderControl = fixture.componentInstance.startInputControl; // The control should start off valid, pristine, and untouched. expect(sliderControl.valid).toBe(true); expect(sliderControl.pristine).toBe(true); expect(sliderControl.touched).toBe(false); // After changing the value, the control should become dirty (not pristine), // but remain untouched. setValueByClick(slider, startInput, 25); expect(sliderControl.valid).toBe(true); expect(sliderControl.pristine).toBe(false); expect(sliderControl.touched).toBe(false); // If the control has been visited due to interaction, the control should remain // dirty and now also be touched. startInput.blur(); fixture.detectChanges(); expect(sliderControl.valid).toBe(true); expect(sliderControl.pristine).toBe(false); expect(sliderControl.touched).toBe(true); })); it('should have the correct start input control state initially and after interaction', fakeAsync(() => { let sliderControl = fixture.componentInstance.endInputControl; // The control should start off valid, pristine, and untouched. expect(sliderControl.valid).toBe(true); expect(sliderControl.pristine).toBe(true); expect(sliderControl.touched).toBe(false); // After changing the value, the control should become dirty (not pristine), // but remain untouched. setValueByClick(slider, endInput, 75); expect(sliderControl.valid).toBe(true); expect(sliderControl.pristine).toBe(false); expect(sliderControl.touched).toBe(false); // If the control has been visited due to interaction, the control should remain // dirty and now also be touched. endInput.blur(); fixture.detectChanges(); expect(sliderControl.valid).toBe(true); expect(sliderControl.pristine).toBe(false); expect(sliderControl.touched).toBe(true); })); }); describe('slider with a two-way binding', () => { let input: MatSliderThumb; let slider: MatSlider; let fixture: ComponentFixture; beforeEach(() => { fixture = createComponent(SliderWithTwoWayBinding); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; input = slider._getInput(_MatThumb.END) as MatSliderThumb; }); it('should sync the value binding in both directions', fakeAsync(() => { checkInput(input, {min: 0, max: 100, value: 0, step: 1, translateX: 0}); slideToValue(slider, input, 10); expect(fixture.componentInstance.value).toBe(10); checkInput(input, {min: 0, max: 100, value: 10, step: 1, translateX: 30}); fixture.componentInstance.value = 20; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.value).toBe(20); checkInput(input, {min: 0, max: 100, value: 20, step: 1, translateX: 60}); })); }); describe('range slider with a two-way binding', () => { let slider: MatSlider; let startInput: MatSliderRangeThumb; let endInput: MatSliderRangeThumb; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { fixture = createComponent(RangeSliderWithTwoWayBinding); fixture.detectChanges(); const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); slider = sliderDebugElement.componentInstance; endInput = slider._getInput(_MatThumb.END) as MatSliderRangeThumb; startInput = slider._getInput(_MatThumb.START) as MatSliderRangeThumb; })); it('should sync the start value binding in both directions', fakeAsync(() => { expect(fixture.componentInstance.startValue).toBe(0); expect(startInput.value).toBe(0); slideToValue(slider, startInput, 10); expect(fixture.componentInstance.startValue).toBe(10); expect(startInput.value).toBe(10); fixture.componentInstance.startValue = 20; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.startValue).toBe(20); expect(startInput.value).toBe(20); })); it('should sync the end value binding in both directions', fakeAsync(() => { expect(fixture.componentInstance.endValue).toBe(100); expect(endInput.value).toBe(100); slideToValue(slider, endInput, 90); expect(fixture.componentInstance.endValue).toBe(90); expect(endInput.value).toBe(90); fixture.componentInstance.endValue = 80; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.endValue).toBe(80); expect(endInput.value).toBe(80); })); }); }); const SLIDER_STYLES = ['.mat-mdc-slider { width: 300px; }']; @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class StandardSlider {} @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class StandardRangeSlider {} @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class DisabledSlider {} @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class DisabledRangeSlider {} @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class SliderWithMinAndMax { min = 25; max = 75; } @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class RangeSliderWithMinAndMax { min = 25; max = 75; } @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class SliderWithValue {} @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class RangeSliderWithValue {} @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class SliderWithStep { step = 25; } @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class RangeSliderWithStep { step = 25; } @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class DiscreteSliderWithDisplayWith { displayWith(v: number) { return `$${v}`; } } @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class DiscreteRangeSliderWithDisplayWith { displayWith(v: number) { return `$${v}`; } } @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class SliderWithOneWayBinding { value = 50; } @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class RangeSliderWithOneWayBinding { startValue = 25; endValue = 75; } @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class SliderWithNgModel { @ViewChild(MatSlider) slider: MatSlider; val: number | undefined = 0; } @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class RangeSliderWithNgModel { @ViewChild(MatSlider) slider: MatSlider; startVal: number | undefined = 0; endVal: number | undefined = 100; } @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class RangeSliderWithNgModelEdgeCase { @ViewChild(MatSlider) slider: MatSlider; startValue: number = -0.7; endValue: number = -0.3; } @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class SliderWithFormControl { control = new FormControl(0); } @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class RangeSliderWithFormControl { startInputControl = new FormControl(0); endInputControl = new FormControl(100); } @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class SliderWithTwoWayBinding { value = 0; } @Component({ template: ` `, styles: SLIDER_STYLES, standalone: false, }) class RangeSliderWithTwoWayBinding { @ViewChild(MatSlider) slider: MatSlider; @ViewChildren(MatSliderThumb) sliderInputs: QueryList; startValue = 0; endValue = 100; } /** Clicks on the MatSlider at the coordinates corresponding to the given value. */ function setValueByClick( slider: MatSlider, input: MatSliderThumb, value: number, isRtl: boolean = false, ) { const inputElement = input._elementRef.nativeElement; const val = isRtl ? slider.max - value : value; const {x, y} = getCoordsForValue(slider, value); dispatchPointerEvent(inputElement, 'pointerdown', x, y); input.value = val; dispatchEvent(input._hostElement, new Event('input')); input.focus(); dispatchPointerEvent(inputElement, 'pointerup', x, y); dispatchEvent(input._hostElement, new Event('change')); flush(); } /** Slides the MatSlider's thumb to the given value. */ function slideToValue(slider: MatSlider, input: MatSliderThumb, value: number) { const sliderElement = slider._elementRef.nativeElement; const {x: startX, y: startY} = getCoordsForValue(slider, input.value); const {x: endX, y: endY} = getCoordsForValue(slider, value); dispatchPointerEvent(sliderElement, 'pointerdown', startX, startY); input.focus(); dispatchPointerEvent(sliderElement, 'pointermove', endX, endY); input._hostElement.value = `${value}`; dispatchEvent(input._hostElement, new Event('input')); dispatchPointerEvent(sliderElement, 'pointerup', endX, endY); dispatchEvent(input._hostElement, new Event('change')); tick(10); } /** Returns the x and y coordinates for the given slider value. */ function getCoordsForValue(slider: MatSlider, value: number): Point { const {min, max} = slider; const percent = (value - min) / (max - min); const {top, left, width, height} = slider._elementRef.nativeElement.getBoundingClientRect(); const x = width * percent + left; const y = top + height / 2; return {x, y}; }