Easy Angular Testing – UI Elements – Slider and Slide Toggle

In this post, we’ll look into the Material Slider and Slide Toggle. In our example below, we develop a color picker that changes the color of a mat card as well as allows us to lock the individual colors in.

What do we use Sliders for? On Google’s Material site it describes the slider as

Sliders allow users to make selections from a range of values.

https://material.io/components/sliders/#

And from a usage perspective, the Angular Material site says

<mat-slider> allows for the selection of a value from a range via mouse, touch, or keyboard, similar to <input type="range">.

https://material.angular.io/components/slider/overview

What do we use the Slide Toggle for? On the Angular Material site it says

<mat-slide-toggle> is an on/off control that can be toggled via clicking.

https://material.angular.io/components/slide-toggle/overview

And on the Material site, the Slide Toggle is akin to the switch under selection controls. Of which, the Material site says –

Use switches to:

Toggle a single item on or off, on mobile and tablet

Immediately activate or deactivate something

Use switches to: Switches should be used instead of radio buttons if each item in a set can be independently controlled.

https://material.io/components/selection-controls/#usage

As it states above sliders give us a way to select a value from a range. Slide toggles allow us to turn functionality on and off. So let’s build a solution and test it!

The Code

Our HTML file contains four mat-cards. The first three cards contain a slider, which will be used to pick our value, as well as a slide toggle to disable the slider. This effectively locks that color in. We’re not using a reactive form here, so we use ngModel to pass values to and from our component.ts file.

The final mat-card is empty, and we use that to show the color that we are adjusting.

<div id="colorPickers">
  <mat-card>
    <label for="redSlider">Red</label>
    <mat-slider
      id="redSlider"
      [min]="1"
      [max]="255"
      [step]="1"
      [(ngModel)]="redValue"
      (change)="changeColorValue()"
      [thumbLabel]="true"
      [tickInterval]="1"
      [disabled]="redDisabled"
    ></mat-slider>
    <mat-slide-toggle
      class="color-toggle"
      [color]="color"
      [checked]="redChecked"
      [(ngModel)]="redDisabled"
    >
      Lock
    </mat-slide-toggle>
  </mat-card>
  <mat-card>
    <label for="greenSlider">Green</label>
    <mat-slider
      id="greenSlider"
      [min]="1"
      [max]="255"
      [step]="1"
      [(ngModel)]="greenValue"
      (change)="changeColorValue()"
      [thumbLabel]="true"
      [tickInterval]="1"
      [disabled]="greenDisabled"
    ></mat-slider>
    <mat-slide-toggle
      class="color-toggle"
      [color]="color"
      [checked]="greenChecked"
      [(ngModel)]="greenDisabled"
    >
      Lock
    </mat-slide-toggle>
  </mat-card>
  <mat-card>
    <label for="blueSlider">Blue</label>
    <mat-slider
      id="blueSlider"
      [min]="1"
      [max]="255"
      [step]="1"
      [(ngModel)]="blueValue"
      (change)="changeColorValue()"
      [thumbLabel]="true"
      [tickInterval]="1"
      [disabled]="blueDisabled"
    ></mat-slider>
    <mat-slide-toggle
      class="color-toggle"
      [color]="color"
      [checked]="blueChecked"
      [(ngModel)]="blueDisabled"
    >
      Lock
    </mat-slide-toggle>
  </mat-card>
</div>
<mat-card id="colorSelection"> </mat-card>

As we often do, we add a margin to our mat-cards to separate them from each other so we can see the cards more clearly. We also add inline-block to those cards so that we can show them in the same row.

div#colorPickers mat-card {
  margin: 0.1em;
  width: 30%;
  display: inline-block;
}

To use our functionality, we need to import the MatSliderModule, MatSlideToggleModule and MatCardModule in our module.ts and spec.ts files.

import {
  MatSliderModule,
  MatSlideToggleModule,
  MatCardModule
} from '@angular/material';

As we always try to do, our component.ts file is fairly sparse. This is so that we can focus on the testing. We do have enough in here to make it clear how to test, for example, a call out to a method. In our file we have our color disabled attributes as well as our value attributes defined for each individual color. These are adjusted by either the slide toggle, in the case of the disabled attributes, or the slider for the value attributes.

  redDisabled = false;
  greenDisabled = false;
  blueDisabled = false;

  redValue = 125;
  greenValue = 125;
  blueValue = 125;

