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

Setting input signals with Angular component tests not supported #29264

Closed
FrankVerbeek opened this issue Apr 4, 2024 · 12 comments · Fixed by #29621
Closed

Setting input signals with Angular component tests not supported #29264

FrankVerbeek opened this issue Apr 4, 2024 · 12 comments · Fixed by #29621
Assignees
Labels
CT Issue related to component testing npm: @cypress/angular @cypress/angular package issues type: feature New feature that does not currently exist

Comments

@FrankVerbeek
Copy link

Current behavior

With Angular versions 17.1 and 17.2, signal and model inputs were added, respectively. As we transition to using signals more frequently in our components, we have encountered several problems while setting up our component tests. It appears that setting input signals in Cypress component tests is not supported, despite the documentation stating that Cypress version 13.5.0 and onwards supports Angular 17.

When attempting to set up a component test by mounting the Component directly, it fails to set the signal/model inputs through the componentProperties and gives incorrect type assertions on componentProperties. Additionally, the outputSpy does not capture the change event from the model input either.

However, mounting the component with input signals through a template does work. I view this as a workaround, though.

Example code can be found here:
https://github.com/FrankVerbeek/angular-signal-component-test/blob/main/angular-signal-component-test/src/app/test-component/test-component.component.cy.ts

type assertion error:
image

not working example (mounting component):
image

working example (template):
image

Desired behavior

We want to be able to set input signals and model inputs directly through the componentProperties when mounting an Angular component using these kind of inputs.

Test code to reproduce

I have created a repo with an example. In the test-component.component.cy.ts file I created two tests, one which mounts the Component directly. This one doesn't work with setting the signal inputs. The second test uses a template and does work, kind of a workaround for now.

https://github.com/FrankVerbeek/angular-signal-component-test

The first test will throw the exception: "TypeError: ctx.title is not a function"

Cypress Version

13.7.2

Node version

20.11.1

Operating System

Windows 11

Debug Logs

No response

Other

No response

@cacieprins
Copy link
Contributor

When mounting a template, it looks like the componentProperty type is assumed to be correct and inferred, rather than sourced from the mounted component.

When you mount TestComponentComponent directly, however, componentProperties is typed as Partial<{ [P in keyof T]: T[P] }>, where T is being inferred as TestComponentComponent.

According to your test component, its properties are:

  • title is an InputSignal<string>
  • count is a ModelSignal<number>

When mounting this component, you are passing a string instead of an InputSignal<string>, and a WritableSignal<number> instead of a ModelSignal<number>.

@cacieprins cacieprins added the stage: wontfix Cypress does not regard this as an issue or will not implement this feature label Apr 4, 2024
@FrankVerbeek
Copy link
Author

FrankVerbeek commented Apr 5, 2024

Hi @cacieprins,

Thank you for the response. I would like to point out, however, that passing the types InputSignal<string> and ModelSignal<number> is only possible within an Angular Injectable context. Additionally, supplying a string as an input to a signal of type input.required<string> is a valid operation. For more information regarding signal inputs: Angular documentation on signal inputs.

Furthermore, the solution provided does not address the issue with outputSpy. Given Angular's increasing emphasis on the use of signals, it's probable that more people will have similar challenges shortly.

@Turom
Copy link

Turom commented Apr 5, 2024

I have the exact same issue with input signals using mount, configuring the component with non-signal data throws the ctx error and using signal inputs as component property throws the Inject context error.

Relying to the inline template method is currently the only way to make it work with latest version, it’s not ideal.

@Waterstraal
Copy link

@cacieprins I am not sure why you closed this issue so soon, because this is very much a big issue for Angular component testing. I think you misunderstood the issue.

This is very important: In Angular, signal inputs are of type InputSignal<T> internally in the Component, but the external api is T.

  • In @FrankVerbeek's example, the Component, internally the title property is of type InputSignal<string>.
  • When this component is used in another component (html, component), a primitive string is passed in.

This is how input signals work in Angular, and this is currently not supported in Cypress.

@jennifer-shehane can this issue be reopened?

@denisyilmaz
Copy link

Dmytro Mezhenskyi from Decoded Frontend suggested a solution with a TestHost Component that sets the inputs, which works with Cypress:
image

https://youtu.be/U8YXaWwyd9k?si=fR4Pd5LErMGztCZ4&t=720

@Waterstraal
Copy link

Waterstraal commented Apr 9, 2024

@denisyilmaz IMHO it's very poor DevX to have to create a wrapper component just to be able to test components with signal inputs. This should be natively supported by Cypress.

@denisyilmaz
Copy link

