Cypress Storybook
这个库包含用于集成 Cypress 和 Storybook 的辅助方法。它包含有用的 Cypress 命令,用于加载故事(stories),而无需完全重新加载应用程序,从而使测试/规范运行速度大大加快。
安装
npm install cypress-storybook --save-dev
安装后,Cypress 和 Storybook 都需要配置才能工作。Storybook 的安装将基于所使用的框架(目前支持 React 和 Angular)。
Cypress
以下内容将添加 Cypress 命令,使其在 Cypress 规范文件中可用
// cypress/support/index.js or .ts
import 'cypress-storybook/cypress'
请确保您 cypress.json
文件中的 baseUrl
指向您的 Storybook。对于开发环境,这很可能是 http://localhost:6006
。
{
"baseUrl": "http://localhost:6006"
}
如果在 CI 流程中运行这些测试,这个基本 URL 必须指向 CI 可以访问 Storybook 页面的位置。
如果您的项目同时包含 Storybook 和真正的端到端 Cypress 测试,您可能需要为每个运行环境使用单独的 cypress.json
文件。Cypress 命令允许您指定使用哪个配置文件:https://docs.cypress.io/guides/guides/command-line.html#cypress-open。例如,您可能需要执行类似 cypress open --config-file cypress-storybook.json
的命令。您可以在 npm
脚本中将其别名化,例如 npm run cypress:storybook:open
。
Storybook
有针对不同框架的适配器。目前支持 React 和 Angular。
以下内容将设置 Storybook 应用程序以理解 Cypress 命令。它将在 Storybook 用于故事(stories)的 iframe 的 window
上注册隐藏函数
React Storybook
// .storybook/config.js (v5) or .storybook/preview.js (v6)
import 'cypress-storybook/react'
Angular Storybook
// .storybook/config.js (v5) or .storybook/preview.js (v6)
import 'cypress-storybook/angular'
使用方法
Storybook 是一个出色的 UI 开发工具。它鼓励将 UI 开发与后端开发分离。它还鼓励构建更小的组件。Cypress 可以用于测试或指定这些组件的行为。网上的许多示例展示了加载主要的 Storybook 应用程序,然后使用 Cypress 点击导航以启用正确的故事(story)。这种方法的问题在于故事位于 iframe 中,这使得操作更加困难。Storybook 带有一个路由器,允许您直接访问故事。如果您将故事展开到全屏,您会看到 URL。它包含类似 iframe.html?id=button--text
的内容。
这个库的工作原理是加载 iframe.html
,这个页面是空白的,因为没有指定故事(story)。稍后使用 Storybook 路由 API 通过它们的标识符来卸载和挂载/重新挂载故事。加载故事不需要刷新故事页面(iframe.html
)。前一个故事会从 DOM 中卸载,下一个故事会通过 Storybook 路由 API 请求。挂载一个故事只需要几毫秒,而重新加载整个页面需要几秒钟。这使得测试速度更快。
这个库只有在故事(Stories)不留下全局状态时才能正常工作。建议您的故事提供它们自己的状态。如果您使用像 Redux 这样的全局状态管理库,请确保每个故事都有自己的状态提供者(store provider),以便为每个故事创建状态。
Controls/Args(控件/参数)
支持 Args。可以在所有属性都是控件的情况下使用 Args。更改一个 Arg 会自动更新故事(story)。控件隐式地使用 Actions 插件(参见下面的 Actions)。
示例
cy.changeArg('buttonText', 'New Text Value')
Knobs(旋钮)
支持 Knobs。可以创建一个故事(story),其中所有属性都是从 knob 导入的,并在测试期间更改这些输入。更改旋钮会刷新故事,清除之前对故事所做的任何更改。请务必在测试开始时更改旋钮。
示例
cy.changeKnob('buttonText', 'New Text Value')
Actions(操作)
支持 action 插件,它将返回 Sinon Spies。任何可以针对 Sinon spy 进行的断言都可以针对 action 进行。action 的参数将是调用 action 时传入的参数。例如
// in a story
export const MyStory = () => {
return (
<>
<button id="button1" onClick={action('click1')}>
Button 1
</button>
<button id="button2" onClick={() => action('click2')('foo')}>
Button 2
</button>
</>
)
}
// in a test
it('should trigger the action', () => {
cy.get('#button1').click()
cy.storyAction('click1').should('have.been.called') // called with a click event
cy.get('#button2').click()
cy.storyAction('click2').should('have.been.calledWith', 'foo') // called with arguments passed
})
使用 Args 的 Actions
// Story
export default {
title: 'Button',
component: Button,
argTypes: { onClick: { action: 'clicked' } },
}
const Template = (props) => <Button {...props} />
export const Controls = Template.bind({})
Controls.args = {
children: 'Button',
}
// Cypress
it('should trigger the action', () => {
cy.get('button').click()
cy.storyAction('clicked').should('have.been.called')
})
一个 Cypress 示例文件可能如下所示
describe('Button', () => {
// Note the use of `before`
before(() => {
// Visit the storybook iframe page once per file
cy.visitStorybook()
})
// Note the use of `beforeEach`
beforeEach(() => {
// The first parameter is the category. This is the `title` in CSF or the value in `storiesOf`
// The second parameter is the name of the story. This is the name of the function in CSF or the value in the `add`
// This does not refresh the page, but will unmount any previous story and use the Storybook Router API to render a fresh new story
cy.loadStory('Button', 'Text')
})
it('should change the knob', () => {
// first parameter is the name of the knob
// second parameter is the value of the knob
cy.changeKnob('buttonText', 'New Text Value')
cy.get('button').should('have.text', 'New Text Value')
})
it('should change the Arg', () => {
// first parameter is the name of the Arg
// second parameter is the value of the Arg
cy.changeArg('buttonText', 'New Text Value')
cy.get('button').should('have.text', 'New Text Value')
})
it('should fire the click action', () => {
cy.get('button').click()
// first parameter is the action name - returns a spy for assertions
cy.storyAction('click').should('have.been.called')
})
})
TypeScript 支持
本项目包含类型定义。如果您的项目使用 TypeScript,并且 cypress/support/commands
文件是 *.ts
文件,并且 cypress/tsconfig.json
已设置为包含 cypress
目录中的所有 TS 文件,那么无需额外操作即可在 Cypress 文件中获取类型定义。如果类型定义没有自动为您设置,您需要在 TS 配置文件中添加以下内容
{
"compilerOptions": {
"types": ["cypress", "cypress-storybook/cypress"]
}
}
Cypress 12+
从 Cypress 12 开始,testIsolation 的默认值更改为 true。为了让本库能够使用 before 块只访问 Storybook 一次,您需要在 e2e 块下更新您的 cypress.config.ts
文件以包含 testIsolation: false
。