Sergey Tihon
Sergey Tihon

Reputation: 12913

How to initialise Angular components with signal inputs from test?

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';
}

Upvotes: 25

Views: 14065

Answers (5)

S&#228;m Grmic
S&#228;m Grmic

Reputation: 3

As George Knap has pointed out, you can use componentRef.setInput(name: string, value: unknown).

To make this more type-safe, you could write it like this:

fixture.componentRef.setInput(
  'key' satisfies keyof typeof component,
  value satisfies ReturnType<(typeof component)['key']>,
);

If you want to avoid doing this over and over again, you could also write a function like this:

/**
 * This updates an input signal of the {@link fixture.componentInstance} component in a type-safe way.
 * @param fixture The fixture of the component.
 * @param key The key of the input signal.
 * @param value The new value passed to the input signal.
 */
function updateInputSignal<
  ComponentType extends { [key in Key]: InputSignal<ValueType> },
  Key extends keyof ComponentType,
  ValueType,
>(fixture: ComponentFixture<ComponentType>, key: Key & string, value: ValueType) {
  fixture.componentRef.setInput(key, value);
}

// And use it:
updateInputSignal(fixture, 'key', value);

Upvotes: 0

Lucas Sim&#245;es
Lucas Sim&#245;es

Reputation: 657

I am still facing this issue. The error occurs during the execution of fixture = TestBed.createComponent(GreetComponent).

A workaround is to encapsulate your component within another component and set up the test suite for this parent component. For example:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, Component, viewChild, ViewEncapsulation } from '@angular/core';
import { mockData } from '../../tests/mocks';
import { ChildComponentWithRequiredInputs } from './child-component-with-required-inputs.component';

// Encapsulation
@Component({
    selector: 'mock-parent',
    template: `<child-component-with-required-inputs [data]="data" inputA="mockValueA" inputB="mockValueB" />`,
    encapsulation: ViewEncapsulation.None,
    changeDetection: ChangeDetectionStrategy.OnPush,
    standalone: true,
    imports: [ChildComponentWithRequiredInputs]
})
class MockComponent {
    data = mockData;
    childComponentWithRequiredInputs = viewChild.required(ChildComponentWithRequiredInputs);
}

describe('ChildComponentWithRequiredInputs', () => {
    let component: ChildComponentWithRequiredInputs;
    let fixture: ComponentFixture<MockComponent>;

    beforeEach(async () => {
        await TestBed.configureTestingModule({
            imports: [MockComponent]
        });

        fixture = TestBed.createComponent(MockComponent);
        component = fixture.componentInstance.childComponentWithRequiredInputs();
        fixture.detectChanges();
    });

    it('should create', () => {
        expect(component).toBeTruthy();
    });
});

Upvotes: 0

Aouidane Med Amine
Aouidane Med Amine

Reputation: 1681

I had the same problem. Since you are calling it like a normal function in your component, you can mock it like this:

      jest.spyOn(component, 'yourInputName').mockReturnValue('17');

Upvotes: 0

George Knap
George Knap

Reputation: 1430

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()
  })
})

Upvotes: 34

el-davo
el-davo

Reputation: 459

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')
});

Upvotes: 5

Related Questions