7

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?

Phil
  • 7,065
  • 8
  • 49
  • 91

4 Answers4

7

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();
  });

});
JoeO
  • 79
  • 1
  • 3
6

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.

Simon Bräuer
  • 86
  • 1
  • 4
  • I worked around it by mocking `querySelectorAll` in my `mockDocument`. `const mockDocument = { location: new Location(), querySelectorAll: jest.fn };` `TestBed.configureTestingModule({ providers: [{ provide: DOCUMENT, useValue: mockDocument }] });` – ArcadeRenegade Dec 07 '21 at 20:25
  • @ArcadeRenegade I tried this way but its not working – Vimal Patel Jan 20 '22 at 10:52
  • I am getting below error: TypeError: Cannot read properties of undefined (reading 'length') at at DOMTestComponentRenderer.insertRootElement (https://jasmine-in-angular-beomut.stackblitz.io/turbo_modules/@angular/platform-browser-dynamic@8.1.0/bundles/platform-browser-dynamic-testing.umd.js:78:42) at TestBedViewEngine.createComponent (https://jasmine-in-angular-beomut.stackblitz.io/turbo_modules/@angular/core@8.1.0/bundles/core-testing.umd.js:2630:35) at Function.TestBedViewEngine.createComponen – Vimal Patel Jan 20 '22 at 11:02
  • Looks like this behavior is indented. Please see these github issues: https://github.com/angular/angular/issues/45031 and https://github.com/angular/angular/issues/45278 – Boris Makhlin Sep 28 '22 at 09:29
3

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
DJ House
  • 1,307
  • 1
  • 11
  • 14
  • 3
    Are you sure this should work? My app is a bit complex, but the part about changing window to (the injected) document is what makes tests pass or fail (with TypeError: el.querySelectorAll is not a function). Your solution is the one I have used originally and this is the error I was getting. – Phil May 20 '19 at 21:00
  • Could you post a minimum stackblitz? I haven't used DOCUMENT in a while so I may be a touch rusty. – DJ House May 21 '19 at 15:16
  • @HumbertoMorera could you elaborate a little bit more? If you need to share snippets of your code, you could open a new question and link to it. I'll take a look when I have time. – DJ House Jul 21 '20 at 18:22
3

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

Oru
  • 61
  • 7