When the component is first initilized, that is ngOnInit is called, it calls out to our changeColorValue() method, which as you would guess changes/sets the color of our colorSelection mat-card. Our changeColorValue method is simple, it grabs our colorSelection element using the dom, and gets our individual colors as integers from their respective attributes. It then creates a css rgb() value string that we will apply to the dom element we have. That is done trough the style.backgroundColor attribute of the element. Here is that code.

  changeColorValue() {
    const el = document.getElementById('colorSelection');

    const bgColor =
      'rgb(' +
      this.redValue +
      ', ' +
      this.greenValue +
      ', ' +
      this.blueValue +
      ')';

    el.style.backgroundColor = bgColor;
  }

And here is our full component.ts file.

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-slide-toggle-slider-ut',
  templateUrl: './slide-toggle-slider-ut.component.html',
  styleUrls: ['./slide-toggle-slider-ut.component.css']
})
export class SlideToggleSliderUtComponent implements OnInit {
  redDisabled = false;
  greenDisabled = false;
  blueDisabled = false;

  redValue = 125;
  greenValue = 125;
  blueValue = 125;

  constructor() {}

  ngOnInit() {
    this.changeColorValue();
  }

  changeColorValue() {
    const el = document.getElementById('colorSelection');

    const bgColor =
      'rgb(' +
      this.redValue +
      ', ' +
      this.greenValue +
      ', ' +
      this.blueValue +
      ')';

    el.style.backgroundColor = bgColor;
  }
}

The Tests

Now on to the interesting pieces, our tests!

We start, again, by importing our MatSlider, MatSlideToggle, and MatCard modules as well as our forms module.

Let’s dive into our first smaller test, ensuring that our color changing method is called. For this we use a very standard method of grabbing our slider. Here we’ve added an id of redSlider to the element to make it easier to find. We then create a spy looking for any calls to the component’s method. We don’t necessarily care in this case if the method’s functionality actually fires, so we don’t add .and.callThrough or and.callFake to our spyOn call. If we were checking the output of that method we would certainly do that.

    const spy = spyOn(component, 'changeColorValue');

We then trigger a mousedown event on the slider to get our (change) event to fire, detect those changes, and ensure the spy was called in our expect() call. Here is that test.

  it('should call our method that changes the color of the selection', () => {
    const sliderDE: DebugElement = fixture.debugElement;
    const sliderEL: HTMLElement = sliderDE.nativeElement;
    const slider = sliderEL.querySelector('mat-slider#redSlider');

    const spy = spyOn(component, 'changeColorValue');

    slider.dispatchEvent(new MouseEvent('mousedown'));
    fixture.detectChanges();

    expect(spy).toHaveBeenCalled();
  });

In the next smaller test, we want to ensure that the toggles lock the correct slider. We’re not testing the library’s slide toggle functionality, we’re testing ours by making sure we link the slide toggle to the correct element. We start by grabbing our individual slider that will change, so we can verify it really did change later.

    const slider = sElement.querySelector('mat-slider#redSlider');

We then want our slideToggle at the highest level. We grab it at that level because we can look for our id, and also there are a lot of elements nested in the toggle. Once we get the high level element we can traverse down to get what we need. With the slide toggle, we want to look down to an input element. That’s actually what triggers our value! When looking at the nesting of this element, it’s mat-slide-toggle -> label -> then both mat-slide-toggle-bar and mat-slide-toggle-element. Once here, we go into the mat-slide-toggle-bar. This is the bar and thumb that you see and click on. Here we grab the bar, and inside of that we have the input we are looking for. We skip a few levels in the second line, and go right for the input.

    const slideToggle = sElement.querySelector('mat-slide-toggle#redToggle');
    const slideToggleBarInput = slideToggle.querySelector('input.mat-slide-toggle-input');

Then we simply fire our click event on the input, detect the changes and test for the element’s disabled attribute to be true. We look at the attribute ng-reflect-disabled for this. The ng-reflect attributes are there specifically for testing, so be sure to use those when they exist!

    slideToggleBarInput.dispatchEvent(new MouseEvent('click'));
    fixture.detectChanges();

    expect(slider.getAttribute('ng-reflect-disabled')).toBeTruthy();

