Easy Angular Testing – UI Elements – Snackbar

Without a User Interface, our application is just…well really it’s probably still super useful! But if we want our application to be useful to the non-superhero non-programmer, we’ll probably want to add some type of UI to it.

There are a lot of elements that can go into a UI – dialogs, lists, spinners, media, buttons. Even the data itself can be looked at on the front end as a UI element that contains or is built of fonts, spacing, positioning. And if you’re using Angular, we can be safe to say that the UI is a terribly important part of your application. And because of this, we need to test it.

In our example below, we’re interested in “non-interactive” elements which would include headings, lists, icons, spinners, dialogs and the data we display in and around them. Specifically here we’re testing the Snackbar. We’ll save more interactive elements like buttons, checkboxes and drop downs for other exercises.

What’s this snackbar thing good for? On Angular’s Material site it describes the snackbar as

MatSnackBar is a service for displaying snack-bar notifications.

https://material.angular.io/components/snack-bar/overview

Which doesn’t help us understand why we should ever use a snackbar. Fortunately, Google’s Material site has a bit more information on the why

Snackbars inform users of a process that an app has performed or will perform. They appear temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience, and they don’t require user input to disappear.

https://material.io/components/snackbars/

Ahh, OK, that’s better. Whenever I want to understand a Material component a little more, I like to check the Google documentation. The Material.io site has nice descriptions and some “rules of the road” for the use of these design elements.

With that in mind, let’s get started testing!

Testing the Snackbar

Our element under test, as we mentioned, is the snackbar. Giving the user feedback on some process that might or is executing is important. Like the tooltip and other material goodies, the snackbar gives us a way to do this without adding reams of text onto our UI, so we can keep our UI clean. And while the scope of testing a snackbar is limited, at times you’ll find yourself needing to do it.

First let’s look into our component.ts file. The first thing you’ll notice is that in our constructor we are injecting a MatSnackBarRef of type SnackbarUTExampleComponent. With snackbars, you can simply add a message and other information into the snackbar when you instantiate it. If you want more control, or perhaps a reusable message, you can also inject a component with your snackbar layout and message into the call for a new snackbar. Our SnackbarUTExampleComponent is there to show how to perform the latter. This component is dynamically loaded, so be sure to add it into the module’s entry components so you can get your ComponentFactory compiled in.

@NgModule({
  declarations: [
    SnackbarUtComponent,
    SnackbarUTExampleComponent
  ]
  imports: [
    MatSnackBarModule
  ],
  entryComponents: [ SnackbarUTExampleComponent ]
})

One thing you’ll notice below, is we have two components in the same file. This is fine in the case below, where the component will not be used anywhere else but this file. In reality, you’ll want to split that component out into its own file. You can rest assured that at some point in your app you’ll need it again. We’re also injecting a snackbar into the constructor of our main component. This is a good practice, as some time in the future we might want to use a third party, or custom developed, snackbar. If we use the interface provided by Material, we can simply push the new snackbar into our component when it’s instantiated.

In our first method, openSnackbarMessage(), we’re simply showing the snackbar and then waiting for observeSnackBarOnAction() to detect that the button within our snackbar was pressed. When that action is taken, here we log a message to console, in the real world we would call some code to perform some business logic. If the button isn’t pressed in five seconds, the snackbar gracefully leaves our view.

In the other method called by a button click, we’re creating our snackbar from our custom component. In this method we wanted a little pause generated by setTimeout(). Why? Because a nice use of snackbar is for notifications that pop up in a browser when some other process needs to notify the user. That process could run for a bit before the snackbar appears. I would not add that into real code, but here we want to have a pause.

import { Component, OnInit, Inject } from '@angular/core';
import { MatSnackBar, MatSnackBarRef } from '@angular/material/snack-bar';
import { MAT_SNACK_BAR_DATA } from '@angular/material/snack-bar';
@Component({
  selector: 'app-snackbar-ut-example',
  templateUrl: 'snackbar-ut-component-example.html',
  styleUrls: ['./snackbar-ut.component.css']
})
export class SnackbarUTExampleComponent {
  constructor(
    public snackBarRef: MatSnackBarRef<SnackbarUTExampleComponent>,
    @Inject(MAT_SNACK_BAR_DATA) public data: any
  ) {}
}
@Component({
  selector: 'app-snackbar-ut',
  templateUrl: './snackbar-ut.component.html',
  styleUrls: ['./snackbar-ut.component.css']
})
export class SnackbarUtComponent {
  constructor(private _snackBar: MatSnackBar) {}
  openSnackBarMessage() {
    const firstSnackBarRef = this._snackBar.open(
      'Snacking message',
      'do the thing',
      { duration: 5000 }
    );
    this.observeSnackBarOnAction(firstSnackBarRef);
  }
  observeSnackBarOnAction(firstSnackBarRef) {
    firstSnackBarRef.onAction().subscribe(() => {
      this.justWriteAConsoleLog();
    });
  }
  justWriteAConsoleLog() {
    console.log('Doing a thing based on the second button press');
  }
  openSnackBarComponent() {
    setTimeout(() => {
      this._snackBar.openFromComponent(SnackbarUTExampleComponent, {
        duration: 5000,
        data: 'Hey! This took an extra second to show!'
      });
    }, 1000);
  }
}

