Skip to content

TestBed: rethink interactions with the @Input / @output of the root component #12313

Closed

Description

@mattdistefano

I'm submitting a ... (check one with "x")

[x] bug report => search github for a similar issue or PR before submitting
[ ] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

TBH I'm not sure if this is a bug, a feature request, or just a product of my ignorance/incomplete docs.

Current behavior

When I have a component using ChangeDetectionStrategy.OnPush, I can create a test fixture via const fixture = TestBed.createComponent(TestComponent) but can't figure out how to force a change detection after altering one of the input properties. Just calling fixture.detectChanges() like I would for a ChangeDetectionStrategy.Default component doesn't seem to do it. I've also tried in conjunction with fixture.changeDetectorRef.markForCheck() but still no luck. However, if I wrap my OnPush component in another component for testing purposes, it works as expected.

Expected behavior

Should be some way to force a change detection on a OnPush component when changing its inputs directly via the component fixture (rather than via a wrapper component).

Minimal reproduction of the problem with instructions

test.component:

import { Component, Input, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'my-test',
  template: '<p>{{ content }}</p>',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TestComponent {
  @Input() content: string;
}

test.component.spec.ts (this test fails w/ OnPush but passes with Default):

import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TestComponent } from './test.component';

describe('Test Component', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({declarations: [TestComponent]});
  });
  it('should render the bound content within a p element', () => {
    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    const p = fixture.debugElement.query(By.css('p'));
    expect((p.nativeElement as HTMLParagraphElement).innerText).toBeFalsy();
    fixture.componentInstance.content = 'Test!';
    fixture.detectChanges();
    expect((p.nativeElement as HTMLParagraphElement).innerText).toEqual('Test!');
  });
});

test.component.wrapped.spec.ts (this passes with OnPush or Default):

import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TestComponent } from './test.component';

@Component({
  selector: 'my-wrapper',
  template: '<my-test [content]="content"></my-test>'
})
class WrapperComponent {
  content: string;
}
describe('Test Component (Wrapped)', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({declarations: [TestComponent, WrapperComponent]});
  });

  it('should render the bound content within a p element', () => {
    const fixture = TestBed.createComponent(WrapperComponent);
    fixture.detectChanges();
    const p = fixture.debugElement.query(By.css('p'));
    expect((p.nativeElement as HTMLParagraphElement).innerText).toBeFalsy();
    fixture.componentInstance.content = 'Test!';
    fixture.detectChanges();
    expect((p.nativeElement as HTMLParagraphElement).innerText).toEqual('Test!');
  });
});

What is the motivation / use case for changing the behavior?

Would prefer to use roughly the same testing patterns regardless of which change detection strategy I'm using.

Please tell us about your environment:

  • Angular version: 2.1.0
  • Browser: all
  • Language: TS 2.0.3