Here is the full test.

  it('should lock the color selection when the slider is switched', () => {
    const dElement: DebugElement = fixture.debugElement;
    const sElement: HTMLElement = dElement.nativeElement;

    const slider = sElement.querySelector('mat-slider#redSlider');
    const slideToggle = sElement.querySelector('mat-slide-toggle#redToggle');
    const slideToggleBarInput = slideToggle.querySelector('input.mat-slide-toggle-input');

    slideToggleBarInput.dispatchEvent(new MouseEvent('click'));
    fixture.detectChanges();

    expect(slider.getAttribute('ng-reflect-disabled')).toBeTruthy();
  });

Since we have three toggles and three sliders, we could loop through this functionality and test all toggles and sliders, which we would want to do in the real world. Here we show the test once only for demonstration.

Finally our largest and most interesting test, from a coding perspective. We would want to pull out some of this functionality into utility functions to keep our spec.ts file lean, and allow reuse, but in this case I want to show the full flow of the test in one place.

There’s a lot of setup for this test. First we grab our slider as we normally would. We then set a percentage variable that will be used to determine where on our slider we are going to click. Just like in the test above, we have to dive into the slider element to get the element we actually need to click on. Here it’s the mat-slider-wrapper. We then call .getBoundingClientRect() to get not only how large the slider is, but where it exists on the page. We’ll use this value shortly.

    const sliderDE: DebugElement = fixture.debugElement;
    const sliderEL: HTMLElement = sliderDE.nativeElement;
    const slider = sliderEL.querySelector('mat-slider#redSlider');

    const percentage = 0.25;
    const trackElement = slider.querySelector('.mat-slider-wrapper');
    const dimensions = trackElement.getBoundingClientRect();

From our dimensions const we pull out the location on the page into the x and y variables. Then we find a location in the element itself to click on. We take the dimensions of the element, and multiply by our percentage variable. With the result we have the relative position inside of the element to click on. By adjusting the percentage, we can click and get different values.

    const x = dimensions.left;
    const y = dimensions.top;
    const relativeX = Math.round(dimensions.width * percentage);
    const relativeY = Math.round(dimensions.height * percentage);

We now create an event so we can perform the click. In past tests, we’ve used the built in mousedown or click events. Here we are going to dive into the initMouseEvent call and set the location of the click. If we don’t do this, the click will be in the middle of the slider. Since we initialize the slider with a 50% value, this won’t change anything! If you don’t initialize to 50%, you can go ahead and use an out of the box mousedown event.

    const event = document.createEvent('MouseEvent');

    event.initMouseEvent(
      'mousedown',
      true /* canBubble */,
      true /* cancelable */,
      window /* view */,
      0 /* detail */,
      x /* screenX */,
      y /* screenY */,
      relativeX /* clientX */,
      relativeY /* clientY */,
      false /* ctrlKey */,
      false /* altKey */,
      false /* shiftKey */,
      false /* metaKey */,
      0 /* button */,
      null /* relatedTarget */
    );

Once we trigger our click event, we need to be sure the color changed. The way we’ll do that here is by setting a const to the color value we expect, grabbing the new color, and testing that they are equal. The backgroundColor style attribute is stored as text. When we pull it out of our element, it will look like ‘rgb(NNN, NNN, NNN)’. Since this is the format of the value, we set up expectedColor element as text. Two of the values here don’t change and will be of value 125, so we add those as static text. The red value is what we need to test for. To do this, we grab our slider, and look to a ng-reflect attribute like we did above. In this case we go after ng-reflect-model to get the current value of the red slider. We then inject that into our string, as it will be what the red color of our colorSelection element will be. We can now easily test that both our colorSelection and expectedColor are the same.

    const expectedColor =
      'rgb(' + slider.getAttribute('ng-reflect-model') + ', 125, 125)';

    const newColor = document.getElementById('colorSelection').style
      .backgroundColor;

    expect(newColor).toBe(expectedColor);

Here is the full test.

  it('should change the color of the color selection when the slider is adjusted', () => {
    const sliderDE: DebugElement = fixture.debugElement;
    const sliderEL: HTMLElement = sliderDE.nativeElement;
    const slider = sliderEL.querySelector('mat-slider#redSlider');

    const percentage = 0.25;
    const trackElement = slider.querySelector('.mat-slider-wrapper');
    const dimensions = trackElement.getBoundingClientRect();

    const x = dimensions.left;
    const y = dimensions.top;
    const relativeX = Math.round(dimensions.width * percentage);
    const relativeY = Math.round(dimensions.height * percentage);

    const event = document.createEvent('MouseEvent');

    event.initMouseEvent(
      'mousedown',
      true /* canBubble */,
      true /* cancelable */,
      window /* view */,
      0 /* detail */,
      x /* screenX */,
      y /* screenY */,
      relativeX /* clientX */,
      relativeY /* clientY */,
      false /* ctrlKey */,
      false /* altKey */,
      false /* shiftKey */,
      false /* metaKey */,
      0 /* button */,
      null /* relatedTarget */
    );

    slider.dispatchEvent(event);
    fixture.detectChanges();

    const expectedColor =
      'rgb(' + slider.getAttribute('ng-reflect-model') + ', 125, 125)';

    const newColor = document.getElementById('colorSelection').style
      .backgroundColor;

    expect(newColor).toBe(expectedColor);
  });

