angulartypescripttestingkarma-jasmineupgrade

Directive inputs with the new input() signals


I've successfully migrated my Angular 18 project to Angular 19 and replaced all @Input decorators with the new input() signals. The code works but the test for a directive with input() signals breaks.

Here is my directive, it just multiplies values of a form group.

import { Directive, OnInit, input } from "@angular/core";
import { FormGroup, FormGroupDirective } from "@angular/forms";
import { FormItem } from "../model/form.model";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";

@UntilDestroy()
@Directive({
  selector: "[appMultiply]",
  standalone: true,
})
export class MultiplyDirective implements OnInit {
  constructor(formGroupDirective: FormGroupDirective) {
    this.form = formGroupDirective.form;
  }

  readonly appMultiply = input<FormItem>();
  readonly formDefinition = input<Array<FormItem>>();

  private form: FormGroup;

  ngOnInit(): void {
    const appMultiply = this.appMultiply();
    if (!appMultiply?.multiply || !this.formDefinition()) {
      return;
    }

    const sourceControl = this.form.get(appMultiply.formControlName);

    if (!sourceControl) {
      return;
    }

    //initial calculation
    this.calculateTargetControlValues(appMultiply, sourceControl.value);

    //wait for source changes
    sourceControl.valueChanges
      .pipe(untilDestroyed(this))
      .subscribe((value) =>
        this.calculateTargetControlValues(this.appMultiply()!, value),
      );
  }

  private calculateTargetControlValues = (
    appMultiply: FormItem,
    sourceValue: number,
  ) => {
    const multiplyByControl = this.form.get(
      appMultiply.multiply!.multiplyByControlName,
    );

    if (!multiplyByControl) {
      return;
    }

    this.formDefinition()!
      .filter((formItem) =>
        this.appMultiply()!.multiply!.targetFormControlNames.includes(
          formItem.formControlName,
        ),
      )
      .forEach((formItem) => {
        const targetControl = this.form.get(formItem.formControlName);
        if (!targetControl) {
          return;
        }
        const result = (sourceValue ?? 0) * (multiplyByControl.value ?? 0);
        targetControl.patchValue(result, { emitEvent: false });
      });
  };
}

This is the basic test of the directive:

import { FormGroupDirective } from "@angular/forms";
import { MultiplyDirective } from "./multiply.directive";

describe("MultiplyDirective", () => {
  it("should create an instance", () => {
    const directive = new MultiplyDirective(new FormGroupDirective([], []));
    expect(directive).toBeTruthy();
  });
});

And I get this error from Karma : "Error: NG0203: inputFunction() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with runInInjectionContext. Find more at https://angular.dev/errors/NG0203"

If I replace the 2 input signals with @Input decorator inputs, the test runs successfully. Like this :

  @Input() appMultiply!: FormItem
  @Input() formDefinition!: Array<FormItem>

What do i do wrong?


Solution

  • You can use the runInInjectionContext context method to make the directive initialize. We have to provide the first argument as the EnvironmentInjector to the method.

    beforeEach(waitForAsync(() => {
      fixture = TestBed.createComponent(AppComponent);
      injector = TestBed.inject(EnvironmentInjector);
      // injector = TestBed.inject(Injector);
      component = fixture.componentInstance;
      fixture.detectChanges();
    }));
    
    it('should create', () => {
      const directive = runInInjectionContext(injector, () => {
        return new MultiplyDirective(new FormGroupDirective([], []));
      });
      expect(directive).toBeTruthy();
    });
    

    Full Code:

    import { TestBed, ComponentFixture, waitForAsync } from '@angular/core/testing';
    import { AppComponent, MultiplyDirective } from './app.component';
    import { AppModule } from './app.module';
    import { FormGroupDirective } from '@angular/forms';
    import {
      runInInjectionContext,
      Injector,
      EnvironmentInjector,
    } from '@angular/core';
    
    describe('AppComponent', () => {
      let component: AppComponent;
      let fixture: ComponentFixture<AppComponent>;
      let injector: EnvironmentInjector;
      // let injector: Injector;
      beforeEach(() => {
        TestBed.configureTestingModule({
          imports: [AppModule],
        }).compileComponents();
      });
    
      beforeEach(waitForAsync(() => {
        fixture = TestBed.createComponent(AppComponent);
        injector = TestBed.inject(EnvironmentInjector);
        // injector = TestBed.inject(Injector);
        component = fixture.componentInstance;
        fixture.detectChanges();
      }));
    
      it('should create', () => {
        const directive = runInInjectionContext(injector, () => {
          return new MultiplyDirective(new FormGroupDirective([], []));
        });
        expect(directive).toBeTruthy();
      });
    });
    

    Stackblitz Demo