Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: Angular 17.1.0 input.required() issues #7976

Open
Sebastian-G opened this issue Jan 18, 2024 · 16 comments
Open

Bug: Angular 17.1.0 input.required() issues #7976

Sebastian-G opened this issue Jan 18, 2024 · 16 comments
Assignees
Labels
bug Something isn't working

Comments

@Sebastian-G
Copy link

Sebastian-G commented Jan 18, 2024

Description of the bug

After upgrading Angular to 17.1.0 and using input.required(), my tests are broken.

see

readonly user = input.required<User>();

It seems like it is not possible to use the MockRenderer to initialize the component anymore:

Message:
      NG0950: Input is required but no value is available yet. Find more at https://angular.io/errors/NG0950      
 NG0303: Can't bind to 'user' since it isn't a known property of 'app-user-profile-card' (used in the 'AppComponent' component template).
    1. If 'app-user-profile-card' is an Angular component and it has the 'user' input, then verify that it is a part of an @NgModule where this component is declared.
    2. If 'app-user-profile-card' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.
    3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.

An example of the bug

  • Angular 17.1.0
  • ngMocks v14.12.1
  • with Jest

Link:
https://stackblitz.com/edit/ex-base-reusable-component-epbxhj?file=src%2Fapp%2Fuser-profile-card%2Fuser-profile-card.component.spec.ts

RUN npm test to see the error.

Expected vs actual behavior

All tests should run through.
There should be no difference between @Input() and the input function.

As you can see it is working as expected on npm start.

@Sebastian-G Sebastian-G added the bug Something isn't working label Jan 18, 2024
@Sebastian-G
Copy link
Author

meta.inputs is empty if we use input() instead of @Input

__ngMocksDeclarations doesn't contain the inputs (see libs/ng-mocks/src/lib/resolve/collect-declarations.ts).

@jits
Copy link

jits commented Jan 23, 2024

It looks like, at the moment, Angular themselves test out signal inputs by manually defining and using wrapper components (i.e. without any reflection on the available inputs and types):

https://github.com/angular/angular/blob/e2272750878e1f74e59121e76a01e910ad1ac607/integration/cli-signal-inputs/src/app/greet.component.spec.ts#L20-L27

@squelix
Copy link

squelix commented Feb 8, 2024

Any news about this issue ?

@jits
Copy link

jits commented Feb 16, 2024

Potentially useful: the Spectator library is being updated to support signal inputs: ngneat/spectator#638

@cuddlecake
Copy link

Seems to me like this can be regarded as resolved.
jest-preset-angular added support for input signals, queries and models in ^14.0.1

For comparison, this is @Sebastian-G 's stackblitz with @angular-builders/[email protected] which uses an older version of jest-preset-angular:
https://stackblitz.com/edit/ex-base-reusable-component-c33uk8?file=package.json ; the tests fail

This is the same stackblitz with @angular-builders/[email protected] which uses jest-preset-angular@^14.0.1:
https://stackblitz.com/edit/ex-base-reusable-component-zpyjgn?file=package.json ; the tests pass, aside from a snapshot test not matching, which I don't care about in this context

TL;DR: Update jest-preset-angular or dependencies that use jest-preset-angular

@jits
Copy link

jits commented Feb 21, 2024

How about when using Jasmine? (The default out-of-the-box experience that Angular provides)

@cuddlecake
Copy link

Fair enough, my comment was jest-centric. I have no idea about jasmine

@jits
Copy link

jits commented Feb 21, 2024

@cuddlecake — it's useful to know, thanks for sharing.

Aside: Angular does have official experimental support for Jest; I wonder where that will go.

@andreandersson
Copy link

andreandersson commented Apr 17, 2024

Not sure if this is a seperate issue or related, but I'm getting Error: NG0303 from signal components. Note that input.required isn't needed, a simple input gives the same error. Here is a test-case:

import { CommonModule } from '@angular/common';
import { Component, NgModule, input } from '@angular/core';

import { MockBuilder, MockRender } from 'ng-mocks';

@Component({
  selector: 'signal-component',
  template: '<h1>{{ header() }}</h1>',
})
class SignalComponent {
  public readonly header = input<string>();
}

@Component({
  selector: 'target-component',
  template: '<signal-component [header]="hello"></signal-component>',
})
class TargetComponent {
  public readonly hello = 'Hello!';
}

@NgModule({
  declarations: [TargetComponent, SignalComponent],
  imports: [CommonModule],
})
class TargetModule {}

describe('signal inputs', () => {
  beforeEach(() => MockBuilder(TargetComponent, TargetModule));

  it('should be created', () => {
    expect(
      MockRender(TargetComponent)?.point?.componentInstance,
    ).toBeTruthy();
  });
});

@andreandersson
Copy link

I believe it might be a good idea to migrate to reflectComponentType from @angular/core to fetch inputs / outputs (and other data regarding the component). That is however quite some work, and I'm not sure if it is the correct path or even where / how / if that should start. But I have been able to confirm that reflectComponentType does return inputs and outputs for signals, decorators and host-properties.

Do you have any input @satanTime?

@satanTime
Copy link
Member

