11

I am having trouble testing a component with OnPush change detection strategy.

The test goes like this

it('should show edit button for featured only for owners', () => {
    let selector = '.edit-button';

    component.isOwner = false;
    fixture.detectChanges();

    expect(fixture.debugElement.query(By.css(selector))).toBeFalsy();

    component.isOwner = true;
    fixture.detectChanges();

    expect(fixture.debugElement.query(By.css(selector))).toBeTruthy();
});

If I use Default strategy it works as expected, but with OnPush the change to isOwner is not rerendered by the call to detectChanges. Am I missing something?

altschuler
  • 3,534
  • 2
  • 25
  • 52
  • 1
    Take a look at this thread https://github.com/angular/angular/issues/12313 and here is your test https://plnkr.co/edit/llJG17sBZZUXXBertYPp?p=preview – yurzui Nov 30 '16 at 20:02

6 Answers6

9

This problem can be easily solved... https://github.com/angular/angular/issues/12313#issuecomment-298697327

TestBed.configureTestingModule({
  declarations: [ MyComponent ] 
})
.overrideComponent(MyComponent, {
  set: {  changeDetection: ChangeDetectionStrategy.Default  }
})
.compileComponents();

keep in mind this approach may cloak some change detection issues

credits: marchitos

  • Note that `overrideComponent` is broken (at least with Ivy?) if you use a bundler for your tests -- it forces the component to be recompiled by the JIT compiler, which it can't do because it doesn't know where the template/style files are. – Coderer Oct 16 '20 at 09:39
5

You need to tell angular that you changed input property of the component. In an ideal world, you would replace

component.isOwner = false;
fixture.detectChanges();

with

component.isOwner = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();

Unfortunately, that doesn't work since there is a bug in angular (https://github.com/angular/angular/issues/12313). You can use one of the workarounds described there.

Marcel Šebek
  • 281
  • 4
  • 7
4

If you check out this great @Günter's answer angular 2 change detection and ChangeDetectionStrategy.OnPush then you can work around it by using event handler like:

const fixture = TestBed.overrideComponent(TestComponent, {set: {host: { "(click)": "dummy" }}}).createComponent(TestComponent);
// write your test
fixture.debugElement.triggerEventHandler('click', null);
fixture.detectChanges();

Here's Plunker Example

Community
  • 1
  • 1
yurzui
  • 190,482
  • 29
  • 403
  • 383
  • I went with the solution from your comment. While this might be "cleaner" it is more cumbersome, so I'd rather wrap the hack in a helper and hope for a better solution in the future :) Thanks! – altschuler Dec 01 '16 at 00:09
2

Similar to the work arounds that @michaelbromley did to expose the ChangeDetectionRef but since this is only for tests I just turned off TypeScript errors for the next line using // @ts-ignore flag from v2.6 so I could leave the ref private.

An example of how this might work:

import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { WidgetComponent } from './widget.component';

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<my-widget *ngIf="widgetEnabled"></my-widget>`,
});
export class PushyComponent {
  @Input() widgetEnabled = true;

  constructor(private cdr: ChangeDetectorRef) {}

  // methods that actually use this.cdr here...
}

TestBed.configureTestingModule({
  declarations: [ PushyComponent, WidgetComponent ],
}).compileComponents();

const fixture = TestBed.createComponent(PushyComponent);
const component = fixture.componentInstance;
fixture.detectChanges();

expect(component.widgetEnabled).toBe(true);
let el = fixture.debugElement.query(By.directive(WidgetComponent));
expect(el).toBeTruthy();

component.widgetEnabled = false;
// @ts-ignore: for testing we need to access the private cdr to detect changes
component.cdr.detectChanges();
el = fixture.debugElement.query(By.directive(WidgetComponent));
expect(el).toBeFalsy();
Rob
  • 3,558
  • 2
  • 31
  • 39
1

There are a few solutions, but in your case, I think the easiest way is split your test into two separate tests. If in each of these tests you call fixture.detectChanges() function only once, everything should works fine.

Example:

it('should hide edit button if not owner', () => {
    let selector = '.edit-button';

    component.isOwner = false;
    fixture.detectChanges();

    expect(fixture.debugElement.query(By.css(selector))).toBeFalsy();
});

it('should show edit button for owner', () => {
    let selector = '.edit-button';

    component.isOwner = true;
    fixture.detectChanges();

    expect(fixture.debugElement.query(By.css(selector))).toBeTruthy();
});
Cichy
  • 3,402
  • 3
  • 21
  • 26
1

It doesn't work because the changeDetectorRef in your fixture isn't the same as in your component. Taken from the issue in Angular:

"...changeDetectorRef on a ComponentRef points to the change detector of the root (host) view of a dynamically created component. Then, inside the host view we've got the actual component view, but the component view is OnPush thus we never refresh it!" - source

Option A. One way to solve this is to use the components injector to get the real changeDetectionRef:

describe('MyComponent', () => {
  let fixture;
  let component;

  beforeEach(() => {
    TestBed.configureTestingModule({ ... }).compileComponents();
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('does something', () => {
    // set the property here
    component.property = 'something';

    // do a change detection on the real changeDetectionRef
    fixture.componentRef.injector.get(ChangeDetectorRef).detectChanges();

    expect(...).toBe(...);
  });
});

You could also just use the initial binding to an @Input (which initially triggers changedetection for an OnPush strategy):

Option B1:

describe('MyComponent', () => {
  let fixture;
  let component;

  beforeEach(() => {
    TestBed.configureTestingModule({ ... }).compileComponents();
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
  });

  it('does something', () => {
    // set the property here
    component.property = 'something';

    // do the first (and only) change detection here
    fixture.detectChanges();

    expect(...).toBe(...);
  });
});

or for example:

Option B2:

describe('MyComponent', () => {
  let fixture;
  let component;

  it('does something', () => {
    // set the property here
    setup({ property: 'something' });
    expect(...).toBe(...);
  });

  function setup(props: { property? } = {}) {
    TestBed.configureTestingModule({ ... }).compileComponents();
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;

    Object.getOwnPropertyNames(props).forEach((propertyName) => {
      component[propertyName] = props[propertyName];
    });

    // do the first (and only) change detection here
    fixture.detectChanges();
  }
});
Remi
  • 3,735
  • 7
  • 33
  • 66