Activity

  1. added
    area: testingIssues related to Angular testing features, such as TestBed
    on Oct 15, 2016
  2. mattdistefano commented on Oct 20, 2016

    @mattdistefano
    Author

    Thanks! I'll take a look at that and let you know. Also glad to hear that this is supposed to work as-is.

  3. mattdistefano commented on Oct 20, 2016

    @mattdistefano
    Author

    Well, setting a default value didn't seem to make a difference, but that suggestion and your example above did help me narrow down the issue a bit. It seems like the first detectChanges() works, but not subsequent calls.

    import { TestBed } from '@angular/core/testing';
    import { By } from '@angular/platform-browser';
    import { TestComponent } from './test.component';
    
    describe('Test Component', () => {
      beforeEach(() => {
        TestBed.configureTestingModule({ declarations: [TestComponent] });
      });
      it('should render the bound content within a p element', () => {
        const fixture = TestBed.createComponent(TestComponent);
        fixture.componentInstance.content = 'Foo';
        fixture.detectChanges();
    
        const p = fixture.debugElement.query(By.css('p'));
    
        // this will pass
        expect((p.nativeElement as HTMLParagraphElement).innerText).toEqual('Foo');
    
        // now change value and rerun change detection
        fixture.componentInstance.content = 'Bar';
        fixture.detectChanges();
        // this will fail - it's still 'Foo'
        expect((p.nativeElement as HTMLParagraphElement).innerText).toEqual('Bar');
      });
    });
  4. mattdistefano commented on Oct 20, 2016

    @mattdistefano
    Author

    Still no luck. The initial value flows through to the DOM but not updates. IIRC the OnPush change detection for a given component more or less runs within the parent component and evaluates its bindings rather than the child component's @Inputs. So if that's correct, it would sort of make sense that the initial detectChanges() would work to initialize the component, but, because there's no bindings within the parent (the fixture in this case), subsequent calls don't do anything.

    Do you guys have any test examples where you're updating an @Input after the component is initialized?

  5. self-assigned this
    on Nov 22, 2016
  6. alexzuza commented on Nov 30, 2016

    @alexzuza
    Contributor

    I think that is because you can trigger updating OnPush component only by changing @Input property from parent component.

    1. As workaround you can try the following hack

    fixture.changeDetectorRef['internalView']['compView_0'].markAsCheckOnce();

    Plunker

    1. Seems it looks very hackly so another way to run manually change detection might be as follows:
    const fixture = TestBed.overrideComponent(TestComponent, {set: {host: { "(click)": "dummy" }}}).createComponent(TestComponent);
    ....
    fixture.debugElement.triggerEventHandler('click', null);
    fixture.detectChanges();
    

    Plunker Example

    All magic happens here

    /TestComponent/host.ngfactory.js

    View_TestComponent_Host0.prototype.handleEvent_0 = function(eventName,$event) {
      var self = this;
      self.debug(0,0,0);
      self.compView_0.markPathToRootAsCheckOnce(); <=== this line
      var result = true;
      result = (self._TestComponent_0_3.handleEvent(eventName,$event) && result);
      return result;
    };
    
    1. One more option is to have ChangeDetectorRef injected in your test component
    export class TestComponent {
      constructor(public cdRef: ChangeDetectorRef) {}
    }
    

    then just use component changeDetector

    fixture.componentInstance.cdRef.markForCheck();

    Plunker Example

  7. jiverson commented on Dec 1, 2016

    @jiverson

    Ran across this today spent a few hours scratching my head. At least an example in the Test docs because this can really run you for a loop.

  8. dabalyan commented on Jan 7, 2017

    @dabalyan

    hey @juliemr is there any update on this ?
    Is it something with how test is written, or is this for real ?
    @alexzuza 's suggestion works btw.

  9. EddyP23 commented on Jan 18, 2017

    @EddyP23
    Contributor

    Found same issue. Very frustating! Thanks for the discussion above.

    The weirdest thing I found about this is that fixture.changeDetectorRef.markForCheck() doesn't seem to be doing anything in tests

    Any ideas when this might get fixed?

  10. sebek64 commented on Feb 1, 2017

    @sebek64

    I've found the same. I'm not proficient in angular internals. In my opinion, the problem is that ComponentFixture doesn't provide access to the component itself, just to its wrapper (except fixture.componentInstance, which is a reference to component class). All other properties of fixture are that of wrapper component - changeDetectorRef, injector, componentRef, debugElement, elementRef, nativeElement). In particular, changeDetectorRef, injector, and componentRef are most problematic.

    As a temporary solution, I do

    compRef = fixture.componentRef.hostView['internalView']['compView_0'];

    in beforeEach. Then, instead of doing (quite natural) fixture.changeDetectorRef.markForCheck(), I do compRef.changeDetectorRef.markForCheck();.

    In my opinion, ComponentFixture should include references to the component itself. If that is not possible for some reason, then there should be these references in addition to those to parent component.

  11. 88 remaining items

  12. angular-automatic-lock-bot commented on Aug 1, 2022

    @angular-automatic-lock-bot

    This issue has been automatically locked due to inactivity.
    Please file a new issue if you are encountering a similar or related problem.

    Read more about our automatic conversation locking policy.

    This action has been performed automatically by a bot.

  13. locked and limited conversation to collaborators on Aug 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions