Phil
Phil

Reputation: 7566

Mocking DOCUMENT in Angular/Karma

How do you mock DOCUMENT (the shadow representation of an HTMLDocument) in Angular? The implementation is using this in the constructor:

@Inject(DOCUMENT) private document: Document

After looking at this How to inject Document in Angular 2 service I have put this in my .spec setup:

const lazyPath = 'dummy';
const pathname = `/${lazyPath}`;
const document = { location: { pathname } as Location } as Document;
beforeEachProviders(() => ([ {provide: DOCUMENT, useValue: document} ]));

But it's giving me errors:

ERROR in ./src/app/main/components/app-lazy/app-lazy.component.spec.ts
Module not found: Error: Can't resolve '@angular/core/testing/src/testing_internal' in '...'
resolve '@angular/core/testing/src/testing_internal' in '....'
  Parsed request is a module
  using description file: .../package.json (relative path: ...)
    Field 'browser' doesn't contain a valid alias configuration
    resolve as module

When I use a simple providers: [] in TestBed.configureTestingModule instead of beforeEachProviders from the testing_internal package, the component is undefined, eg not initialized properly. It only initializes in unit tests (in the non-test execution both works) when I switch from an injected document, to the window object (on which I cannot set/mock location). What can I do?

Upvotes: 8

Views: 20731

Answers (4)

JoeO
JoeO

Reputation: 99

You should avoid mocking the entire document object and mock/spy individual methods/properties on it instead.

Assuming you have the following in your component/service:

import { DOCUMENT } from '@angular/common';
...
constructor(@Inject(DOCUMENT) private document: Document) {}

You can test against the document object by injecting it inside your beforeEach

describe('SomeComponent', () => {
  let component: SomeComponent;
  let doc: Document;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [SomeComponent],
      imports: [
        RouterTestingModule,
        HttpClientTestingModule
      ]
    });
    const fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
    doc = TestBed.inject(DOCUMENT); // Inject here **************
  });


  it('set document title', () => {
    component.setPageTitle('foobar'); // Assuming this component method is `this.document.title = title`
    expect(doc.title).toBe('foobar');
  });

  it('calls querySelectorAll', () => {
    const spy = spyOn(doc, 'querySelectorAll');
    component.someMethodThatQueries();
    expect(spy).toHaveBeenCalled();
  });

});

Upvotes: 8

Oru
Oru

Reputation: 61

If you provide alias DOCUMENT in your app.module.ts as follows:

import { DOCUMENT } from "@angular/common";

...

providers: [
    { provide: Document, useExisting: DOCUMENT }
]

You can inject it casually like this:

export class Component {
    constructor (private document: Document) {
       document.getElementById("button")
    }
}

And you can even mock it easily:

class MockDocument {}

describe('MostAwesomeComponent', () => {
    let component: Component;
    let fixture: ComponentFixture<Component>;
  
    beforeEach(() => {
        TestBed.configureTestingModule({
            declarations: [ Component ],
            providers: [{ provide: Document, useClass: MockDocument}]
        })
        .compileComponents();
    });

    beforeEach(() => {
        fixture = TestBed.createComponent(AppComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
    });

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

Thanks to Ernestas Buta - source

Upvotes: 3

Simon Br&#228;uer
Simon Br&#228;uer

Reputation: 86

I experience most likely a similar issue as @Phil. It appears that the problem is related to injecting DOCUMENT into a component.

When you mock the injected DOCUMENT, then the call on TestBed.createComponent() throws an error when internally calling document.querySelectorAll().

TestBed.createComponent() appears to be accessing the injected mocked document object. Not sure if this is a bug or intended.

I experience the issue with Angular 11 recently. Because I was too lazy to set up a new stackblitz, I reproduced it on an existing stackblitz based on Angular 8. But issue is the same there.

https://stackblitz.com/edit/jasmine-in-angular-beomut?file=src%2Fapp%2Fapp.component.spec.ts

My current solution/workaround for this issue is:

Move the logic related to documentinto a service. There it can be tested easily without calling TestBed.createComponent(). In your component you can then mock the service.

Upvotes: 6

DJ House
DJ House

Reputation: 1397

Posting this as an answer because the formatting doesn't work in a comment.

Could you share a stackblitz if possible? When I need to inject a mock, I usually set it up like:

  // ... beginning of file

  const mockDocument = { location: { pathname } };

  beforeEach(() => TestBed.configureTestingModule({
    imports: [...],
    // Provide DOCUMENT Mock 
    providers: [
      { provide: DOCUMENT, useValue: mockDocument }
    ]
  }));

  // ...rest of file

Upvotes: 3

Related Questions