How do I add svg files via MatIconRegistry in unit tests?
Asked Answered
B

7

36

I'm currently implementing 'unit testing' in my angular application. However, if I run them, I receive multiple warnings/errors similar to this one: 'Error retrieving icon: Unable to find icon with the name ":myIcon"'. I suspect it might be caused by not adding the svgs to my MatIconRegistry. I usually do this in my AppComponent, like so:

constuctor(private iconRegistry: MatIconRegistry,
           private sanitizer: DomSanitizer,
           ...) {
        iconRegistry.addSvgIcon('myIcon', sanitizer.bypassSecurityTrustResourceUrl('./assets/icons/myIcon.svg'));
}

If I run a unit test of another component though, this will not execute and thus not add my svg to the registry. I already tried adding it in my .spec file of the corresponding component, like so:

fdescribe('MyComponent', () => {
  let component: MyComponent;
  let fixture: ComponentFixture<MyComponent>;
  let iconRegistry;
  let sanitizer;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        ...
      ],
      imports: [
        ...
      ],
      providers: [
        ...
      ],
      schemas: [CUSTOM_ELEMENTS_SCHEMA]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    iconRegistry = TestBed.get(MatIconRegistry);
    sanitizer = TestBed.get(DomSanitizer);
    iconRegistry.addSvgIcon('myIcon', sanitizer.bypassSecurityTrustResourceUrl('./../../assets/icons/myIcon.svg'));
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

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

If I run this, it doesn't work. It just returns a different error message:

Error retrieving icon: <svg> tag not found

My initial thought was that I have made a mistake in the path, but after trying various other paths I'm sure thats not the case.

Does anyone know how do solve this problem? Or maybe there is a better way doing this, since I would have to add my svg icon in every component I'm testing, which would be kinda redundant.

Bowline answered 2/4, 2019 at 10:14 Comment(0)
B
22

Mock the mat-icon selector with the following component at the top of the unit test

@Component({
    selector: 'mat-icon',
    template: '<span></span>'
})
class MockMatIconComponent {
    @Input() svgIcon: any;
    @Input() fontSet: any;
    @Input() fontIcon: any;
}

Then override the MatIconModule in the unit test as follows

beforeEach(() => {
    TestBed.configureTestingModule({
        declarations: [ ...],
        providers: [ ...  ],
        imports: [ MatIconModule, NoopAnimationsModule ]
    })
    .overrideModule(MatIconModule, {
    remove: {
       declarations: [MatIcon],
       exports: [MatIcon]
    },
    add: {
        declarations: [MockMatIconComponent],
        exports: [MockMatIconComponent]
   }
   })
  .compileComponents();

You will no longer have the 'Error retrieving icon: Unable to find icon with the name ":myIcon"' issue when running the unit tests

Bicker answered 5/4, 2019 at 4:32 Comment(4)
I actually found another answer similar to this a few days ago and thats exactly how I did it in the end and it works great! Still thank you for adding this information! Might help people who end up on this question.Bowline
@gv99 what was the similar answer to this that you found? and I noticed this works by ignoring the mat-icon altogether. What if you actually do ant to load the svg icon?Weingarten
Just pray you never have to maintain someone else's angular unit tests.Billowy
@vandermast: looks like you accidentally re-added the component metadata to the bottom of the MockMatIconComponent class. selector and template properties are nonsensical in the body of the class. (Also, the end parenthesis should be removedDiedra
G
79

can just do:

import { MatIcon } from '@angular/material/icon';
import { MatIconTestingModule } from '@angular/material/icon/testing';

describe('MyComponent', () => {

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [MyComponent, MatIcon],
      imports: [MatIconTestingModule],
    }).compileComponents();
  }));

...

});

This will generate a test icon rendering without the HTTP request.

NOTE: Newer versions of Angular, e.g., >16, may produce an error unless you remove import { MatIcon }.

Gravimetric answered 9/6, 2020 at 8:3 Comment(6)
underrated answerLithophyte
agreed. not knocking accepted answer, but you'd think the testing module is there for a reason.Waylin
the best! how much code I have already written to register each iconDyanne
Using MatIconTestingModule is really the easiest solution. Just wanted to mention again :-)Glucoside
If underrated was a SO answer:Betide
after trying this solution, I still get 20 07 2022 12:52:47.521:ERROR [karma-server]: UnhandledRejectionVinavinaceous
B
22

