Angular 示例
你将学习到
- 如何挂载Angular组件
- 如何向Angular组件传递数据
- 如何测试Angular组件的多种场景
- 如何测试Angular信号量
- 如何为Angular自定义
cy.mount()
方法
挂载组件
使用 cy.mount()
要使用cy.mount()
挂载组件,需导入组件并传递给该方法:
import { StepperComponent } from './stepper.component'
it('mounts', () => {
cy.mount(StepperComponent)
})
向组件传递数据
可以通过在选项中的componentProperties设置输入和输出:
cy.mount(StepperComponent, {
componentProperties: {
count: 100,
change: new EventEmitter(),
},
})
测试事件处理器
将Cypress的spy传递给事件属性并验证其调用:
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
,可在选项中配置(类似于在应用中的ngModule
设置):
注意:imports
、declarations
和providers
选项不适用于standalone
组件,因为它们已在测试ngModule
中设置。这是Angular 19的默认行为。
cy.mount(ComponentThatFetchesData, {
imports: [HttpClientModule],
declarations: [ButtonComponent],
providers: [DataService],
})
参见默认声明、提供者或导入,在自定义cy.mount()
命令中设置通用选项,避免为每个测试重复样板代码。
使用独立组件
不仅支持独立组件,它们还是编写测试最简单的组件。独立组件通过其@Component()
装饰器为Angular编译器提供所需的一切。这意味着在大多数情况下,独立组件无需提供任何imports
、decorators
或providers
即可挂载。挂载变得非常简单:
cy.mount(MyStandaloneComponent)
使用Angular模板语法
cy.mount()
方法也支持在挂载组件时使用Angular模板语法。一些开发者可能更喜欢这种方式而非基于对象的挂载风格:
cy.mount(`<app-stepper [count]="100"></app-stepper>`, {
declarations: [StepperComponent],
})
使用模板语法时,组件需添加到选项参数的declarations中。
结合事件发射器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');
访问组件实例
有时可能需要直接在测试中访问组件实例。使用.then()
可以处理cy.mount()
命令返回的主题。此时mount返回一个包含渲染组件和fixture的对象。
以下示例中,我们直接在组件上对change
事件发射器进行spy。
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)
})
使用createOutputSpy()
为了更轻松地对事件发射器进行spy,提供了createOutputSpy()
工具函数,可自动创建EventEmitter
并在其.emit()
方法上设置spy。用法如下:
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')
})
使用autoSpyOutputs
可能会发现需要为每个组件输出重复创建cy.spy()
。为此,我们创建了一个简便机制。通过将autoSpyOutputs
标志传入MountConfig
即可启用此功能。组件挂载后,可以使用@Output()
属性名+Spy
访问生成的spy。例如,change
属性可通过别名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)
})
autoSpyOutput
标志仅在向mount函数传递组件时有效。目前不支持模板语法。
autoSpyOutput
是实验性功能,未来可能被移除或更改
信号量
随着Angular版本17.1和17.2的发布,input和model信号量被引入到@angular/core
API中。虽然基础信号量在Angular 16
中引入,但使用所有信号量需要Angular 17.2
及以上版本。
Cypress 14直接在cypress/angular
测试工具中支持信号量。
以下示例中,我们将使用一个非常简单的组件TestComponent
,如下所示:
// 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>
测试信号量
在Cypress组件测试中有两种测试信号量的方法:
推断泛型类型
以下示例中,传递给TestComponent
的title
属性是string
类型。string
是我们在TestComponent
中定义的input()
信号量的泛型类型。
let titleProp = 'Test Component'
cy.mount(TestComponent, {
componentProperties: {
title: titleProp,
},
})
cy.get('[data-cy="test-component-title-display"]').should(
'have.text',
'Test Component'
)
在底层,Cypress将泛型值包装在可写的signal()
中并合并到属性中。换句话说,此示例实际上是:
cy.mount(TestComponent, {
componentProperties: {
title: signal('Test Component'),
},
})
这适用于任何信号量。以下示例展示了如何使用泛型类型number
测试model()
信号量,如我们的TestComponent
所示:
cy.mount(TestComponent, {
componentProperties: {
title: 'Test Component',
count: 3,
},
})
cy.get('[data-cy="test-component-count-display"]').should('have.text', '3')
可写信号量
推断泛型类型在大多数测试场景中非常有效。然而,它们 不允许我们在属性传入后更新组件中的属性。为此用例,我们需要使用可写的signal()
。
这允许我们测试input()
信号量的单向数据绑定。
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(() => {
// 现在通过信号量设置input()以更新单向绑定
myTitlePropAsSignal.set('FooBar')
})
cy.get('[data-cy="test-component-title-display"]').should('have.text', 'FooBar')
以及model()
信号量的双向数据绑定。
let count = signal(5)
cy.mount(TestComponent, {
componentProperties: {
title: 'Test Component',
count,
},
})
cy.then(() => {
// 现在通过信号量设置model()以更新组件中的绑定
count.set(8)
})
cy.get('[data-cy="test-component-count-display"]').should('have.text', '8')
// 组件内发生某些操作将计数更改为9,这会更新测试中的绑定
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)
})
变更Spy
Cypress不会通过spy从input()
信号量传播变更。
对于可写信号量,如model()
或signal()
,如果创建了以属性名加Change
后缀的输出spy,Cypress会传播变更。以下示例中,countChange
将spycount
信号量的变更。
cy.mount(TestComponent, {
componentProperties: {
title: 'Test Component',
count: 4,
// @ts-expect-error
countChange: createOutputSpy('countChange'),
},
})
// 发生某些操作更改计数
cy.get('[data-cy="test-component-count-incr"]').click()
cy.get('@countChange').should('have.been.called')
如果配置了autoSpyOutputs: true
,这些spy会自动创建。此时后缀将为ChangeSpy
。
自定义挂载命令
自定义 cy.mount()
默认情况下,cy.mount()
是mount()
的简单传递,但可以自定义cy.mount()
以满足需求。例如,可能会发现挂载过程中有重复工作。为了减少样板代码,创建自定义mount命令可能很有用。
默认声明、提供者或导入
如果发现在单个测试中注册了大量declarations、providers或imports,建议在自定义cy.mount()
命令中进行。通常这对所有测试的开销很小,有助于保持规范代码的整洁。
以下示例注册了几个默认组件声明,同时仍允许通过config参数传递额外的声明。相同的模式也可应用于providers和模块导入。
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)
此自定义mount命令将允许跳过在每个cy.mount()
调用中手动传递ButtonComponent
和CardComponent
作为declarations。
autoSpyOutputs
以下是为每个挂载的组件默认设置autoSpyOutputs
的示例:
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,
})
}
)
autoSpyOutput
标志仅在向mount函数传递组件时有效。目前不支持模板语法。