Angular Examples
What you'll learn​
- How to mount an Angular component
- How to pass data to an Angular component
- How to test multiple scenarios of Angular components
- How to test Angular signals
- How to customize the
cy.mount()
for Angular
Mounting Components​
Using cy.mount()
​
To mount a component with cy.mount()
, import the component and pass it to the
method:
import { StepperComponent } from './stepper.component'
it('mounts', () => {
cy.mount(StepperComponent)
})
Passing Data to a Component​
You can pass inputs and outputs to a component by setting componentProperties in the options:
cy.mount(StepperComponent, {
componentProperties: {
count: 100,
change: new EventEmitter(),
},
})
Testing Event Handlers​
Pass a Cypress spy to an event prop and validate it was called:
it('clicking + fires a change event with the incremented value', () => {
cy.mount(StepperComponent, {
componentProperties: {
change: createOutputSpy('changeSpy'),
},
})
cy.get('[data-cy=increment]').click()
cy.get('@changeSpy').should('have.been.calledWith', 1)
})
Imports/Declarations/Providers​
If you need to set up any additional imports
, declarations
, or providers
for your component to mount successfully, you can set them in the options
(similar to setting them up in ngModule
in a app):
cy.mount(ComponentThatFetchesData, {
imports: [HttpClientModule],
declarations: [ButtonComponent],
providers: [DataService],
})
See
Default Declarations, Providers, or Imports
to set up common options in a custom cy.mount()
command to avoid having to
repeat this boilerplate for each test.
Using Standalone​
Not only are Standalone Components supported, they are the simplest components to write tests for.
Standalone Components provide the Angular compiler with everything it needs to compile through its @Component()
decorator.
This means that in most cases a Standalone Component can be mounted without ever providing any imports
, decorators
, or providers
. Mounting then becomes as simple as:
cy.mount(MyStandaloneComponent)
Using Angular Template Syntax​
The cy.mount()
method also supports the Angular template syntax when mounting
a component. Some developers might prefer this approach to the object based
mount style:
cy.mount(`<app-stepper [count]="100"></app-stepper>`, {
declarations: [StepperComponent],
})
When using template syntax, the component needs to added to the declarations in the options parameter.
Using with event emitter spy:
cy.mount('<app-button (click)="onClick.emit($event)">Click me</app-button>', {
declarations: [ButtonComponent]
componentProperties: {
onClick: createOutputSpy('onClickSpy'),
},
})
cy.get('button').click();
cy.get('@onClickSpy').should('have.been.called');
Accessing the Component Instance​
There might be times when you might want to access the component instance
directly in your tests. To do so, use .then()
, which enables us to work with
the subject that was yielded from the cy.mount()
command. In this case, mount
yields an object that contains the rendered component and the fixture.
In the below example, we use the component to spy directly on the change
event
emitter.
it('clicking + fires a change event with the incremented value', () => {
cy.mount(
'<app-stepper count="100" (change)="change.emit($event)"></app-stepper>',
{
componentProperties: { change: new EventEmitter() },
declarations: [StepperComponent],
}
).then((wrapper) => {
console.log({ wrapper })
cy.spy(wrapper.component.change, 'emit').as('changeSpy')
return cy.wrap(wrapper).as('angular')
})
cy.get(incrementSelector).click()
cy.get('@changeSpy').should('have.been.calledWith', 101)
})
Using createOutputSpy()​
To make spying on event emitters easier, there is a utility function called
createOutputSpy()
which can be used to automatically create an EventEmitter
and setup the spy on it's .emit()
method. It can be used like the following:
import { createOutputSpy } from 'cypress/angular'
it('clicking + fires a change event with the incremented value', () => {
// Arrange
cy.mount('<app-stepper (change)="change.emit($event)"></app-stepper>', {
declarations: [StepperComponent],
componentProperties: {
change: createOutputSpy<boolean>('changeSpy'),
},
})
cy.get(incrementSelector).click()
cy.get('@changeSpy').should('have.been.called')
})
Using autoSpyOutputs​
You might find yourself repeatedly creating a cy.spy()
for each of your
component outputs. Because of this, we created an easy mechanism to handle this
for you. This feature can be turned on by passing the autoSpyOutputs
flag into
MountConfig
. After the component has been mounted you can then access each of
the generated spies using the @Output()
property name + Spy
. So our change
property can be accessed via its alias of cy.get('@changeSpy')
it('clicking + fires a change event with the incremented value', () => {
cy.mount(StepperComponent, {
autoSpyOutputs: true,
componentProperties: {
count: 100,
},
})
cy.get(incrementSelector).click()
cy.get('@changeSpy').should('have.been.calledWith', 101)
})
The autoSpyOutput
flag only works when passing in a component to the mount
function. It currently does not work with the template syntax.
autoSpyOutput
is an experimental feature and could be removed or changed
in the future
Signals​
With the releases of Angular versions 17.1 and 17.2, input and model signals were introduced into the @angular/core
API.
Since signals introduced new methods and types to the core API, Cypress introduced a new test harness, @cypress/angular-signals
.
Though basic signals were introduced in Angular 16
, this testing harness requires Angular 17.2
and above.
For the below examples, we'll be working with a very simple component called TestComponent
, which looks something like shown below:
// app/components/test-component.component.ts
import { Component, input, model } from '@angular/core'
@Component({
selector: 'test-component',
templateUrl: './test-component.component.html',
standalone: true,
})
export class TestComponent {
title = input.required<string>()
count = model<number>(1)
}
<!-- app/components/test-component.component.html -->
<p data-cy="test-component-title-display">{{ title() }}</p>
<p data-cy="test-component-count-display">{{ count() }}</p>
<button data-cy="test-component-count-incr" (click)="count.set(count() + 1)">
Increase
</button>
<button data-cy="test-component-count-decr" (click)="count.set(count() - 1)">
Decrease
</button>
Testing Signals​
There are two ways to test signals within Cypress Component Testing:
Inferred Generic Type​
In the example below, the title
prop being passed into our TestComponent
is a string
. A string
is the generic type of our input()
signal we defined in our TestComponent
.
let titleProp = 'Test Component'
cy.mount(TestComponent, {
componentProperties: {
title: titleProp,
},
})
cy.get('[data-cy="test-component-title-display"]').should(
'have.text',
'Test Component'
)
Under the hood, Cypress wraps the generic value in a writable signal()
and merges it into the prop. In other words, the @cypress/angular-signals
test harness in this example is really:
cy.mount(TestComponent, {
componentProperties: {
title: signal('Test Component'),
},
})
This works for any signal. Shown below is an example of testing a model()
signal with a generic type number
as seen in our TestComponent
:
cy.mount(TestComponent, {
componentProperties: {
title: 'Test Component',
count: 3,
},
})
cy.get('[data-cy="test-component-count-display"]').should('have.text', '3')
Writable Signal​
Inferred generic types work very well for most test cases. However, they don't allow us to update the prop in the component after the prop is passed in. For this use case, we need to use a writable signal()
.
This allows us to test our one-way data binding for our input()
signals.
const myTitlePropAsSignal = signal('Test Component')
cy.mount(TestComponent, {
componentProperties: {
title: myTitlePropAsSignal,
},
})
cy.get('[data-cy="test-component-title-display"]').should(
'have.text',
'Test Component'
)
cy.then(() => {
// now set the input() through a signal to update the one-way binding
myTitlePropAsSignal.set('FooBar')
})
cy.get('[data-cy="test-component-title-display"]').should('have.text', 'FooBar')
And our two-way data binding for our model()
signals.
let count = signal(5)
cy.mount(TestComponent, {
componentProperties: {
title: 'Test Component',
count,
},
})
cy.then(() => {
// now set the model() through a signal to update the binding in the component
count.set(8)
})
cy.get('[data-cy="test-component-count-display"]').should('have.text', '8')
// some action occurs that changes the count to 9 inside the component, which updates the binding in our test
cy.get('[data-cy="test-component-count-incr"]').click()
cy.get('[data-cy="test-component-count-display"]').should('have.text', '9')
cy.then(() => {
expect(count()).to.equal(9)
})
Change Spies​
Cypress doesn't propagate changes via spy from input()
signals.
For writable signals, such as model()
s or signal()
s, Cypress will propagate changes if an output spy is created with the prop's name suffixed with Change
. In the example below,
countChange
will spy on changes to the count
signal.
cy.mount(TestComponent, {
componentProperties: {
title: 'Test Component',
count: 4,
// @ts-expect-error
countChange: createOutputSpy('countChange'),
},
})
// some action occurs that changes the count
cy.get('[data-cy="test-component-count-incr"]').click()
cy.get('@countChange').should('have.been.called')
These spies can automatically be created if autoSpyOutputs: true
is configured. The suffix in this case will be ChangeSpy
.
Custom Mount Commands​
Customizing cy.mount()
​
By default, cy.mount()
is a simple passthrough to mount()
, however, you can
customize cy.mount()
to fit your needs. For instance, you may find yourself
doing repetitive work during mounting. In order to reduce boilerplate you may
find it useful to create a custom mount command.
Default Declarations, Providers, or Imports​
If you find yourself registering a bunch of declarations, providers, or imports
in your individual tests, we recommend doing them all within a custom
cy.mount()
command. The overhead is usually minimal for all your tests and it
helps keep your spec code clean.
Below is a sample that registers several default component declarations while still allowing additional ones to be passed in via the config param. The same pattern can also be applied to providers and module imports.
import { Type } from '@angular/core'
import { mount, MountConfig } from 'cypress/angular'
import { ButtonComponent } from 'src/app/button/button.component'
import { CardComponent } from 'src/app/card/card.component'
declare global {
namespace Cypress {
interface Chainable {
mount: typeof customMount
}
}
}
const declarations = [ButtonComponent, CardComponent]
function customMount<T>(component: string | Type<T>, config?: MountConfig<T>) {
if (!config) {
config = { declarations }
} else {
config.declarations = [...(config?.declarations || []), ...declarations]
}
return mount<T>(component, config)
}
Cypress.Commands.add('mount', customMount)
This custom mount command will allow you to skip manually passing in the
ButtonComponent
and CardComponent
as declarations into each cy.mount()
call.
autoSpyOutputs​
Here is an example of defaulting autoSpyOutputs
for every mounted component:
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}
Cypress.Commands.add(
'mount',
(component: Type<unknown> | string, config: MountConfig<T>) => {
return mount(component, {
...config,
autoSpyOutputs: true,
})
}
)
The autoSpyOutput
flag only works when passing in a component to the mount
function. It currently does not work with the template syntax.