Testing Ionic 6 Popups

Introduction

If you are anything like me, you get a lot of use out of the Ionic Framework’s “popups” - Alert (ion-alert), Loading (ion-loading), Toast (ion-toast), Modal (ion-modal), etc.

But how do we get ahold of these things in order to test?

The Wrong Approach

Initially, let’s try the approach that comes to mind when testing a button click on a Toast.

First, we will set the cssClass of the button we want to target in order to be able to grab it:

buttons: [
        {
          text: 'Retry',
          cssClass: 'retry-button',
          handler: () => {
            const date = this.current.date;
            this.retrieveEntry(moment(date));
          }
        },
      ]

Next, we write our test to make sure that retry button works, and is calling the retrieveEntry function:

it('should make a call to retrieval method on retry', async () => {
      spyOn(component, 'retrieveEntry');

      // Will present Toast:
      await component.retryRetrieval();
      fixture.detectChanges();

      const retryButton = fixture.debugElement.query(By.css('.retry-button'));
      retryButton.nativeElement.click();
      fixture.detectChanges();

      expect(component.retrieveEntry).toHaveBeenCalled();
});

Looks good…

But wait, the test fails! Why? retryButton is coming back null! But why?

Shadow DOM

Well, it turns out that that since Ionic 4, Ionic components are using Shadow DOM. What’s that you ask? Basically, a bit of DOM that is isolated from the global scope. In our case, the Toast and the button we are trying to target.

Check out this article to learn a bit more about working with Shadow DOM.

Getting a Solution

The first thing we need to do is find the Shadow host. In this case, it ends up being the Toast container. In order to reference this, we set an id inside of htmlAttributes in our Toast properties:

const toast = await this.toastController.create({
      message: 'An error occurred retrieving entry.',
      htmlAttributes: {
        id: 'retry-toast'
      },
      color: 'danger',
const host = document.getElementById('retry-toast');

Next, let’s get the Shadow root -

const root = host.shadowRoot;

Now we’re in the Shadow DOM! We can grab that button:

const retryButton = root.querySelector('.retry-button') as HTMLElement;

Here’s what it looks like all put together:

it('should make a call to retrieval method on retry', async () => {
    spyOn(component, 'retrieveEntry');

    await component.retryRetrieval();
    fixture.detectChanges();

    const host = document.getElementById('retry-toast');
    const root = host.shadowRoot;
    const retryButton = root.querySelector('.retry-button') as HTMLElement;

    retryButton.click();
    fixture.detectChanges();

    expect(component.retrieveEntry).toHaveBeenCalled();
});

Conclusion

The introduction of Shadow DOM in Ionic 4 has made testing components a touch trickier, but once you understand the API and how it works, it’s not too much more difficult to test!


Erik August Johnson is a software developer working with JavaScript to build useful things. Follow them on Twitter.