@Waterstraal absolutely! I mentioned this as a workaround for the time being until this is natively supported by Cypress.

@cacieprins
Copy link
Contributor

I am sorry for closing this prematurely. Angular is not my area of expertise, and I did not fully understand the issue at hand.

@cacieprins cacieprins reopened this Apr 9, 2024
@jennifer-shehane jennifer-shehane added npm: @cypress/angular @cypress/angular package issues stage: needs investigating Someone from Cypress needs to look at this and removed stage: wontfix Cypress does not regard this as an issue or will not implement this feature labels Apr 9, 2024
@grosch-intl
Copy link

grosch-intl commented Apr 9, 2024

Having to wrap every input and output in a test component is pretty painful.

@cacieprins, when do you think you'll be able to provide an ETA on a fix for this? I converted my entire project to using input/output/model/viewChild signals about a month ago. We just got somebody who is going to focus on our tests, which haven't been touched in ages, but now he can't do anything cleanly.

@jennifer-shehane jennifer-shehane added type: feature New feature that does not currently exist CT Issue related to component testing labels Apr 12, 2024
@Waterstraal
Copy link

@jennifer-shehane could I kindly ask for an update on this issue?

I think this issue is very important to fix because Angular apps are swiftly migrating to using signals, and this issue is making it hard to test those components and apps.

Note that a Playwright release that supports Angular Component testing with signal inputs is close to being released.

Thank you!

@AtofStryker AtofStryker self-assigned this May 29, 2024
@jennifer-shehane jennifer-shehane added stage: investigating Someone from Cypress is looking into this and removed stage: needs investigating Someone from Cypress needs to look at this labels Jun 3, 2024
@AtofStryker
Copy link
Contributor

AtofStryker commented Jun 5, 2024

Updates

Hey all. Sorry for the delay on the issue update here. The good news is we are getting pretty close to supporting signals for Angular version 17.2 and up. I have a draft PR that I am working on getting ready for review #29621.

Since signals introduced new methods/types to the API, we need to introduce a new test harness to Cypress in order to support signals. Right now we are calling it cypress/angular-signals. This will be readily available once the PR is merged in and cypress is released. In the future, we should be able to eventually merge this upstream into cypress/angular with the next major version of cypress when we remove/deprecate support for angular 13-16. In other words, the new harness need should not be permanent.

If any of you want to try the new testing harness, it is available on this commit. Give it an npm install and try it with your angular tests using signals! Just make sure to use the correct mount function inside you component.ts support file, as well as related imports within your tests, such as createOutputSpy.

import { mount } from 'cypress/angular-signals'

@FrankVerbeek I created a PR against your sample issue with the new testing harness. Let me know what you think and if it solves your needs.

Documentation / Behavior

I am currently starting work to get docs.cypress.io updated with angular-signals component testing. But until that is finished, I figure I will try to describe the new behavior of how to test with signals.

Typings issue

Mentioned in the issue is the following:

When attempting to set up a component test by mounting the Component directly, it fails to set the signal/model inputs through the componentProperties and gives incorrect type assertions on componentProperties.

This is because in our standard cypress/angular mount function, we expect a direct 1:1 mapping as the prop types

componentProperties?: Partial<{ [P in keyof T]: T[P] }>

In our new cypress/angular-signals harness, we need to make sure that a given generic, type T (or in the example given in the issue, a string) , can be inferred by an input(), signal(), or model() signal.

componentProperties?: Partial<{ [P in keyof T]: T[P] extends InputSignal<infer V> ? InputSignal<V> | WritableSignal<V> | V : T[P]}>

This way, specifying a string for an InputSignal<string> is a completely valid type.

Input Signal Behavior

Getting input() signals to work OOTB was a bit difficult, since we cannot create them outside of an angular context and there is no way to provide an injection context, even when setting an initial value. Because of this, Cypress handles input() signals when being injected into a component from the cypress context the following way:

  1. If a prop is an input() signal, but the value provided is a match of the generic type, we wrap the generic value in a writable signal() and merge that into the prop. In other words:
  cy.mount(TestComponentComponent, {
    componentProperties: {
      title: 'Test Component',
    },
  });

is really

  cy.mount(TestComponentComponent, {
    componentProperties: {
      title: signal('Test Component'),
    },
  });

This allows us to make a signal and avoid the ctx.title is not a function error mentioned in the issue while allowing the passing of primitives into the component to work.

Change spy for inputs

Since the prop in question is an input(), we do not propagate changes to titleChange or related output spies. In other words, this will not work:

  cy.mount(TestComponentComponent, {
    componentProperties: {
      title: 'Test Component',
      // @ts-expect-error
      titleChange: createOutputSpy('titleChange')
    },
  });

  // some action that changes the title...

  // this assertion will NEVER be true, even if you make `title` a signal. we will NEVER emit change events for props that are type `input()`
  cy.get('@titleChange').should('have.been.called');

