简体   繁体   中英

Unit-Testing Angular service with RendererFactory2 and private Renderer2 without TestBed

I created a service to

  1. open modals
  2. blur the background (as long as there is at least one open modal)

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM