原文地址:How to Do Visual Regression Testing in Vue with Vitest?,作者是Alex,一位来自德国的开发人员。在 25 年的 Vue Nation
上有演讲。作者在 X 上很活跃,有条件可以去那里看看。
TL;DR: 视觉回归测试通过比较屏幕截图来检测意外的 UI 更改。借助 Vitest 的实验性浏览器模式和 Playwright,您可以:
- 在真实浏览器环境中运行测试
- 为不同状态定义组件 stories
- 捕获屏幕截图并使用快照测试将其与基线图像进行比较
在本指南中,您将学习如何使用 Vitest 为 Vue 组件设置视觉回归测试。
我们的测试将生成以下截图:
视觉回归测试会捕获 UI 组件的屏幕截图,并将其与基线图像进行比较,以标记视觉差异。这可确保整个设计系统的样式和布局保持一致。
Vitest 配置
首先使用 Vue 插件配置 Vitest:
import vue from '@vitejs/plugin-vue' import { defineConfig } from 'vitest/config' export default defineConfig({ plugins: [vue()], })
设置浏览器测试
视觉回归测试需要真实的浏览器环境。安装以下依赖项:
npm install -D vitest @vitest/browser playwright
您也可以使用以下命令来初始化浏览器模式:
npx vitest init browser
原作者没有提到的需要安装的包
npm i -D jsdom @types/jsdom @vue/test-utils
首先,使用工作区文件vitest.workspace.ts
配置 Vitest 以支持单元测试和浏览器测试。有关工作区配置的更多详细信息,请参阅Vitest 工作区文档。
使用工作区配置,您可以为单元测试和浏览器测试保留单独的设置,同时共享通用配置。这样可以更轻松地管理项目中的不同测试环境。
import { defineWorkspace } from 'vitest/config' export default defineWorkspace([ { extends: './vitest.config.ts', test: { name: 'unit', include: ['**/*.spec.ts', '**/*.spec.tsx'], exclude: ['**/*.browser.spec.ts', '**/*.browser.spec.tsx'], environment: 'jsdom', }, }, { extends: './vitest.config.ts', test: { name: 'browser', include: ['**/*.browser.spec.ts', '**/*.browser.spec.tsx'], browser: { enabled: true, provider: 'playwright', headless: true, instances: [{ browser: 'chromium' }], }, }, }, ])
在package.json
中添加脚本
{ "scripts": { "test": "vitest", "test:unit": "vitest --project unit", "test:browser": "vitest --project browser", }, }
现在我们可以在单独的环境中运行测试,如下所示:
npm run test:unit npm run test:browser
BaseButton 组件
将BaseButton.vue
组件视为一个可重复使用的按钮,具有可自定义的大小、变体和禁用状态:
完整代码查看github
<script setup lang="ts"> interface Props { size?: 'small' | 'medium' | 'large' variant?: 'primary' | 'secondary' | 'outline' disabled?: boolean } defineProps<Props>() defineEmits<{ (e: 'click', event: MouseEvent): void }>() </script> <template> <button class="button" :class="[ `button--${size}`, `button--${variant}`, { 'button--disabled': disabled }, ]" :disabled="disabled" @click="$emit('click', $event)" > <slot /> </button> </template> <style scoped lang="scss"> .button { display: inline-flex; align-items: center; justify-content: center; /* Additional styling available in the GitHub repository */ } /* Size, variant, and state modifiers available in the GitHub repository */ </style>
定义测试故事
创建“故事”来展示不同的按钮配置:
const buttonStories = [ { name: 'Primary Medium', props: { variant: 'primary', size: 'medium' }, slots: { default: 'Primary Button' }, }, { name: 'Secondary Medium', props: { variant: 'secondary', size: 'medium' }, slots: { default: 'Secondary Button' }, }, // and much more ... ]
每个故事都定义了一个名称、道具和槽点内容。
为渲染故事截图
在一个容器中渲染所有故事以捕获全面的屏幕截图:
import type { Component } from 'vue' interface Story<T> { name: string props: Record<string, any> slots: Record<string, string> } function renderStories<T>(component: Component, stories: Story<T>[]): HTMLElement { const container = document.createElement('div') container.style.display = 'flex' container.style.flexDirection = 'column' container.style.gap = '16px' container.style.padding = '20px' container.style.backgroundColor = '#ffffff' stories.forEach((story) => { const storyWrapper = document.createElement('div') const label = document.createElement('h3') label.textContent = story.name storyWrapper.appendChild(label) const { container: storyContainer } = render(component, { props: story.props, slots: story.slots, }) storyWrapper.appendChild(storyContainer) container.appendChild(storyWrapper) }) return container }
编写可视化回归测试
编写一个测试来呈现故事并捕获屏幕截图:
import type { Component } from 'vue' import { page } from '@vitest/browser/context' import { describe, expect, it } from 'vitest' import { render } from 'vitest-browser-vue' import BaseButton from '../BaseButton.vue' // [buttonStories and renderStories defined above] describe('BaseButton', () => { describe('visual regression', () => { it('should match all button variants snapshot', async () => { const container = renderStories(BaseButton, buttonStories) document.body.appendChild(container) const screenshot = await page.screenshot({ path: 'all-button-variants.png', }) // this assertion is acutaly not doing anything // but otherwise you would get a warning about the screenshot not being taken expect(screenshot).toBeTruthy() document.body.removeChild(container) }) }) })
使用 vitest-browser-vue
的 render
来捕获在真实浏览器中出现的组件。
使用.browser.spec.ts
扩展名(例如BaseButton.browser.spec.ts
)保存此文件以匹配您的浏览器测试配置。
超越截图:自动比较
通过以 base64 形式编码屏幕截图并将其与基线快照进行比较来自动进行图像比较:
// Helper function to take and compare screenshots async function takeAndCompareScreenshot(name: string, element: HTMLElement) { const screenshotDir = './__screenshots__' const snapshotDir = './__snapshots__' const screenshotPath = `${screenshotDir}/${name}.png` // Append element to body document.body.appendChild(element) // Take screenshot const screenshot = await page.screenshot({ path: screenshotPath, base64: true, }) // Compare base64 snapshot await expect(screenshot.base64).toMatchFileSnapshot(`${snapshotDir}/${name}.snap`) // Save PNG for reference await expect(screenshot.path).toBeTruthy() // Cleanup document.body.removeChild(element) }
然后更新测试:
describe('BaseButton', () => { describe('visual regression', () => { it('should match all button variants snapshot', async () => { const container = renderStories(BaseButton, buttonStories) await expect( takeAndCompareScreenshot('all-button-variants', container) ).resolves.not.toThrow() }) }) })
Vitest 正在讨论浏览器模式下的原生屏幕截图比较。请关注github.com/vitest-dev/vitest/discussions/690并做出贡献。
小结
Vitest 的实验性浏览器模式使开发人员能够在真实的浏览器环境中对 Vue 组件进行准确的视觉回归测试。虽然当前的工作流程需要手动审查屏幕截图比较,但它为未来更自动化的视觉测试奠定了基础。这种方法还加强了开发人员和 UI 设计师之间的协作。设计师可以通过访问组件库中生成的屏幕截图,在生产部署之前查看组件的视觉变化。对于高级视觉测试功能,团队应该探索 Playwright 或 Cypress 等提供更多功能和成熟度的专用工具。请记住对您的基本组件执行视觉回归测试。