Unit Testing Angular Components That Use MatIconRegistry
Tags: angular, testing
Published: 2020-06-12 18:48:08

If you use an Angular Material component (part of Angular Components) that relies on the `MatIconRegistry`, chances are you have wondered how you could test it with mock data. I recently ran into this, and figured I would turn it into a post as a way to document it for myself. Who knows -- maybe one of you out there might find this helpful as well!

`MatIconRegistry` is useful if you have a custom SVG icon set that you want to use within your app. This allows you to explicitly add the icons under a custom namespace using the `addSvgIcon(iconName: string, url: string)` method.

Let's say you use the `mat-icon` component in _your_ component's template:

```html
<mat-icon svgIcon="icon-name"></mat-icon>
```

...testing your component means you'll need to ensure that the the icon is served properly, at least with a fake icon.

To do that, you'll need to first add the icon names that your component expects to find in the registry. Ideally, you'd do this in the `beforeEach` setup function. 

```typescript
import { HttpClientTestingModule } from "@angular/common/http/testing";
import { inject, TestBed } from "@angular/core/testing";
import { MatIconRegistry } from "@angular/material/icon";
import { DomSanitizer } from "@angular/platform-browser";

describe("TestingComponent", () => {
    const fakeSvg = `<svg><path id="someId" name="someSvg"></path></svg>`;
    const fakeIconPath = "/fake/icon.svg";

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
    }).compileComponents();
  });

  beforeEach(inject([MatIconRegistry, DomSanitizer], (mir: MatIconRegistry, sanitizer: DomSanitizer) => {
    // The `MatIconRegistry` will make GET requests to fetch any SVG icons that are in the registry. More on this below...
    const sanitizedUrl = sanitizer.bypassSecurityTrustResourceUrl(fakeIconPath);
    // Make sure that the icon name matches the icon name your component would be looking up.
    mir.addSvgIcon("icon-name", sanitizedUrl);
  }));

  it("test my component", () => {
     // Create a test fixture of your component's instance and test your component.
  });
}
```

That should do it, right? Nope. Your test would likely fail with a timeout error. But the reason may not be immediately clear. The answer lies within the functionality of the `MatIconRegistry` and what happens when you use the attribute with `svgIcon` with a `<mat-icon>` component.

Looking at the docs for [`<mat-icon>`](https://material.angular.io/components/icon/overview#svg-icons), note how it says this:

> In order to guard against XSS vulnerabilities, any SVG URLs and HTML strings passed to the MatIconRegistry must be marked as trusted by using Angular's DomSanitizer service.
> 
> MatIconRegistry fetches all remote SVG icons via Angular's HttpClient service. If you haven't included HttpClientModule from the @angular/common/http package in your NgModule imports, you will get an error at runtime.

That's good. We did that in our `beforeEach` call above. Note that for unit testing we use `HttpClientTestingModule` and not `HttpClientModule` because, well, it's a unit testing compatible HTTP client. One that doesn't actually hit a real endpoint. This means we need to control the response we send back to callers making HTTP requests based on the URL they request. Sort of like setting up a method spy and returning a mock value when the method is called. Only here we would be matching against the URL being requested and return an appropriate response.

So let's update our snippet above:

```typescript
import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
import { inject, TestBed } from "@angular/core/testing";
import { MatIconRegistry } from "@angular/material/icon";
import { DomSanitizer } from "@angular/platform-browser";

describe("TestingComponent", () => {
    const fakeSvg = `<svg><path id="someId" name="someSvg"></path></svg>`;
    const fakeIconPath = "/fake/icon.svg";

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
    }).compileComponents();
    httpTestingController = TestBed.inject(HttpTestingController);
  });

  beforeEach(inject([MatIconRegistry, DomSanitizer], (mir: MatIconRegistry, sanitizer: DomSanitizer) => {
    // The `MatIconRegistry` will make GET requests to fetch any SVG icons that are in the registry. More on this below...
    const sanitizedUrl = sanitizer.bypassSecurityTrustResourceUrl(fakeIconPath);
    // Make sure that the icon name matches the icon name your component would be looking up.
    mir.addSvgIcon("icon-name", sanitizedUrl);
  }));

  it("test my component", () => {
     // Create a test fixture of your component's instance and test your component.
     ...
     ...
     // Use this to capture icon requests
     // and flush it manually so that the source observable will emit a value. Otherwise, async calls will timeout waiting for a response.
     httpTestingController.match(fakeIconPath)[0].flush(fakeSvg);
  });
}
```

That's it! Happy testing!🙂