Hi @andreandersson, it used to be like that, but it doesn't represent all the data which is needed for mocks, that's why ng-mocks replies on decorators data instead.

@andreandersson
Copy link

andreandersson commented Apr 23, 2024

Done some digging. This probably relates to angular/angular#54013

For example, this does not work in ng-mocks, but in a new Angular-repository:

import {
    Component,
    input,
    reflectComponentType,
  } from '@angular/core';
  
  @Component({
    selector: 'test-component',
    template: '<h1>{{ header() }}</h1>',
  })
  class TestComponent {
    public readonly header = input.required<string>();
  }
  
  describe('signals', () => {
    it('should print header', () => {
      expect(reflectComponentType(TestComponent)?.inputs?.length).toBe(1);
    });
  });

andreandersson pushed a commit to andreandersson/ng-mocks that referenced this issue Apr 24, 2024
Adds a custom transformer to transform Angular Code, specifically,
signals (input, query and models) to behave better in jit-environment.

Solves help-me-mom#7976
andreandersson pushed a commit to andreandersson/ng-mocks that referenced this issue Apr 24, 2024
Adds a custom transformer to transform Angular Code, specifically,
signals (input, query and models) to behave better in jit-environment.

Solves help-me-mom#7976
andreandersson pushed a commit to andreandersson/ng-mocks that referenced this issue Apr 25, 2024
Adds a custom transformer to transform Angular Code, specifically,
signals (input, query and models) to behave better in jit-environment.

Solves help-me-mom#7976
@henriquecustodia
Copy link

henriquecustodia commented May 3, 2024

I've noticed that Signal Inputs are just treated as Decorator Input.

I have the following component:

@Component({
  selector: 'app-list-item',
  standalone: true,
  templateUrl: './list-item.component.html',
  styleUrl: './list-item.component.scss',
})
export class ListItemComponent { 
  task = input.required<Task>();
}

I mock the component using MockComponent function:

 await TestBed.configureTestingModule({
      imports: [ListComponent],
      providers: [
        {
          provide: TasksService,
          useValue: tasksServiceStub,
        },
      ],
    })
      .overrideComponent(ListComponent, {
        remove: {
          imports: [ListItemComponent],
        },
        add: {
          imports: [MockComponent(ListItemComponent)], // I mock here
        },
      })
      .compileComponents();

And my test try to access the Singal Input from ListItemComponent to assert its value:

 expect((<ListItemComponent>item.componentInstance).task()).toEqual(completedTasksStub[index]);

But the test breaks because the task property (it's a Signal Input) is not a function.

The complete error:
TypeError: completedItemDebugEl.componentInstance.task is not a function

Logging the <ListItemComponent>item.componentInstance).task in the test, I see that it's a property

task: { id: '1', name: 'Buy milk', completed: true }

Working with Signal Inputs, we need to test it as a Signal and not as a property as it was with Decorator Inputs.

Any news about that @satanTime?

Thank for your amazing work btw

@henriquecustodia
Copy link

I just created an issue related to my last comment
#8887

andreandersson pushed a commit to andreandersson/ng-mocks that referenced this issue May 14, 2024
Adds a custom transformer to transform Angular Code, specifically,
signals (input, query and models) to behave better in jit-environment.

Solves help-me-mom#7976
andreandersson pushed a commit to andreandersson/ng-mocks that referenced this issue May 20, 2024
Adds a custom transformer to transform Angular Code, specifically,
signals (input, query and models) to behave better in jit-environment.

Solves help-me-mom#7976
andreandersson pushed a commit to andreandersson/ng-mocks that referenced this issue May 20, 2024
Adds a custom transformer to transform Angular Code, specifically,
signals (input, query and models) to behave better in jit-environment.

Solves help-me-mom#7976

Signed-off-by: André Andersson <[email protected]>
@Xample
Copy link

Xample commented Jun 11, 2024

This works:

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

@Component({
    selector: 'app-banner',
    standalone: true,
    imports: [],
    templateUrl: './banner.component.html',
    styleUrl: './banner.component.scss',
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BannerComponent {
    title = input.required<string>();
}
import { MockBuilder, MockRender } from 'ng-mocks';
import { I18nTestingModule } from '@/i18n/i18n.testing.module';
import { BannerComponent } from './banner.component';

describe('BannerComponent', () => {
    async function setup() {
        await MockBuilder(BannerComponent).keep(I18nTestingModule);
        return MockRender(BannerComponent, { title: 'Hello World' }); // set your input here (it does not seem to be typed btw)
    }

    it('should create', async () => {
        const fixture = await setup();
        expect(fixture.point.componentInstance).toBeTruthy();
    });
});

@evtk
Copy link

evtk commented Nov 30, 2024

@Xample thanks. I can confirm that it seems to be simply working. At first I got stuck on the typings. As when you store the fixture as a MockedComponentFixture you will get a conflict on the properties of the component.

Types of property 'myPropertyAsBoolean' are incompatible.
      Type 'boolean' is not assignable to type 'InputSignal<boolean>'.

But we can work around it of course with the penalty of losing some typings. Or, some sort of wrapper type for the MockedComponentFixture can be added, where the option is added to convert properties to a InputSignal typed one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

9 participants