Which brings up the question, how can I change or assert on input updates/changes?

  1. If a prop is an input() signal, but the value provided is a match of the generic type wrapped in a signal, we merge the value as is to allow for one-way data binding (more on this below).

Since input() in our case can also take a WritableSignal, we can just pass a signal() as a variable reference into the component and mutate the signal directly. This gives us the one-way binding we need to test input() signals outside the context of Angular.

  const myTitleSignal = signal('Test Component')
  cy.mount(TestComponentComponent, {
    componentProperties: {
      title: myTitleSignal
    },
  });

cy.get('the-title-element').should('have.text', 'Test Component')
cy.then(() => {
   // now set the input() through a signal to update the one-way binding
   myTitleSignal.set('FooBar')
})
// works
cy.get('the-title-element').should('have.text', 'FooBar')

Model Signal Behavior

Since model() signals are writable signals, they have the ability to support two-way data binding. This means Cypress handles model() signals the following way

  1. If a prop is an modal() signal, but the value provided is a match of the generic type, we set the generic value in a model() and merge that into the prop. In other words:
  cy.mount(TestComponentComponent, {
    componentProperties: {
      count: 1
    },
  });

cy.get('the-count-element').should('have.text', '1')
Change spy for models

Since the prop in question is an model() and is a WritableSignal, we WILL propagate changes to countChange if the output spy is created, either like the example below or if autoSpyOutputs: true is configured. However, the typing for countChange is not supported since this is technically not a prop (@Output() actually adds this as a prop which is not true in our case, even though the signal output is completely valid).

  cy.mount(TestComponentComponent, {
    componentProperties: {
      count: 4,
      // @ts-expect-error
      countChange: createOutputSpy('countChange')
    },
  });

  // some action that changes the count...

  // this assertion will be true
  cy.get('@countChange').should('have.been.called');

However, since count is a primitive in this case and outside the angular context, we CANNOT support two-way data-binding to this variable. In other words, these will not work:

  let count = 5
  cy.mount(TestComponentComponent, {
    componentProperties: {
      count,
      // @ts-expect-error
      countChange: createOutputSpy('countChange')
    },
  });

  // some action that changes the count to 7 inside the component

  cy.then(() => {
    // this assertion will never be true. Count will be 5 
    expect(count).to.be(7)
  })
  // However, the change spy WILL be called since the change occurred inside the component
  let count = 5
  cy.mount(TestComponentComponent, {
    componentProperties: {
      count,
      // @ts-expect-error
      countChange: createOutputSpy('countChange')
    },
  });

  cy.then(() => {
   count = 8
  })
   // this assertion will never be true. Count will be 5 
  cy.get('the-count-element`).should('have.text`, '8')
   // the change spy will also NOT be called

Which brings up the question, how can I have two-way data-binding set up?

  1. If a prop is an model() signal, but the value provided is a match of the generic type wrapped in a signal, we set the initial value and set up two-way data binding (more on this below). This means you can achieve two way data-binding in your component tests like such:
  let count = signal(5)
  cy.mount(TestComponentComponent, {
    componentProperties: {
      count
    },
  });

  // some action that changes the count to 7 inside the component

  cy.then(() => {
    // this assertion will be true
    expect(count()).to.be(7)
  })
  // Additionally, if registered, the change spy will also be called
  let count = signal(5)
  cy.mount(TestComponentComponent, {
    componentProperties: {
      count
    },
  });

  cy.then(() => {
   count.set(8)
  })
  // this assertion will be true
  cy.get('the-count-element').should('have.text', '8')
  // the change spy will also be called

  // later in the test , some action that changes the count to 14 inside the component
  // this assertion will be true
  cy.get('the-count-element').should('have.text', '14')

I am not an expert on Angular, but I think after reading the signals documentation this makes sense. Please let me know if anything seems off or if there are things we are missing or need to add/change! Once again we appreciate your patience as we work to get this support available.

@cypress-bot
Copy link
Contributor

cypress-bot bot commented Jul 1, 2024

Released in 13.13.0.

This comment thread has been locked. If you are still experiencing this issue after upgrading to
Cypress v13.13.0, please open a new issue.

@cypress-bot cypress-bot bot locked as resolved and limited conversation to collaborators Jul 1, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
CT Issue related to component testing npm: @cypress/angular @cypress/angular package issues type: feature New feature that does not currently exist
Projects
Status: Generally Available
8 participants