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!