Mock the mat-icon selector with the following component at the top of the unit test

@Component({
    selector: 'mat-icon',
    template: '<span></span>'
})
class MockMatIconComponent {
    @Input() svgIcon: any;
    @Input() fontSet: any;
    @Input() fontIcon: any;
}

Then override the MatIconModule in the unit test as follows

beforeEach(() => {
    TestBed.configureTestingModule({
        declarations: [ ...],
        providers: [ ...  ],
        imports: [ MatIconModule, NoopAnimationsModule ]
    })
    .overrideModule(MatIconModule, {
    remove: {
       declarations: [MatIcon],
       exports: [MatIcon]
    },
    add: {
        declarations: [MockMatIconComponent],
        exports: [MockMatIconComponent]
   }
   })
  .compileComponents();

You will no longer have the 'Error retrieving icon: Unable to find icon with the name ":myIcon"' issue when running the unit tests

Bicker answered 5/4, 2019 at 4:32 Comment(4)
I actually found another answer similar to this a few days ago and thats exactly how I did it in the end and it works great! Still thank you for adding this information! Might help people who end up on this question.Bowline
@gv99 what was the similar answer to this that you found? and I noticed this works by ignoring the mat-icon altogether. What if you actually do ant to load the svg icon?Weingarten
Just pray you never have to maintain someone else's angular unit tests.Billowy
@vandermast: looks like you accidentally re-added the component metadata to the bottom of the MockMatIconComponent class. selector and template properties are nonsensical in the body of the class. (Also, the end parenthesis should be removedDiedra
S
7

Just spent a couple hours on this. It looks like Angular now provides a FakeMatIconRegistry. It squashed about 90% of the karma warnings, but still a few remain...

Had to do a lot of this:

TestBed.configureTestingModule({
    declarations: [ MyComponent ],
    imports: [
        ...
        MatIconModule,
    ],
    providers: [
        ...
        { provide: MatIconRegistry, useClass: FakeMatIconRegistry }
    ]
}).compileComponents();
Sarong answered 9/4, 2020 at 23:38 Comment(1)
Taking this answer as a clue I ended up copying the FakeMatIconRegistry source code and then imports: [MatIconModule, MatIconTestingModule], I did not use update any providerYahairayahata
S
1

Using typemoq for mocking; following worked for me:

const mockIconRegistry = Mock.ofType<MatIconRegistry>();
mockIconRegistry.setup(s => s.getNamedSvgIcon(It.isAny(), It.isAny())).returns(() => of(Mock.ofType<SVGElement>().object));

and then

providers: [{ provide: MatIconRegistry, useFactory: () => mockIconRegistry.object }]
Scharaga answered 14/8, 2019 at 14:19 Comment(0)
D
1

What you could do is install ng-mocks and use MockModule to mock the MatIconModule in your tests e.g.:

beforeEach(async(() => {
  TestBed.configureTestingModule({
    imports: [MockModule(MatIconModule)],
  }).compileComponents();
}));
Diandrous answered 30/9, 2019 at 13:20 Comment(0)
O
1

There's also another way how to avoid this problem. Spectator (which is btw really awesome tool for Angular unit testing) has handy shallow flag, which basically prevents all child components (including mat-icon) to be compiled but handled as simple HTML tag instead.

const createComponent = createComponentFactory({
  component: MyComponent,
  imports: [
    ...
  ],
  mocks: [
    ...
  ],
  providers: [
    ...
  ],
  shallow: true // <-- ignore child components
});

With this flag enabled, you don't need to import MatIconModule anymore, also you reduce the test runtime, because Angular doesn't compile extra components while running the test. This allows you to really focus on the component itself while writing unit tests, without worrying about the stuff you don't need anyway.

It's a win-win if you ask me :)

Olshausen answered 29/1, 2020 at 12:16 Comment(1)
While I do appreciate the use of spectator, it just sets NO_ERRORS_SCHEMA under the hood, so you can achieve the same thing without it. SourceMachos
B
0

The only solution that did it for me was mocking MatIcon with the ng-mock library, as suggested in this thread.

That's what my test setup looks like now (I use testing library):

import { MatIcon } from '@angular/material/icon'
import { MockComponent } from 'ng-mocks'

// ... 

await render(MyComponent, {
  imports: [], // You should not import MatIconModule here
  declarations: [MockComponent(MatIcon)],
})
Brey answered 24/4, 2023 at 18:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.