cypress
cypress copied to clipboard
Setting input signals with Angular component tests not supported
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:
not working example (mounting component):
working example (template):
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
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 anInputSignal<string>
-
count
is aModelSignal<number>
When mounting this component, you are passing a string
instead of an InputSignal<string>
, and a WritableSignal<number>
instead of a ModelSignal<number>
.
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.
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.
@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?
Dmytro Mezhenskyi from Decoded Frontend suggested a solution with a TestHost Component that sets the inputs, which works with Cypress:
https://youtu.be/U8YXaWwyd9k?si=fR4Pd5LErMGztCZ4&t=720
@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.
@Waterstraal absolutely! I mentioned this as a workaround for the time being until this is natively supported by Cypress.
I am sorry for closing this prematurely. Angular is not my area of expertise, and I did not fully understand the issue at hand.
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 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!
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 https://github.com/cypress-io/cypress/pull/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:
- 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 writablesignal()
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?
- 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
- If a prop is an
modal()
signal, but the value provided is a match of the generic type, we set the generic value in amodel()
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?
- 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.
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.