And here is our full spec.ts file.

import {
  async,
  ComponentFixture,
  TestBed
} from '@angular/core/testing';

import { SlideToggleSliderUtComponent } from './slide-toggle-slider-ut.component';
import { DebugElement } from '@angular/core';
import {
  MatSliderModule,
  MatSlideToggleModule,
  MatCardModule
} from '@angular/material';
import { FormsModule } from '@angular/forms';

describe('SlideToggleSliderUtComponent', () => {
  let component: SlideToggleSliderUtComponent;
  let fixture: ComponentFixture<SlideToggleSliderUtComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [SlideToggleSliderUtComponent],
      imports: [
        MatSliderModule,
        MatSlideToggleModule,
        FormsModule,
        MatCardModule
      ]
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(SlideToggleSliderUtComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should change the color of the color selection when the slider is adjusted', () => {
    const sliderDE: DebugElement = fixture.debugElement;
    const sliderEL: HTMLElement = sliderDE.nativeElement;
    const slider = sliderEL.querySelector('mat-slider#redSlider');

    const percentage = 0.25;
    const trackElement = slider.querySelector('.mat-slider-wrapper');
    const dimensions = trackElement.getBoundingClientRect();

    const x = dimensions.left;
    const y = dimensions.top;
    const relativeX = Math.round(dimensions.width * percentage);
    const relativeY = Math.round(dimensions.height * percentage);

    const event = document.createEvent('MouseEvent');

    // https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-eventgroupings-mouseevents
    event.initMouseEvent(
      'mousedown',
      true /* canBubble */,
      true /* cancelable */,
      window /* view */,
      0 /* detail */,
      x /* screenX */,
      y /* screenY */,
      relativeX /* clientX */,
      relativeY /* clientY */,
      false /* ctrlKey */,
      false /* altKey */,
      false /* shiftKey */,
      false /* metaKey */,
      0 /* button */,
      null /* relatedTarget */
    );

    slider.dispatchEvent(event);
    fixture.detectChanges();

    const expectedColor =
      'rgb(' + slider.getAttribute('ng-reflect-model') + ', 125, 125)';

    const newColor = document.getElementById('colorSelection').style
      .backgroundColor;

    expect(newColor).toBe(expectedColor);
  });

  it('should call our method that changes the color of the selection', () => {
    const sliderDE: DebugElement = fixture.debugElement;
    const sliderEL: HTMLElement = sliderDE.nativeElement;
    const slider = sliderEL.querySelector('mat-slider#redSlider');

    const spy = spyOn(component, 'changeColorValue');

    slider.dispatchEvent(new MouseEvent('mousedown'));
    fixture.detectChanges();

    expect(spy).toHaveBeenCalled();
  });

  it('should lock the color selection when the slider is switched', () => {
    expect(component).toBeTruthy();
  });
});

Wrapup

Sliders are great for allowing users to select values from a range. It’s a natural movement, and easily understood what will happen when you click and drag on a slider. Sliders have many options for value range and steps, so they are easy to customize to get them to fit a certain scenario. Material slide toggles are great in that they mimic the look of a real world switch. Again, this control makes it easy to understand what the outcome will be of clicking on it.

We only tested the basics of Sliders and Slide Toggles here, just enough to grab the bits and pieces we would need to then dive deeper. While sliders and slide toggles lend themselves well for mobile development, due to the natural movement of the elements on the devices, they work equally as well on the web when viewed on a desktop device without a touchscreen.

Always remember, our goal in testing shouldn’t be individual element based it should be looking for outcomes of a workflow. Testing a user expectation. So the tests outlined above should be part of a larger “should” and shouldn’t be the ends itself.

All of the code shown here, and more Angular tests, can be found in this GitHub repo.