Our component.html is as simple as can be. Two buttons, one to call our message based code, and one to call our component based code. I’ve added an id to each. This helps both with styling, and when we have to find the button in our code when we have multiple of a type of element. In this case we’re using it later in our tests.

<button
  mat-button
  id='messageButton'
  (click)="openSnackBarMessage()"
  aria-label="Show an example snack-bar"
>
  SB Message 
</button>
<button
  mat-button
  id='compButton'
  (click)="openSnackBarComponent()"
  aria-label="Show an example snack-bar based on a component"
>
  SB Message from a component
</button>

Our simple component-example.html file. It has a message, some data passed in from code, and a button. Also notice that we call snackBarRef.dismiss(). In our component for the custom snackbar we have two variables injected, our MatSnackBarRef named snackBarRef, and some MAT_SNACK_BAR_DATA called data. When we want to get rid of our snackbar with a button press, and trigger our onAction() call, we do that with the call to snackBarRef.dismiss()

export class SnackbarUTExampleComponent {
  constructor(
    public snackBarRef: MatSnackBarRef<SnackbarUTExampleComponent>,
    @Inject(MAT_SNACK_BAR_DATA) public data: any
  ) {}
}
<span class="example-ut-snackbar">
  Snacking from a component!
</span>
<br />
<span>{{ data }}</span>
<button mat-button color="accent" (click)="snackBarRef.dismiss()">Do the other thing</button>

Our tests.

Now on to the meat of the conversation. We have three tests that aren’t really good examples of BDD (Behaviour Driven Development) level tests, but are here to show you the technical how to of creating tests around the snackbar.

The first test is simply looking if we have a snackbar after we press the button. This “should create a snackbar” test, when we write our actual tests, would be us saying it should take some action. This could be “it should save the order” or “it should validate the ticket”. The snackbar that pops up would be the result of some other process running, and notifying us, and not just that we pressed a button or received a snackbar.

In our test case, we have multiple buttons on the page. As we discussed above, it made sense to give those buttons ids so we could reference the buttons easier. This is one case where we’ll make use of that. Here we’re getting our fixture.debugElement.nativeElement, and looking for it using querySelector with the element name ‘button’ and the id ‘messageButton’. For us this will return one element, the one we’re looking for. We added in the ‘as’ call to turn this element into a button element, otherwise it would be a straight HTMLelement and the click event wouldn’t exist. This isn’t a deal breaker, as we’ll see later, but makes our test better able to express intent. Make sure you call fixture.detectChanges(), otherwise your button click will not be recognized.

const buttonDe: DebugElement = fixture.debugElement;
const buttonEl: HTMLElement = buttonDe.nativeElement;
const button = buttonEl.querySelector(
      'button#messageButton'
    ) as HTMLButtonElement;
button.click();
fixture.detectChanges();

And here we grab our snackbar. The snackbar is part of the CDK (Component Dev Kit) overlay. It lives at div#cdk-overlay-container -> div#cdk-global-overlay-wrapper -> div#cdk-overlay-0 -> snack-bar-container.mat-snack-bar-container.

const snackingDiv = document.querySelector('snack-bar-container');    
expect(snackingDiv).toBeTruthy();

The message and action themselves live at simple-snack-bar.mat-simple-snackbar (message) -> div.mat-simple-snackbar-action -> button, which we’ll use in our third test.

The second test is identical to our first test, only we are getting the snackbar created with our component. Since the code is the same, we will skip the description of that test and move to our third, more interesting test.

