TestBed: rethink interactions with the @Input / @output of the root component
mattdistefano opened this issue · comments
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
Every single one of my components is using OnPush
and I haven't noticed this behavior using Angular 2.1.0 at all?? Looking over your provided example nothing jumps out at me though :/
But I do have a theory based on something odd that I noticed. Are you certain that fixture.componentInstance.content
is actually there? Check to see if it's visible on fixture.componentInstance
in the beforeEach
block. I noticed an issue where any @Input
I had declared inside a component class
implementation that WAS NOT initialized to some value wasn't showing up inside my test.
So for example if I had an Input like so:
@Input() paymentMethods : List<PaymentMethodState>;
It would not show up on my fixture.componentInstance
. However, if I had:
@Input() paymentMethods : List<PaymentMethodState> = List();
Then it would be there. Wasted quite a bit of time on that one :/ I noticed that you're not initializing @Input() content: string;
to anything so perhaps you're running into the same oddity I was experiencing?? It would explain why it works in a wrapper component though since it's being set to something by the wrapper.
If you can verify the same behavior I'd say we have a reasonable defect here. I hate to have to go back and change all my component @Input
declarations now >.<
UPDATE:
Oddly enough, whatever it was that was causing the condition I described above, sigh...seems to have gone away. The only change I made on my end was working with fixture.nativeElement
instead of fixture.debugElement
??? No clue
Thanks! I'll take a look at that and let you know. Also glad to hear that this is supposed to work as-is.
@mattdistefano here's an example unit test that seems to behave with Angular 2.1.0:
import {
ComponentFixture,
TestBed
} from '@angular/core/testing';
import {Http} from '@angular/http';
import {List} from 'immutable';
import {
TranslateModule,
TranslateLoader,
TranslateStaticLoader
} from 'ng2-translate/ng2-translate';
import {PaymentStepsMethodComponent} from './payment-steps-method.component';
import {SvgIconComponent} from '../../../../shared/components/SvgIcon/svg-icon.component';
import {
EnumPaymentMethodType,
PaymentMethodState
} from '../../../../store/Payments/';
// suite of related tests
describe('Payment Steps Method Component', () => {
let fixture : ComponentFixture<PaymentStepsMethodComponent>,
comp : PaymentStepsMethodComponent,
element : any,
paymentsMethods : List<PaymentMethodState>;
// setup tasks to perform before each test
beforeEach(() => {
// refine the initial testing module configuration
TestBed.configureTestingModule({
imports : [
TranslateModule.forRoot({
provide : TranslateLoader,
useFactory : (http : Http) => new TranslateStaticLoader(http, 'i18n', '.json'),
deps : [Http]
})
],
declarations : [
SvgIconComponent,
PaymentStepsMethodComponent
]
});
// create component fixture
fixture = TestBed.createComponent(PaymentStepsMethodComponent);
// grab instance of component class
comp = fixture.componentInstance;
// grab DOM representation
element = fixture.nativeElement;
// wire up @Input bindings
paymentsMethods = List([
{
type : EnumPaymentMethodType.BANK_DRAFT,
label : 'Bank Account',
code : 0,
selected : false
},
{
type : EnumPaymentMethodType.CHECK,
label : 'Send a Check',
code : 1,
selected : false
}
].map(value => new PaymentMethodState(value)));
// bind to input
comp.paymentMethods = paymentsMethods;
// trigger initial bindings
fixture.detectChanges();
});
// test definitions
it('should display payment methods', () => {
// inspect content of *ngFor for child nodes
expect(element.getElementsByClassName('Method__Card').length).toEqual(2);
});
it('should raise paymentMethodSelected event when payment method is chosen', () => {
let selectedPaymentMethod : PaymentMethodState = undefined;
// subscribe to paymentMethodSelected event
comp.paymentMethodSelected.subscribe((method : PaymentMethodState) => {
// store selected payment method
selectedPaymentMethod = method;
});
// trigger click event on first payment method
element.getElementsByClassName('Method__Card')[0].click();
// verify selection
expect(selectedPaymentMethod).toBe(comp.paymentMethods.get(0));
});
});
Just a component that displays a couple buttons...user selects payment method by clicking either of the buttons, etc. Component has one @Input
paymentMethods and one @Output
paymentMethodSelected
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');
});
});
try moving it inside the beforeEach
block instead of calling it inside the first it
block. I know that first call to fixture.detectChanges
will init the component, call ngOnInit
, etc. maybe just some odd timing issue??
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 @Input
s. 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?
Let me try to reproduce what you're seeing in that test example I posted above #strengthinnumbers
Okay I'm definitely seeing the same behavior on my end. The initial binding to the @Input
is picked up by the fixture.detectChanges
call but not subsequent ones. Changing component's changeDetection
to ChangeDetectionStrategy.Default
works, just not when it's OnPush
I think that is because you can trigger updating OnPush component only by changing @Input
property from parent component.
- As workaround you can try the following hack
fixture.changeDetectorRef['internalView']['compView_0'].markAsCheckOnce();
- 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();
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;
};
- 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();
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.
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?
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.
Can somebody fix it? Pretty please...
The provided workarounds don't work in angular 4-rc1, since internalView no longer contains compView_0. It would be nice to get this fixed in 4 final. Otherwise, there would be no way to test OnPush components.
@sebek64 Anyway here is 4-rc1 version :) https://plnkr.co/edit/veBSvE8dw9ucyQneI1Wf?p=preview
fixture.changeDetectorRef['_view'].nodes[0].componentView.state |= (1 << 1);
What indeed is a weird behaviour, I can also reproduce it. But why is that?
Any progress on this ?
Ran into the same problem.
@alexzuza's fix worked out quite well.
I've spent that last 12 hours trying to figure out wtf was going on with my unit tests until I found this >_<
It was also broken for me after upgrade to ng4. @alexzuza soluction solved all failing tests :) Thanks
I don't know if it is an acceptable work around but I use to override the component to reset the changeDetectionStrategy to Default in the component initialization like this:
TestBed.configureTestingModule({
declarations: [ MyComponent ]
})
.overrideComponent(MyComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
})
.compileComponents();
Overriding the change detection described in #12313 (comment) didn't work for me in 4.1.1.
However
fixture.changeDetectorRef['_view'].nodes[0].componentView.state |= (1 << 3);
does.
Having issues with this as well. The way I got around it was to do this:
describe('LoadingIndicatorComponent', () => {
let component: LoadingIndicatorComponent;
let fixture: ComponentFixture<LoadingIndicatorComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CommonModule],
schemas: [NO_ERRORS_SCHEMA],
declarations: [LoadingIndicatorComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoadingIndicatorComponent);
component = fixture.componentInstance;
});
it('should be created', () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});
it('should show spinner when loading is true', () => {
component.loading = true;
fixture.detectChanges();
const result = fixture.debugElement.query(By.css('md-spinner'));
expect(result).toBeTruthy();
});
it('should hide spinner when loading is false', () => {
component.loading = false;
fixture.detectChanges();
const result = fixture.debugElement.query(By.css('md-spinner'));
expect(result).toBeFalsy();
});
});
by not calling detectChanges in the beforeEach
block my tests began to pass. I do feel like this is not good behavior for tests with @Input()
's or it needs to be better documented. What really threw me for a loop was that the angular cli generates components with the detect changes in the beforeEach() when you specify that you want OnPush
change detection.
@kylecannon The real problem is that there is a defect in Angular and it run only once in testing! so in each of your test u put it in the begining and it run the on push once now per each test, that the reason that it work for you.
I found that the simplest and cleanest solution was to use a fake host component to contain the dumb one: https://angular.io/guide/testing#test-a-component-inside-a-test-host-component
This is still happening in the latest cli & angular. Is it really not going to be addressed?
I lost too much time figuring that out... Let's hope it can be solved soon :)
Apparently it's intended behaviour, you need to use a test component in which you test the OnPush component in.
Please let Testbed have a function to handle the change detection of "onPush" components in tests without any workarounds.
The angular docs mention using ComponentFixture.changeDetectorRef
as
most valuable when testing a component that has the
ChangeDetectionStrategy.OnPush
method
however, I did not find it success with it. I created a test component taking a queue from what is recommended here: #14087 (comment).
@juliemr since this is assigned to you, and has been open for almost a year, would you mind offering what the recommended workaround path would be while this is addressed or if this is simply "working as expected"?
I had a bit more hair on my head yesterday. I read & reread & rereread the testing docs, went through the apis, tried everything on StackOverflow, frantically went through 10 videos on ng testing on youtube,... was almost in tears & was about to call my mom to pour my heart out about the bad life choices I've made... boy, am i glad to have stumbled across this. Come on Angular... not one thing in the docs about this. In bird culture, this is considered a dik move.
I similarly was running into an issue using TDD where I had a simple toggle button component that would display a "on" or "off" depending on a boolean that was using ChangeDetectionStrategy.OnPush. I wanted to make sure that the component would both display the correct value and pass to any given parent the proper boolean value. Needless to say I ran in to wall before coming across this article and a few others like it.
I was able to eventually find a solution when I noticed that detectChanges seems to reset every time the component emitted its change. That got me thinking that in conjunction with changeDetection I could also dispatch an event that would emit a value. In this case the event basically simulates the user clicking the button but then I would be able to call detect changes to see the updated value in the test. Sure enough this seems to work at least in the simple case in which I was testing.
Here is my toggle component code:
export interface SliderButtonText {
on: string;
off: string;
}
@Component({
selector: 'conn-slider-toggle',
templateUrl: './slider-toggle.component.html',
styleUrls: ['./slider-toggle.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SliderToggleComponent {
@Input() isOn: boolean;
@Input() labelName: string;
@Input() buttonText: SliderButtonText;
@Output() sliderEmitter: EventEmitter<boolean>;
constructor() {
this.sliderEmitter = new EventEmitter<boolean>();
if (!this.buttonText) {
this.buttonText = {
on: "On",
off: "Off"
} as SliderButtonText;
}
}
toggle() {
if (this.isOn === undefined) {
this.isOn = false;
}
this.isOn = !this.isOn;
this.sliderEmitter.emit(this.isOn);
}
}
The test I was able to run to show that this component changes and displays button text correctly is as follows:
describe('SliderToggleComponent', () => {
let component: SliderToggleComponent;
let fixture: ComponentFixture<SliderToggleComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SliderToggleComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SliderToggleComponent);
component = fixture.componentInstance;
component.isOn = false;
fixture.detectChanges();
});
it('should be able to customize the text being used for the button', () => {
const newButtonText = {on: "Subscribed", off: "Unsubscribed"}; // setting default conditions
// Change made but fixture.detectChanges() won't see this unless the emit has
component.buttonText = newButtonText;been dispatched
// Emit gets called when a click event occurs, then detectChanges sees the changes and tests work as expected
fixture.nativeElement.querySelector('button').dispatchEvent(new Event("click"));
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('button').innerText).toBe('Subscribed', "First test failed");
fixture.nativeElement.querySelector('button').dispatchEvent(new Event("click"));
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('button').innerText).toBe('Unsubscribed', "Second test failed");
});
});
Hope this helps anyone who is struggling with using OnPush for their changeDetection.
Using this hack/workaround by @alexzuza and @fjozsef:
fixture.changeDetectorRef['_view'].nodes[0].componentView.state |= (1 << 3);
fixture.detectChanges();
The test started rendering changes to the inputs, but it's still ignoring lifecycle hooks such as ngOnChanges().
Update: Using a light test host component seems to solve all these issues.
People, stop fighting with this and follow the documentation: https://angular.io/guide/testing#test-a-component-inside-a-test-host-component
This will work seamlessly.
Scratched my head 30minutes. Im guilty though, should had read the documentation for testing first 😄
Will create an example of testing a OnPush Component.
The approach would be to create a container test component as mentioned in the docs.
Basically change detection is available only once when you are testing component with
changeDetection: ChangeDetectionStrategy.OnPush
So if you remove the fixture.detectChanges(); from beforeEach block and use them separately in each test it works like a charm.
Manually creating entire new components and configuring all of that just to unit test the inputs and outputs of an OnPush component feels excessive. It might be the "recommended" way, but it certainly doesn't feel like an ideal developer experience.
The approach would be to create a container test component as mentioned in the docs.
I have a case where even this does not seem to work. I have a component (UserDetailComponent
) with no inputs (it is a "router component" i.e. mounted by the router via the RouteConfig) and I use the OnPush change detection strategy. This works fine because I am using observables + async pipe for my template bindings.
In this case even using the container test component approach does not work:
@Component({
selector: 'test-host',
template: `<user-detail></user-detail>`
})
class TestHostComponent {}
Calling testHostComponentFixture.detectChanges()
does not trigger change detection in the UserDetailComponent
So the only way I can find to actually test my component is to inject the ChangeDetectorRef
into UserDetailComponent
and expose it publicly so I can invoke userDetailComponent.changeDetectorRef.detectChanges()
in my test.
This is unsatisfactory since I now have a dependency in my component which is absolutely redundant apart from its use as a hack for unit testing.
Interesting... this issue has almost 1year and a half and we have no rock solid solution. I've feeling that people just don't write that much tests or don't use onPush and immutable data structures, which is sad 😥...
@michaelbromley hmm,
so your use case looks something like following ?:
@Component({..., onPush})
class UserDetailComponent {
userId$ = this.route.paramMap.pipe(map(pmap=>pmap.get('id')))
user$ = this.userId$.pipe(switchMap((userId)=>this.svc.get(userId)))
constructor(private route: ActivatedRoute, private svc: MySvc){}
}
are you providing those observables with new values in the test properly ? It should work even without container, because CD is run with onPush if Input changed ( not your case ) or something async happens within the component, user clicks somewhere or new observable data are pushed. But I may be wrong ofc.
Also have you tried "dirty" solution, overriding CD to Default ?
Interesting... this issue has almost 1year and a half and we have no rock solid solution. I've feeling that people just don't write that much tests or don't use onPush and immutable data structures, which is sad...
I think you are confusing users and the angular team priorities there ;)
@Hotell Yeah that's pretty much my setup, except that my component also has a FormGroup
which gets updated and then I want to check if the "submit" button is enabled:
<button [disabled]="submitIsDisabled()"></button>
In the actual app, the form is of course updated via a user event, which then triggers change detection. On the other hand, my unit test updates the form directly with form.patchValue()
and then I want to manually trigger change detection, which is where I am stuck.
I don't really want to override the cd strategy to default, because then I'm no longer testing the actual component. I think I'll just have to directly invoke submitIsDisabled()
and test the result.
@michaelbromley You can use method 2 from this comment:
All my components are onPush
change detection and your right there are examples where you don't have any @Input()
properties to trigger the CD. Plus the whole Host Element thing seems messy for testing purposes.
Similar to the work around 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.
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();
Thanks @victornoel. I use a test host component as mentioned. Here is my test:
it(
'should show some conditional button',
fakeAsync(() => {
const newConfig = ...;
testHost.cofig = newConfig;
fixture.detectChanges();
tick(100);
fixture.detectChanges();
const someEl = fixture.nativeElement.querySelector('.some-class');
expect(someEl).not.toBeNull();
})
);
My html has this:
*ngIf="isSomeCondition$ | async"
And my isSomeCondition$ has a debounceTime(100). So I had to do this to let the test pass:
fixture.detectChanges();
tick(100);
fixture.detectChanges();
@soupay said:
Basically change detection is available only once when you are testing component with
changeDetection: ChangeDetectionStrategy.OnPush
So if you remove the fixture.detectChanges(); from beforeEach block and use them separately in each test it works like a charm.
@soupay, your suggestion worked perfectly after trying so many. Sorry you got so many thumb downs, but I added one up. ty!
You can use the injector of the component to get the change detector which is reachable from the fixture:
const cd: ChangeDetectorRef = fixture.componentRef.injector.get(ChangeDetectorRef);
But it's not like a binding update, rather like the component changes its state then calls the change detector, so no life-cycle hook will be called. To properly test with inputs currently the official way is to wrap with a test host component.
A quick example how to do this: https://stackblitz.com/edit/angular-testing-template-h9wks4
@nagytom But the lifecycle hook ngOnChanges would not be called, https://stackblitz.com/edit/angular-testing-template-tmynke?file=app/dummy.component.ts
So I think the best way if you are using lifecycle hooks is to use a wrapper component.
Still not resolved on Angular v 7.1. Cute when you find out about the issue after you've written 50 unit tests haha.
@markomisura yep, lost an entire day. And this doesn't work with @Output
s anyway.
I tried on this issue for half an hour following is the reason and two solutions :
Reason : It fails due to a defect in Angular that fixture.detectChanges() works only the first time with ChangeDetectionStrategy.OnPush
Solution 1 : override a component metadata with overrideComponent in TestBed, override OnPush with Default change detection for testing .
TestBed.overrideComponent(TestComponent, {
changeDetection: ChangeDetectionStrategy.Default
});
Solution 2 : Remove fixture.detectChanges from beforeEach block add it in your every spec.
Both the solutions works !
beforeEach(() => {
fixture = TestBed.createComponent(TodoComponent);
component = fixture.componentInstance;
// fixture.detectChanges();
});
it('should......., () => {
fixture.detectChanges();
});
it('should......., () => {
fixture.detectChanges();
});
I have no idea why fixture.detectChanges
won't work in my test case here where I am replacing an object. But I found a way to get the ChangeDetectorRef that will detectChanges.
it('should set wrapper styles', () => {
const compiled: HTMLElement = fixture.debugElement.nativeElement;
const wrapEl: HTMLElement | null = compiled.querySelector('.wrapper');
if (wrapEl) {
fixture.detectChanges();
expect(wrapEl.getAttribute('style')).toBe(`transform: ${component.myStyles.transform};`, 'default style');
component.myStyles = { transform: 'initial' };
// detect changes for the OnPush Component
const debugEl = fixture.debugElement.query(By.directive(WidgetComponent));
// Need to cast as any until https://github.com/angular/angular/issues/23611 resolved
const cdr = debugEl.injector.get<ChangeDetectorRef>(ChangeDetectorRef as any);
cdr.detectChanges();
expect(wrapEl.getAttribute('style')).toBe('transform: initial;', 'manual style');
} else {
fail('div.wrapper element not found');
}
});
Except the linter warning that required me to cast the injector.get parameter as any this seems like an acceptable work around for Components that don't have the CDR in the constructor as in my last comment.
I have no idea why
fixture.detectChanges
won't work in my test case here where I am replacing an object. But I found a way to get the ChangeDetectorRef that will detectChanges.it('should set wrapper styles', () => { const compiled: HTMLElement = fixture.debugElement.nativeElement; const wrapEl: HTMLElement | null = compiled.querySelector('.wrapper'); if (wrapEl) { fixture.detectChanges(); expect(wrapEl.getAttribute('style')).toBe(`transform: ${component.myStyles.transform};`, 'default style'); component.myStyles = { transform: 'initial' }; // detect changes for the OnPush Component const debugEl = fixture.debugElement.query(By.directive(WidgetComponent)); // Need to cast as any until https://github.com/angular/angular/issues/23611 resolved const cdr = debugEl.injector.get<ChangeDetectorRef>(ChangeDetectorRef as any); cdr.detectChanges(); expect(wrapEl.getAttribute('style')).toBe('transform: initial;', 'manual style'); } else { fail('div.wrapper element not found'); } });
Except the linter warning that required me to cast the injector.get parameter as any this seems like an acceptable work around for Components that don't have the CDR in the constructor as in my last comment.
After hitting that same problem, we decided that overriding the changing detection in the TestBed means you are testing your component using a behaviour that is different from the one it is actually running on. Not ideal and not really useful particularly when your strategy relies on you knowing when to trigger change detection (or when it happens particularly).
Heavily inspired on the solution quoted, here is an util function we export as a part of a test-util kindda lib we use for this particularities like this.
import { ChangeDetectorRef } from '@angular/core';
import { ComponentFixture } from '@angular/core/testing';
export async function runOnPushChangeDetection<T>(cf: ComponentFixture<T>) {
const cd = cf.debugElement.injector.get<ChangeDetectorRef>(
// tslint:disable-next-line:no-any
ChangeDetectorRef as any
);
cd.detectChanges();
await cf.whenStable();
return;
}
On the gist there is also an example on how to use it. Hope it helps!
https://gist.github.com/7jpsan/5ac270f19c5d8d715220c06f6ccd123a
We used the code from @psoares-resilient and improved it a bit.
You can copy this code directly into the test-setup.ts
or write it into a library and call the function ImproveChangeDetection()
in the test-setup.ts
.
Now you can always call fixture.detectChanges()
as usual.
Note that some tests will be async
by this.
import { ChangeDetectorRef, Type } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
function runOnPushChangeDetection<T>(cf: ComponentFixture<T>) {
return async () => {
const cd: ChangeDetectorRef = cf.debugElement.injector.get<ChangeDetectorRef>(ChangeDetectorRef as any);
cd.detectChanges();
return await cf.whenStable();
}
}
export const ImproveChangeDetection = () => {
const originalCreate = TestBed.createComponent;
TestBed.createComponent = <T>(component: Type<T>) => {
const componentFixture: ComponentFixture<T> = originalCreate(component);
componentFixture.detectChanges = runOnPushChangeDetection(componentFixture);
return componentFixture;
};
};
the solution works, but for some reason there is a difference in output between the following two statements:
console.log(fixture.debugElement.query(By.css('.download-button')).attributes.href);
console.log((fixture.nativeElement as HTMLElement).getElementsByClassName('download-button').item(0).getAttribute('href'));
the first one says undefined, the second gives a nice URL. Both do correctly output the class name (which is not behind a binding). Any idea why that could be?
You can use the injector of the component to get the change detector which is reachable from the fixture:
const cd: ChangeDetectorRef = fixture.componentRef.injector.get(ChangeDetectorRef);
But it's not like a binding update, rather like the component changes its state then calls the change detector, so no life-cycle hook will be called. To properly test with inputs currently the official way is to wrap with a test host component.
A quick example how to do this: https://stackblitz.com/edit/angular-testing-template-h9wks4
This is the right way to do at the moment I think. It avoids overriding changeDetection
.
This is the right way to do at the moment I think. It avoids overriding
changeDetection
.
Based on the code, shouldn't fixture.componentRef.injector.get(ChangeDetectorRef)
and fixture.changeDetectorRef
be the same thing?
I guess I don't understand why the injected one works but the fixture's does not
Any updates on this ? will be there any fix :(
Any updates on this ? will be there any fix :(
This seems to be working for me with Angular 8:
it('should display the loading bar if isLoading is true', fakeAsync(() => {
fixture.detectChanges()
component.isLoading = true;
fixture.detectChanges();
flush();
const loadingBar = fixture.debugElement.query(By.css('[data-test=loading-bar]'));
expect(loadingBar).toBeTruthy();
}));
You can move the first fixture.detectChanges()
in your beforeEach
hook.
Docs team would like engineering input on verification of details in this conversation, if docs should be updated and if so, what we should focus on. Thanks!
This is the right way to do at the moment I think. It avoids overriding
changeDetection
.Based on the code, shouldn't
fixture.componentRef.injector.get(ChangeDetectorRef)
andfixture.changeDetectorRef
be the same thing?I guess I don't understand why the injected one works but the fixture's does not
Can anybody please explain this behavior? It at least feels completely wrong.
@meriturva - I think that clarifies things: the version on the fixture is the component's changeDetectorRef
, whereas calling componentRef.injector.get(ChangeDetectorRef)
does not return the changeDetectorRef
of the fixture component, but instead the component's parent (<div root>
probably?)
Is there any reason fixture.detectChanges()
shouldn't be calling change detection at that top level?
A good solution which doesn't involve 'hacking' into the changedetectionstrategy of the component is described here:
The library is also a very decent improvement for the angular testing experience.
experienced this today. how come such framework struggles with it for 4 years and 9 major releases?
tried on this issue for half an hour following is the reason and two solutions :
Reason : It fails due to a defect in Angular that fixture.detectChanges() works only the first time with ChangeDetectionStrategy.OnPush
Solution 1 : override a component metadata with overrideComponent in TestBed, override OnPush with Default change detection for testing .
TestBed.overrideComponent(TestComponent, {
changeDetection: ChangeDetectionStrategy.Default
});Solution 2 : Remove fixture.detectChanges from beforeEach block add it in your every spec.
Both the solutions works !
beforeEach(() => {
fixture = TestBed.createComponent(TodoComponent);
component = fixture.componentInstance;
this should be pinned or anything for quick reference. Solution 2 seems the best, easiest and least intrusive way to deal with the problem.
I tried on this issue for half an hour following is the reason and two solutions :
Reason : It fails due to a defect in Angular that fixture.detectChanges() works only the first time with ChangeDetectionStrategy.OnPush
Solution 1 : override a component metadata with overrideComponent in TestBed, override OnPush with Default change detection for testing .
TestBed.overrideComponent(TestComponent, {
changeDetection: ChangeDetectionStrategy.Default
});Solution 2 : Remove fixture.detectChanges from beforeEach block add it in your every spec.
Both the solutions works !
beforeEach(() => {
fixture = TestBed.createComponent(TodoComponent);
component = fixture.componentInstance;
// fixture.detectChanges();
});
it('should......., () => {
fixture.detectChanges();
});
it('should......., () => {
fixture.detectChanges();
});
You saved my day. It's a really good answer. I prefer the second solution so that the test is the same component.
Ohh ! thank you, My pleasure 😄 @albertzubkowicz , @xuanchuong
I am glad to know that my answer saved your day ! Njoy !!
Add fixture.detectChanges();
after the change.
I also meet with this annoying issue recently: wangzixi-diablo/Spartacus-learning#4
Great thanks to my colleague @Platonn who has provided a solution for it:
https://github.com/SAP/spartacus/blob/develop/projects/storefrontlib/src/shared/components/table/table.component.spec.ts#L42-L44
Confirmed on:
Angular CLI: 9.1.12
Node: 14.15.0
OS: darwin x64
Angular: 9.1.12
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router
Ivy Workspace: Yes
Package Version
-----------------------------------------------------------
@angular-devkit/architect 0.901.12
@angular-devkit/build-angular 0.901.12
@angular-devkit/build-optimizer 0.901.12
@angular-devkit/build-webpack 0.901.12
@angular-devkit/core 9.1.12
@angular-devkit/schematics 9.1.12
@angular/cdk 9.2.4
@angular/material 9.2.4
@angular/pwa 0.901.12
@ngtools/webpack 9.1.12
@schematics/angular 9.1.12
@schematics/update 0.901.12
rxjs 6.5.5
typescript 3.9.7
webpack 4.42.0
What works is to override component's changeDetection to default AND provide ComponentFixtureAutoDetect as true.
beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [SpinnerComponent], providers: [ {provide: ComponentFixtureAutoDetect, useValue: true} ] }) .overrideComponent(SpinnerComponent, { set: { changeDetection: ChangeDetectionStrategy.Default } }) .compileComponents(); fixture = TestBed.createComponent(SpinnerComponent); component = fixture.componentInstance; }));
Now fixture.detectChanges() should work in the tests.
Though this works, keep in mind that you are testing one behaviour, and actually running another. A couple ways of managing it without having to override the change detection strategy (in this issue):
#12313 (comment)
#12313 (comment)
We have used used it since v8 If memory serves me well and still works perfectly (v10.2.x), removing the need to override the component change detection strategy.
Still running on this to this day...
I personally like the overriding "ChangeDetectionStrategy" solution as it's a one time thing on TestBed setup, but I understand this kind of intrusive solution isn't ideal.
TestBed.configureTestingModule({
imports: [],
declarations: [TestComponent],
providers: []
})
.overrideComponent(TestComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
})
.compileComponents();
There's the "ChangeDetectorRef" solution which I've seen being used on the Component class itself with "changeDetector.markForCheck()" and that is not a good way as your component should not have to adapt to testing, but you can still use this solution, without messing with the actual component, by calling instead of the normal "detectChanges()", as presented here
const cdr = debugEl.injector.get<ChangeDetectorRef>(ChangeDetectorRef as any);
cdr.detectChanges();"
And finally there's the simplest solution, at least in my head, and which, curiously, I haven't found any mentions to it.
So, you already probably know you can (or end up having to) create a host component to wrap the one you're testing, a lot of blogs, for example, out there showcase the usage of a @ViewChild(ComponentUnderTestComponent)
approach which would be perfect if jasmine could actually perceive a change in the child component, but, as it looks like, it doesn't and we are stuck with the normal intuitive approach of just listing the inputs in the host and binding them directly in the template of the testing component, like this:
@Component({
template: `<component-tag [(ngModel)]="val" [someProperty]="flag"></component-tag>`
})
class HostComponent {
val: number;
flag: boolean = false;
}
with that, now you can actually change the value of HostComponent.someProperty and then call detectChanges() and jasmine will perfectly do what it's supposed to and update the DOM with the change:
fixture.componentInstance.readonly = true;
fixture.detectChanges();
Now, if your component goes ahead and have dozens of input attributes, then I guess this isn't really viable, but anyway, I thought I'd throw it out there, enjoy
Writing a wrapper component every time can become annoying.
You can use libraries like @testing-library/angular or @ngneat/spectator that wrap the tested component automatically.
You can also use ng-mocks, https://ng-mocks.sudo.eu/api/MockRender#testing-changedetectionstrategyonpush
I have no idea why
fixture.detectChanges
won't work in my test case here where I am replacing an object. But I found a way to get the ChangeDetectorRef that will detectChanges.it('should set wrapper styles', () => { const compiled: HTMLElement = fixture.debugElement.nativeElement; const wrapEl: HTMLElement | null = compiled.querySelector('.wrapper'); if (wrapEl) { fixture.detectChanges(); expect(wrapEl.getAttribute('style')).toBe(`transform: ${component.myStyles.transform};`, 'default style'); component.myStyles = { transform: 'initial' }; // detect changes for the OnPush Component const debugEl = fixture.debugElement.query(By.directive(WidgetComponent)); // Need to cast as any until https://github.com/angular/angular/issues/23611 resolved const cdr = debugEl.injector.get<ChangeDetectorRef>(ChangeDetectorRef as any); cdr.detectChanges(); expect(wrapEl.getAttribute('style')).toBe('transform: initial;', 'manual style'); } else { fail('div.wrapper element not found'); } });
Except the linter warning that required me to cast the injector.get parameter as any this seems like an acceptable work around for Components that don't have the CDR in the constructor as in my last comment.
After hitting that same problem, we decided that overriding the changing detection in the TestBed means you are testing your component using a behaviour that is different from the one it is actually running on. Not ideal and not really useful particularly when your strategy relies on you knowing when to trigger change detection (or when it happens particularly).
Heavily inspired on the solution quoted, here is an util function we export as a part of a test-util kindda lib we use for this particularities like this.
import { ChangeDetectorRef } from '@angular/core'; import { ComponentFixture } from '@angular/core/testing'; export async function runOnPushChangeDetection<T>(cf: ComponentFixture<T>) { const cd = cf.debugElement.injector.get<ChangeDetectorRef>( // tslint:disable-next-line:no-any ChangeDetectorRef as any ); cd.detectChanges(); await cf.whenStable(); return; }
On the gist there is also an example on how to use it. Hope it helps!
https://gist.github.com/7jpsan/5ac270f19c5d8d715220c06f6ccd123a
Can this be used with fakeAsync() instead of async()?
In FakeAsync you cannot wait with "await" for the Promise to resolve.
But in my opinion fakeAsync() will do that with tick() automatically (or even without tick())?
with async it is straightforward:
it('test', async() => {
...
await runOnPushChangeDetection(fixture);
// do some stuff AFTER the fixture is stable
});
But for fakeAsync?
it('test', fakeAsync(() => {
....
runOnPushChangeDetection(fixture);
// is tick() needed?
// will it be guaranteed that the fixture is already stable?
// do some stuff AFTER the fixture is stable
}));
If it helps to understand, fakeAsync
is supposed to make all promises synchronous -- all pending tasks are resolved before another statement is executed. You shouldn't need whenStable
etc because it "should" always be stable after every statement. (This also makes fakeAsync
unsuitable for certain design patterns, but that's another issue.)
I have no idea why
fixture.detectChanges
won't work in my test case here where I am replacing an object. But I found a way to get the ChangeDetectorRef that will detectChanges.it('should set wrapper styles', () => { const compiled: HTMLElement = fixture.debugElement.nativeElement; const wrapEl: HTMLElement | null = compiled.querySelector('.wrapper'); if (wrapEl) { fixture.detectChanges(); expect(wrapEl.getAttribute('style')).toBe(`transform: ${component.myStyles.transform};`, 'default style'); component.myStyles = { transform: 'initial' }; // detect changes for the OnPush Component const debugEl = fixture.debugElement.query(By.directive(WidgetComponent)); // Need to cast as any until https://github.com/angular/angular/issues/23611 resolved const cdr = debugEl.injector.get<ChangeDetectorRef>(ChangeDetectorRef as any); cdr.detectChanges(); expect(wrapEl.getAttribute('style')).toBe('transform: initial;', 'manual style'); } else { fail('div.wrapper element not found'); } });
Except the linter warning that required me to cast the injector.get parameter as any this seems like an acceptable work around for Components that don't have the CDR in the constructor as in my last comment.
After hitting that same problem, we decided that overriding the changing detection in the TestBed means you are testing your component using a behaviour that is different from the one it is actually running on. Not ideal and not really useful particularly when your strategy relies on you knowing when to trigger change detection (or when it happens particularly).
Heavily inspired on the solution quoted, here is an util function we export as a part of a test-util kindda lib we use for this particularities like this.import { ChangeDetectorRef } from '@angular/core'; import { ComponentFixture } from '@angular/core/testing'; export async function runOnPushChangeDetection<T>(cf: ComponentFixture<T>) { const cd = cf.debugElement.injector.get<ChangeDetectorRef>( // tslint:disable-next-line:no-any ChangeDetectorRef as any ); cd.detectChanges(); await cf.whenStable(); return; }
On the gist there is also an example on how to use it. Hope it helps!
https://gist.github.com/7jpsan/5ac270f19c5d8d715220c06f6ccd123aCan this be used with fakeAsync() instead of async()?
In FakeAsync you cannot wait with "await" for the Promise to resolve.
But in my opinion fakeAsync() will do that with tick() automatically (or even without tick())?with async it is straightforward:
it('test', async() => { ... await runOnPushChangeDetection(fixture); // do some stuff AFTER the fixture is stable });
But for fakeAsync?
it('test', fakeAsync(() => { .... runOnPushChangeDetection(fixture); // is tick() needed? // will it be guaranteed that the fixture is already stable? // do some stuff AFTER the fixture is stable }));
Might look a bit weird, but fakeAsync allows you pass an a async function to it. I see no reason the following example wouldn't work, but happy to hear if people have a different view on it:
it('test', fakeAsync(async () => {
.... doing some setup and invoking some method that triggers a promise somewhere internally that will resolve in 1s...
.... call "tick()" so that promise will resolve ...
// Next, call the "runOnPushChangeDetection()" and you're golden!
// Use the same syntax as the non fakeAsync() version.
await runOnPushChangeDetection(fixture);
... Assert on things changing based on what should have happened as you normally would. Trust the CD was called :D ...
}));
An actual example from one of our tests:
it('should render a badge when there are new records', fakeAsync(async () => {
component.ngOnInit();
tick();
await testUtil.runOnPushChangeDetection();
expect(fixture.debugElement.query(By.css(`[e2e="tab-1"] [badge].active`))).toBeFalsy();
expect(fixture.debugElement.query(By.css(`[e2e="tab-2"] [badge].active`))).toBeFalsy();
expect(fixture.debugElement.query(By.css(`[e2e="tab-3"] [badge].active`))).toBeTruthy();
expect(fixture.debugElement.query(By.css(`[e2e="tab-4"] [badge].active`))).toBeFalsy();
}));
As stated rather early in this discussion, creating a wrapping component is the only way currently to test the actual real world functionality of an OnPush component. @alexzuza references this with regard to testing @input(). All of the workarounds in this discussion are subverting the nature of OnPush.
- Do not override the change detection strategy.
- Do not be sneaky about getting a reference to the change detector (this could break in subsequent Angular updates)
- Don't you WANT to test that your OnPush component updates when it should, legitimately test @inputs @outputs and RxJS streams to test change detection properly.
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.