angular - 使用 RendererFactory2 和没有 TestBed 的私有 Renderer2 对 Angular 服务进行单元测试
问题描述
我创建了一个服务
- 开放模态
- 模糊背景(只要至少有一个打开的模态)
服务看起来像这样:
@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');
}
}
由于 Angular 禁止将 Renderer2 作为依赖项直接注入到服务中,因此我必须注入 RendererFactory2 并createRenderer
在构造函数中调用。
现在我想对服务进行单元测试并检查是否_modalCount
正确递增,然后将其传递给每个打开的模式作为depth
输入来管理它们的 CSS z-index
。
我的单元测试看起来像这样(我注释掉了一些expect
s 以查明问题:
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);
});
});
如您所见,我没有使用 Angulars TestBed
,因为它添加了更多隐藏功能。运行测试抛出
Unhandled promise rejection: TypeError: Cannot read property 'addClass' of undefined
显然,_renderer
即使createRenderer
已被调用,也没有定义。我该如何解决这个问题?
解决方案
我认为您的订单不正确,请在创建之前尝试存根new ModalService
// do this first before creating a new ModalService
rendererSpyFactory.createRenderer.and.returnValue(rendererStub);
modalService = new ModalService(documentSpy, injectorSpy, rendererSpyFactory);
推荐阅读
- javascript - FullCalendar v4 动态设置日历的高度
- pandas - 如何将仅包含数字的数据框中的列拆分为熊猫中的多列
- python - Groupby 用于选择多列 Pandas python
- swift - iOS 13 模拟器:reverseGeocodeLocation - [GEOAddressObject] [NSLocale currentLocale] 对 NSLocaleCountryCode@ 失败
- c++ - ::clock 尚未声明
- laravel - 如何获取在线用户(实时)
- unit-testing - Delta Control 中的 KDB+ Q 单元测试分析代码
- node.js - 当浏览器重新加载时,所有对话都被清除了。我需要在 bot 框架节点 js 中重新开始旧对话
- reactjs - 如何重置反应钩子状态?
- c# - 如何在 selenium c# 中运行多个 chrome 配置文件?