19

In one of our component, we use Renderer2 to add and remove some css classes/styles to the body element. To achieve that, we simply do something like:

this.renderer.setStyle(document.body, 'padding-left', '10px');
this.renderer.addClass(document.body, 'modal-container--opened');

As soon as we run the tests, we encounter errors:

Cannot read property 'add' of undefined

Cannot set property 'padding-left' of undefined

So it seems angular testBed doesn't create any body element.

In our test configuration, how to create a mocked body element? So we can run our tests against that element and see if style/class was properly applied by renderer.

It also seems mocking the Renderer2 is not possible.

We tried to create a spy:

let renderer: jasmine.SpyObj<Renderer2>;
renderer = jasmine.createSpyObj('renderer', ['addClass', 'removeClass', 'setStyle']);

then in TestBed.configureTestingModule (also tested in overrideProviders without more success):

{ provide: Renderer2, useValue: renderer }

But Angular ignores completely this override.

How to be able to test our component behavior, acting on document.body?

Community
  • 1
  • 1
BlackHoleGalaxy
  • 9,160
  • 17
  • 59
  • 103
  • I'm not sure how it's possible. It uses real document.body. What browser do you use? – Estus Flask Feb 16 '18 at 12:31
  • Chrome, Firefox and Edge works properly – BlackHoleGalaxy Feb 16 '18 at 21:46
  • I mean for tests. Is it Phantom? I'm not aware of missing document.body.styles there, but it's possible. Try Chrome launcher if it's so. – Estus Flask Feb 16 '18 at 21:56
  • We use chrome and chrome headless. On both the problem occurs. – BlackHoleGalaxy Feb 17 '18 at 13:18
  • It's unclear what's going on in your case. It [should be working](http://plnkr.co/edit/cERP9zbPXIaIbdsZGE2J?p=preview) . Consider providing a way to replicate the problem. You can start with debugging document.body and document.body.styles and how comes that styles is undefined (this is what property 'padding-left' of undefined is about) – Estus Flask Feb 17 '18 at 15:52

2 Answers2

26

Instead of mocking the renderer try to hijack it. This should be working with Angular 6+.

In your component.spec.ts:

let renderer2: Renderer2;
...
beforeEach(async( () => {
    TestBed.configureTestingModule({
        ...
        providers: [Renderer2]
    }).compileComponents();
}));

beforeEach(() => {
    fixture = TestBed.createComponent(YourComponent);
    // grab the renderer
    renderer2 = fixture.componentRef.injector.get<Renderer2>(Renderer2 as Type<Renderer2>);
    // and spy on it
    spyOn(renderer2, 'addClass').and.callThrough();
    // or replace
    // spyOn(renderer2, 'addClass').and.callFake(..);
    // etc
});

it('should call renderer', () => {
    expect(renderer2.addClass).toHaveBeenCalledWith(jasmine.any(Object), 'css-class');
});
reduckted
  • 2,358
  • 3
  • 28
  • 37
GKA
  • 1,230
  • 1
  • 15
  • 15
  • 1
    Missing something on my end; TypeError: Cannot read property 'remove' of undefined at DefaultDomRenderer2.removeClass (../packages/platform-browser/src/dom/dom_renderer.ts:194:59). – Winnemucca Oct 16 '19 at 20:00
  • 1
    This does not appear to work in Angular 13. Every method I've tried appears to be thwarted by the Renderer2Interceptor. – jake May 12 '22 at 01:47
5

Other approach would be, mocking service:

let renderer: MockRenderer;
class MockRenderer {
   addClass(document: string, cssClass: string): boolean {
     return true;
   }
}

beforeEach(async( () => {
   TestBed.configureTestingModule({
    ...
       providers: [{
              provide: Renderer2,
              useClass: MockRenderer
            }]
    }).compileComponents().then(() => {
        fixture = TestBed.createComponent(YourComponent);
        renderer =  fixture.debugElement.injector.get(Renderer2);
    });
}));

...
it('should call render', () => {
      spyOn(renderer, 'addClass');
      ...
      expect(renderer.addClass).toHaveBeenCalledWith(jasmine.any(Object), 'css-class');
});
Jose Rojas
  • 3,490
  • 3
  • 26
  • 40