Unlike our first two tests where we are just interested in seeing a snackbar, in our third test we are calling out to some action from the snackbar. This can be a service, some code in our current component, or something else. The basic idea behind this test, is that our snackbar is asking us if we want to do something. We do, and so click the button on the first snackbar which would then trigger that action to be called. In our real code and test, we would want to mock out any service or other code that we call from the button. We always want to isolate our SUT (system under test). We would then spy on our code to ensure it’s called. Here we add a fake to our spy that simply prints to the console.

 const ourSpy = spyOn(component, 'observeSnackBarOnAction').and.callFake(
    () => {
        console.log('We could use a mock or stub here!');
    }
);

Once we have our first button and our spy, we click the button on our first snackbar. This is exactly like the first and second test. The difference is, when we click that button to trigger the event, the event needs some other action taken from the snackbar. Instead of ignoring the message, we’re clicking the action on the snackbar as well. Since we can only have one snackbar at a time, per the Material documentation, we only have to look for one div with the class of mat-simple-snackbar-action, and the button that lives in that div.

const snackingDivButton = document.querySelector(
    'div.mat-simple-snackbar-action button'
);

This time I didn’t want to coerce my element. Instead I am using detectChanges, with no other reason to show you it can be done. Then we, as always, call fixture.detectChanges() to ensure the fixture knows we’ve done a thing.

const mouseEvent = new MouseEvent('click');
snackingDivButton.dispatchEvent(mouseEvent);
fixture.detectChanges();

We don’t have to in this case, but I am calling fixture.whenRenderingDone(). When we’re testing code that is interacting with the UI, you want to be sure that the fixture is stable after rendering. This call gets a promise that resolves when the UI’s state is stable after animations. Inside the arrow function, I have our expect call. This time I want to be sure our spy is called. We don’t care if the service that’s injected works, we should test that as part of the service’s code, instead we just want to know that the code that triggers the service in this particular component was called properly.

fixture.whenRenderingDone().then(() => {
    expect(ourSpy).toHaveBeenCalled();
});

And here’s our full test, in all it’s glory.

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { SnackbarUtComponent } from './snackbar-ut.component';
import { DebugElement } from '@angular/core';
describe('ui-noninteractive - SnackbarUtComponent', () => {
  let component: SnackbarUtComponent;
  let fixture: ComponentFixture<SnackbarUtComponent>;
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [SnackbarUtComponent],
      imports: [MatSnackBarModule, BrowserAnimationsModule]
    }).compileComponents();
  }));
  beforeEach(() => {
    fixture = TestBed.createComponent(SnackbarUtComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
  it('should create', () => {
    expect(component).toBeTruthy();
  });
  it('should create a snackbar from a message when the button is clicked', () => {
    const buttonDe: DebugElement = fixture.debugElement;
    const buttonEl: HTMLElement = buttonDe.nativeElement;
    const button = buttonEl.querySelector(
      'button#messageButton'
    ) as HTMLButtonElement;
    button.click();
    fixture.detectChanges();
    const snackingDiv = document.querySelector('snack-bar-container');
    expect(snackingDiv).toBeTruthy();
  });
  it('should create a snackbar using our component when the button is clicked', () => {
    const buttonDe: DebugElement = fixture.debugElement;
    const buttonEl: HTMLElement = buttonDe.nativeElement;
    const button = buttonEl.querySelector(
      'button#compButton'
    ) as HTMLButtonElement;
    button.click();
    fixture.detectChanges();
    const snackingDiv = document.querySelector('snack-bar-container');
    expect(snackingDiv).toBeTruthy();
  });
  it('should do a thing when action is taken on the first snackbar', () => {
    const buttonDe: DebugElement = fixture.debugElement;
    const buttonEl: HTMLElement = buttonDe.nativeElement;
    const button = buttonEl.querySelector('button');
    const ourSpy = spyOn(component, 'observeSnackBarOnAction').and.callFake(
      () => {
        console.log('We could use a mock or stub here!');
      }
    );
    button.click();
    fixture.detectChanges();
    const snackingDivButton = document.querySelector(
      'div.mat-simple-snackbar-action button'
    );
    const mouseEvent = new MouseEvent('click');
    snackingDivButton.dispatchEvent(mouseEvent);
    fixture.detectChanges();
    fixture.whenRenderingDone().then(() => {
      expect(ourSpy).toHaveBeenCalled();
    });
    expect(ourSpy).toHaveBeenCalled();
  });
});

We do a bit better here at testing some action, instead of just testing functionality. What I mean is we’ve moved towards BDD (Behaviour Driven Development), even if in these contrived code examples that can be hard. I think we’ve been successful at showing the technical how-to, in this case testing the snackbar. Remember though that 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.