I created a service to
The Servie looks like this:
@Injectable({
providedIn: 'root',
})
export class ModalService {
private readonly _renderer: Renderer2;
// Overlaying hierarchy
private _modalCount = 0;
// Functional references
private _viewContainerRef: ViewContainerRef;
constructor(
@Inject(DOCUMENT) private readonly _document: Document,
private readonly _injector: Injector,
private readonly _rendererFactory: RendererFactory2,
) {
this._renderer = this._rendererFactory.createRenderer(null, null);
}
/**
* Set the point where to insert the Modal in the DOM. This will generally be the
* AppComponent.
* @param viewContainerRef
*/
public setViewContainerRef(viewContainerRef: ViewContainerRef): void {
this._viewContainerRef = viewContainerRef;
}
/**
* Adds a modal to the DOM (opens it) and returns a promise. the promise is resolved when the modal
* is removed from the DOM (is closed)
* @param componentFactory the modal component factory
* @param data the data that should be passed to the modal
*/
public open<S, T>(componentFactory: ComponentFactory<AbstractModalComponent<S, T>>, data?: S): Promise<T> {
return new Promise(resolve => {
const component = this._viewContainerRef.createComponent(componentFactory, null, this._injector);
if (data) {
component.instance.data = data;
}
const body = this._document.body as HTMLBodyElement;
// we are adding a modal so we uppen the count by 1
this._modalCount++;
this._lockPage(body);
component.instance.depth = this._modalCount;
component.instance.close = (result: T) => {
component.destroy();
// we are removing a modal so we lessen the count by 1
this._modalCount--;
this._unlockPage(body);
resolve(result);
};
component.changeDetectorRef.detectChanges();
});
}
/**
* Prevents the page behind modals from scrolling and blurs it.
*/
private _lockPage(body: HTMLBodyElement): void {
this._renderer.addClass(body, 'page-lock');
}
/**
* Removes the `page-lock` class from the `body`
* to enable the page behind modals to scroll again and be clearly visible.
*/
private _unlockPage(body: HTMLBodyElement): void {
// If at least one modal is still open the page needs to remain locked
if (this._modalCount > 0) {
return;
}
this._renderer.removeClass(body, 'page-lock');
}
}
Since Angular forbids to directly inject the Renderer2 as a dependency into services, I have to inject the RendererFactory2 and call createRenderer
in the constructor.
Now I want to unit test the service and check whether the _modalCount
is correctly incremented and then passed to every opened modal as depth
input to manage their CSS z-index
.
My unit test looks like this (I commented out some expect
s to pin-point the problem:
describe('ModalService', () => {
let modalService: ModalService;
let injectorSpy;
let rendererSpyFactory;
let documentSpy;
let viewContainerRefSpy;
@Component({ selector: 'ww-modal', template: '' })
class ModalStubComponent {
instance = {
data: {},
depth: 1,
close: _ => {
return;
},
};
changeDetectorRef = {
detectChanges: () => {},
};
destroy = () => {};
}
const rendererStub = ({
addClass: jasmine.createSpy('addClass'),
removeClass: jasmine.createSpy('removeClass'),
} as unknown) as Renderer2;
beforeEach(() => {
rendererSpyFactory = jasmine.createSpyObj('RendererFactory2', ['createRenderer']);
viewContainerRefSpy = jasmine.createSpyObj('ViewContainerRef', ['createComponent']);
documentSpy = {
body: ({} as unknown) as HTMLBodyElement,
querySelectorAll: () => [],
};
modalService = new ModalService(documentSpy, injectorSpy, rendererSpyFactory);
rendererSpyFactory.createRenderer.and.returnValue(rendererStub);
modalService.setViewContainerRef(viewContainerRefSpy);
});
it('#open should create a modal component each time and keep track of locking the page in the background', done => {
function openModal(depth: number) {
const stub = new ModalStubComponent();
viewContainerRefSpy.createComponent.and.returnValue(stub);
const currentModal = modalService.open(null, null);
expect(viewContainerRefSpy.createComponent).toHaveBeenCalledWith(null, null, injectorSpy);
expect(rendererSpyFactory.createRenderer).toHaveBeenCalledWith(null, null);
// expect(stub.instance.depth).toBe(depth);
// expect(rendererStub.addClass).toHaveBeenCalledWith(documentSpy.body, pageLockClass);
return { currentModal, stub };
}
openModal(1); // Open first modal; `modalCount` has `0` as default, so first modal always gets counted as `1`
const { currentModal, stub } = openModal(2); // Open second modal; `modalCount` should count up to `2`
currentModal.then(_ => {
// expect(rendererStub.removeClass).toHaveBeenCalledWith(documentSpy.body, pageLockClass);
openModal(2); // If we close a modal, `modalCount` decreases by `1` and opening a new modal after that should return `2` again
done();
});
stub.instance.close(null);
});
});
As you see, I am not using Angulars TestBed
as it adds more hidden functionalities. Running the test throws
Unhandled promise rejection: TypeError: Cannot read property 'addClass' of undefined
So apparently, _renderer
is not defined even though createRenderer
has been called. How can I solve this?
I think your order is not correct, try stubbing before creating a new ModalService
// do this first before creating a new ModalService
rendererSpyFactory.createRenderer.and.returnValue(rendererStub);
modalService = new ModalService(documentSpy, injectorSpy, rendererSpyFactory);
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.