How to initialise Angular components with signal inputs from test?
Asked Answered
C

2

12

Let's say I have a simple component with signal input introduced in Angular 17.1

@Component({
  selector: 'greet',
  template: `
    <span class="greet-text">{{firstName()}}</span>`,
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GreetComponent {
  firstName = input.required<string>();
}

How can I instantiate this component from TestBed and assign signal inputs?

The only solution I found in Angular repo, it to wrap it into another component without signal input.

Is it the only way to test new inputs? Doubling number of components...

describe('greet component', () => {
  it('should allow binding to an input', () => {
    const fixture = TestBed.createComponent(TestCmp);
    fixture.detectChanges();
  });
});

@Component({
  standalone: true,
  template: `<greet [firstName]="firstName" />`,
  imports: [GreetComponent],
})
class TestCmp {
  firstName = 'Initial';
}
Culture answered 18/1 at 9:27 Comment(0)
R
17

As stated in docs of InputFuction interface, InputSignal is non-writable:

/**
 * `InputSignal` is represents a special `Signal` for a directive/component input.
 *
 * An input signal is similar to a non-writable signal except that it also
 * carries additional type-information for transforms, and that Angular internally
 * updates the signal whenever a new value is bound.
 *
 * @developerPreview
 */

However, Alex Rickabaugh pointed out in this comment:

When you create a component "dynamically" (e.g. with ViewContainerRef.createComponent) then you "control" the component and its inputs via its ComponentRef. ComponentRef has .setInput to update the values for inputs of that component.

In tests, ComponentFixture exposes the .componentRef so you can use .setInput to test the component by manipulating its inputs. All of this works today, and will work with signal components too.

This now works with jest-preset-angular v14.0.2 and Angular 17.3.2.

Example

import { ComponentRef } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { GreetComponent } from './greet.component'

describe('GreetComponent', () => {
  let component: GreetComponent
  let componentRef: ComponentRef<GreetComponent>
  let fixture: ComponentFixture<GreetComponent>

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [GreetComponent],
    }).compileComponents()

    fixture = TestBed.createComponent(GreetComponent)

    component = fixture.componentInstance
    componentRef = fixture.componentRef
    componentRef.setInput('firstName', 'Feyd-Rautha')
    fixture.detectChanges()
  })

  it('should create', () => {
    expect(component).toBeTruthy()
  })
})
Radiotherapy answered 21/1 at 12:48 Comment(3)
Yes, does not work to set inputs with setInput. I got the same error.Herndon
this is currently blocked by github.com/thymikee/jest-preset-angular/issues/2246Radiotherapy
This seems working fine for me but say with same inputs but different values if have to do some more tests , how would i initialize them here? here when i set Input to a different value for testing purpose?Eustazio
P
3

To initialize inputs or models in unit tests you can wrap the initialization in an injection context like so

TestBed.runInInjectionContext(() => {
    component.firstName = input('Test user')
});

Note this also works for models also

TestBed.runInInjectionContext(() => {
    component.firstName = model('Test user')
});
Pervade answered 2/4 at 10:22 Comment(2)
This does not work when the input is readonly :) componentRef.setInput works thoughLiberati
@MaximilianFriedmann That's fair enough, though you can say the same for any class level variable that is readonly even if they are not inputs or models, you will not be able to touch them in your unit tests.Pervade

© 2022 - 2024 — McMap. All rights reserved.