Reputation: 7566
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
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
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
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 document
into a service. There it can be tested easily without calling TestBed.createComponent()
. In your component you can then mock the service.
Upvotes: 6
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