[{"data":1,"prerenderedAt":351},["ShallowReactive",2],{"content:\u002Fposts\u002F2025\u002Fhow-to-do-visual-regression-testing-in-vue-with-vitest":3,"surround:\u002Fposts\u002F2025\u002Fhow-to-do-visual-regression-testing-in-vue-with-vitest":340},{"id":4,"title":5,"body":6,"canonical":19,"categories":311,"date":313,"description":44,"draft":314,"extension":315,"image":316,"meta":317,"navigation":321,"path":322,"permalink":316,"readingTime":323,"recommend":316,"references":316,"seo":328,"sitemap":329,"stem":333,"tags":334,"type":316,"updated":338,"__hash__":339},"content\u002Fposts\u002F2025\u002Fhow-to-do-visual-regression-testing-in-vue-with-vitest.md","【翻译】如何使用 Vitest 在 Vue 中进行视觉回归测试？",{"type":7,"value":8,"toc":299},"minimark",[9,23,26,29,42,45,48,55,61,66,69,81,84,87,95,98,104,115,128,133,139,146,154,157,163,167,174,182,190,193,196,203,206,209,212,218,221,224,230,241,254,258,261,267,270,276,287,293,296],[10,11,12],"auths-alex",{},[13,14,15,16],"p",{},"原文地址：",[17,18,22],"a",{"href":19,"rel":20},"https:\u002F\u002Falexop.dev\u002Fposts\u002Fvisual-regression-testing-with-vue-and-vitest-browser\u002F",[21],"nofollow","How to Do Visual Regression Testing in Vue with Vitest?",[24,25],"hr",{},[13,27,28],{},"TL;DR: 视觉回归测试通过比较屏幕截图来检测意外的 UI 更改。借助 Vitest 的实验性浏览器模式和 Playwright，您可以：",[30,31,32,36,39],"ul",{},[33,34,35],"li",{},"在真实浏览器环境中运行测试",[33,37,38],{},"为不同状态定义组件 stories",[33,40,41],{},"捕获屏幕截图并使用快照测试将其与基线图像进行比较",[13,43,44],{},"在本指南中，您将学习如何使用 Vitest 为 Vue 组件设置视觉回归测试。",[13,46,47],{},"我们的测试将生成以下截图：",[13,49,50],{},[51,52],"img",{"alt":53,"src":54},"生成不同状态和样式的所有按钮变体的屏幕截图","https:\u002F\u002Fbitmc.uno\u002Fpicgo\u002F67c3caa86ee7e-78.webp",[56,57,58],"alert",{},[13,59,60],{},"视觉回归测试会捕获 UI 组件的屏幕截图，并将其与基线图像进行比较，以标记视觉差异。这可确保整个设计系统的样式和布局保持一致。",[62,63,65],"h2",{"id":64},"vitest-配置","Vitest 配置",[13,67,68],{},"首先使用 Vue 插件配置 Vitest：",[70,71,78],"pre",{"className":72,"code":74,"filename":75,"language":76,"meta":77},[73],"language-ts","import vue from '@vitejs\u002Fplugin-vue'\nimport { defineConfig } from 'vitest\u002Fconfig'\n\nexport default defineConfig({\n    plugins: [vue()],\n})\n","vitest.config.ts","ts","",[79,80,74],"code",{"__ignoreMap":77},[62,82,83],{"id":83},"设置浏览器测试",[13,85,86],{},"视觉回归测试需要真实的浏览器环境。安装以下依赖项：",[70,88,93],{"className":89,"code":91,"language":92,"meta":77},[90],"language-shell","npm install -D vitest @vitest\u002Fbrowser playwright\n","shell",[79,94,91],{"__ignoreMap":77},[13,96,97],{},"您也可以使用以下命令来初始化浏览器模式：",[70,99,102],{"className":100,"code":101,"language":92,"meta":77},[90],"npx vitest init browser\n",[79,103,101],{"__ignoreMap":77},[56,105,106,109],{},[13,107,108],{},"原作者没有提到的需要安装的包",[70,110,113],{"className":111,"code":112,"language":92,"meta":77},[90],"npm i -D jsdom @types\u002Fjsdom @vue\u002Ftest-utils\n",[79,114,112],{"__ignoreMap":77},[13,116,117,118,121,122,127],{},"首先，使用工作区文件",[79,119,120],{"code":120},"vitest.workspace.ts","配置 Vitest 以支持单元测试和浏览器测试。有关工作区配置的更多详细信息，请参阅",[17,123,126],{"href":124,"rel":125},"https:\u002F\u002Fcn.vitest.dev\u002Fguide\u002Fworkspace",[21],"Vitest 工作区文档","。",[56,129,130],{},[13,131,132],{},"使用工作区配置，您可以为单元测试和浏览器测试保留单独的设置，同时共享通用配置。这样可以更轻松地管理项目中的不同测试环境。",[70,134,137],{"className":135,"code":136,"filename":120,"language":76,"meta":77},[73],"import { defineWorkspace } from 'vitest\u002Fconfig'\n\nexport default defineWorkspace([\n    {\n        extends: '.\u002Fvitest.config.ts',\n        test: {\n            name: 'unit',\n            include: ['**\u002F*.spec.ts', '**\u002F*.spec.tsx'],\n            exclude: ['**\u002F*.browser.spec.ts', '**\u002F*.browser.spec.tsx'],\n            environment: 'jsdom',\n        },\n    },\n    {\n        extends: '.\u002Fvitest.config.ts',\n        test: {\n            name: 'browser',\n            include: ['**\u002F*.browser.spec.ts', '**\u002F*.browser.spec.tsx'],\n            browser: {\n                enabled: true,\n                provider: 'playwright',\n                headless: true,\n                instances: [{ browser: 'chromium' }],\n            },\n        },\n    },\n])\n",[79,138,136],{"__ignoreMap":77},[13,140,141,142,145],{},"在",[79,143,144],{"code":144},"package.json","中添加脚本",[70,147,152],{"className":148,"code":150,"filename":144,"language":151,"meta":77},[149],"language-json","{\n  \"scripts\": {\n    \"test\": \"vitest\",\n    \"test:unit\": \"vitest --project unit\",\n    \"test:browser\": \"vitest --project browser\",\n  },\n}\n","json",[79,153,150],{"__ignoreMap":77},[13,155,156],{},"现在我们可以在单独的环境中运行测试，如下所示：",[70,158,161],{"className":159,"code":160,"language":92,"meta":77},[90],"npm run test:unit\nnpm run test:browser\n",[79,162,160],{"__ignoreMap":77},[62,164,166],{"id":165},"basebutton-组件","BaseButton 组件",[13,168,169,170,173],{},"将",[79,171,172],{"code":172},"BaseButton.vue","组件视为一个可重复使用的按钮，具有可自定义的大小、变体和禁用状态：",[13,175,176,177],{},"完整代码查看",[17,178,181],{"href":179,"rel":180},"https:\u002F\u002Fgithub.com\u002Falexanderop\u002Fvitest-oc\u002Fblob\u002Fmain\u002Fsrc\u002Fcomponents\u002FBaseButton.vue",[21],"github",[70,183,188],{"className":184,"code":186,"filename":172,"language":187,"meta":77},[185],"language-vue","\u003Cscript setup lang=\"ts\">\ninterface Props {\n    size?: 'small' | 'medium' | 'large'\n    variant?: 'primary' | 'secondary' | 'outline'\n    disabled?: boolean\n}\n\ndefineProps\u003CProps>()\ndefineEmits\u003C{\n    (e: 'click', event: MouseEvent): void\n}>()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cbutton\n        class=\"button\" :class=\"[\n            `button--${size}`,\n            `button--${variant}`,\n            { 'button--disabled': disabled },\n        ]\"\n        :disabled=\"disabled\"\n        @click=\"$emit('click', $event)\"\n    >\n        \u003Cslot \u002F>\n    \u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped lang=\"scss\">\n.button {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    \u002F* Additional styling available in the GitHub repository *\u002F\n}\n\n\u002F* Size, variant, and state modifiers available in the GitHub repository *\u002F\n\u003C\u002Fstyle>\n","vue",[79,189,186],{"__ignoreMap":77},[62,191,192],{"id":192},"定义测试故事",[13,194,195],{},"创建“故事”来展示不同的按钮配置：",[70,197,201],{"className":198,"code":199,"filename":200,"language":76,"meta":77},[73],"const buttonStories = [\n    {\n        name: 'Primary Medium',\n        props: { variant: 'primary', size: 'medium' },\n        slots: { default: 'Primary Button' },\n    },\n    {\n        name: 'Secondary Medium',\n        props: { variant: 'secondary', size: 'medium' },\n        slots: { default: 'Secondary Button' },\n    },\n    \u002F\u002F and much more ...\n]\n","BaseButton.spec.ts",[79,202,199],{"__ignoreMap":77},[13,204,205],{},"每个故事都定义了一个名称、道具和槽点内容。",[62,207,208],{"id":208},"为渲染故事截图",[13,210,211],{},"在一个容器中渲染所有故事以捕获全面的屏幕截图：",[70,213,216],{"className":214,"code":215,"filename":200,"language":76,"meta":77},[73],"import type { Component } from 'vue'\n\ninterface Story\u003CT> {\n    name: string\n    props: Record\u003Cstring, any>\n    slots: Record\u003Cstring, string>\n}\n\nfunction renderStories\u003CT>(component: Component, stories: Story\u003CT>[]): HTMLElement {\n    const container = document.createElement('div')\n    container.style.display = 'flex'\n    container.style.flexDirection = 'column'\n    container.style.gap = '16px'\n    container.style.padding = '20px'\n    container.style.backgroundColor = '#ffffff'\n\n    stories.forEach((story) => {\n        const storyWrapper = document.createElement('div')\n        const label = document.createElement('h3')\n        label.textContent = story.name\n        storyWrapper.appendChild(label)\n\n        const { container: storyContainer } = render(component, {\n            props: story.props,\n            slots: story.slots,\n        })\n        storyWrapper.appendChild(storyContainer)\n        container.appendChild(storyWrapper)\n    })\n\n    return container\n}\n",[79,217,215],{"__ignoreMap":77},[62,219,220],{"id":220},"编写可视化回归测试",[13,222,223],{},"编写一个测试来呈现故事并捕获屏幕截图：",[70,225,228],{"className":226,"code":227,"filename":200,"language":76,"meta":77},[73],"import type { Component } from 'vue'\n\nimport { page } from '@vitest\u002Fbrowser\u002Fcontext'\nimport { describe, expect, it } from 'vitest'\nimport { render } from 'vitest-browser-vue'\n\nimport BaseButton from '..\u002FBaseButton.vue'\n\n\u002F\u002F [buttonStories and renderStories defined above]\ndescribe('BaseButton', () => {\n    describe('visual regression', () => {\n        it('should match all button variants snapshot', async () => {\n            const container = renderStories(BaseButton, buttonStories)\n            document.body.appendChild(container)\n\n            const screenshot = await page.screenshot({\n                path: 'all-button-variants.png',\n            })\n\n            \u002F\u002F this assertion is acutaly not doing anything\n            \u002F\u002F but otherwise you would get a warning about the screenshot not being taken\n            expect(screenshot).toBeTruthy()\n\n            document.body.removeChild(container)\n        })\n    })\n})\n",[79,229,227],{"__ignoreMap":77},[13,231,232,233,236,237,240],{},"使用 ",[79,234,235],{"code":235},"vitest-browser-vue"," 的 ",[79,238,239],{"code":239},"render"," 来捕获在真实浏览器中出现的组件。",[56,242,243],{},[13,244,245,246,249,250,253],{},"使用",[79,247,248],{"code":248},".browser.spec.ts","扩展名（例如",[79,251,252],{"code":252},"BaseButton.browser.spec.ts","）保存此文件以匹配您的浏览器测试配置。",[62,255,257],{"id":256},"超越截图自动比较","超越截图：自动比较",[13,259,260],{},"通过以 base64 形式编码屏幕截图并将其与基线快照进行比较来自动进行图像比较：",[70,262,265],{"className":263,"code":264,"language":76,"meta":77},[73],"\u002F\u002F Helper function to take and compare screenshots\nasync function takeAndCompareScreenshot(name: string, element: HTMLElement) {\n    const screenshotDir = '.\u002F__screenshots__'\n    const snapshotDir = '.\u002F__snapshots__'\n    const screenshotPath = `${screenshotDir}\u002F${name}.png`\n\n    \u002F\u002F Append element to body\n    document.body.appendChild(element)\n\n    \u002F\u002F Take screenshot\n    const screenshot = await page.screenshot({\n        path: screenshotPath,\n        base64: true,\n    })\n\n    \u002F\u002F Compare base64 snapshot\n    await expect(screenshot.base64).toMatchFileSnapshot(`${snapshotDir}\u002F${name}.snap`)\n\n    \u002F\u002F Save PNG for reference\n    await expect(screenshot.path).toBeTruthy()\n\n    \u002F\u002F Cleanup\n    document.body.removeChild(element)\n}\n",[79,266,264],{"__ignoreMap":77},[13,268,269],{},"然后更新测试：",[70,271,274],{"className":272,"code":273,"language":76,"meta":77},[73],"describe('BaseButton', () => {\n    describe('visual regression', () => {\n        it('should match all button variants snapshot', async () => {\n            const container = renderStories(BaseButton, buttonStories)\n            await expect(\n                takeAndCompareScreenshot('all-button-variants', container)\n            ).resolves.not.toThrow()\n        })\n    })\n})\n",[79,275,273],{"__ignoreMap":77},[56,277,278],{},[13,279,280,281,286],{},"Vitest 正在讨论浏览器模式下的原生屏幕截图比较。请关注",[17,282,285],{"href":283,"rel":284},"https:\u002F\u002Fgithub.com\u002Fvitest-dev\u002Fvitest\u002Fdiscussions\u002F690",[21],"github.com\u002Fvitest-dev\u002Fvitest\u002Fdiscussions\u002F690","并做出贡献。",[13,288,289],{},[51,290],{"alt":291,"src":292},"整体流程一览","https:\u002F\u002Fbitmc.uno\u002Fpicgo\u002F67c3c3fa74858-78.webp",[62,294,295],{"id":295},"小结",[13,297,298],{},"Vitest 的实验性浏览器模式使开发人员能够在真实的浏览器环境中对 Vue 组件进行准确的视觉回归测试。虽然当前的工作流程需要手动审查屏幕截图比较，但它为未来更自动化的视觉测试奠定了基础。这种方法还加强了开发人员和 UI 设计师之间的协作。设计师可以通过访问组件库中生成的屏幕截图，在生产部署之前查看组件的视觉变化。对于高级视觉测试功能，团队应该探索 Playwright 或 Cypress 等提供更多功能和成熟度的专用工具。请记住对您的基本组件执行视觉回归测试。",{"title":77,"searchDepth":300,"depth":300,"links":301},4,[302,304,305,306,307,308,309,310],{"id":64,"depth":303,"text":65},2,{"id":83,"depth":303,"text":83},{"id":165,"depth":303,"text":166},{"id":192,"depth":303,"text":192},{"id":208,"depth":303,"text":208},{"id":220,"depth":303,"text":220},{"id":256,"depth":303,"text":257},{"id":295,"depth":303,"text":295},[312],"译","2025-03-02 10:10:47",false,"md",null,{"aside":318},[319,320],"toc","auth-alex",true,"\u002Fposts\u002F2025\u002Fhow-to-do-visual-regression-testing-in-vue-with-vitest",{"text":324,"minutes":325,"time":326,"words":327},"7 min read",6.765,405900,1353,{"title":5,"description":44},{"loc":322,"images":330},[331,332],{"loc":54},{"loc":292},"posts\u002F2025\u002Fhow-to-do-visual-regression-testing-in-vue-with-vitest",[335,336,337],"翻译","Vue","Vitest","2026-02-07 23:23:50","Ykp-qNLuEYArgo5pO2physDt1OXv2C3s0RPlgQedzjY",[341,346],{"title":342,"path":343,"stem":344,"date":345,"type":316,"children":-1},"【翻译】如何使用测试库和 Vitest 测试 Vue Router 组件","\u002Fposts\u002F2025\u002Fhow-to-test-vue-router-components-with-testing-library-and-vitest","posts\u002F2025\u002Fhow-to-test-vue-router-components-with-testing-library-and-vitest","2025-03-02 09:45:11",{"title":347,"path":348,"stem":349,"date":350,"type":316,"children":-1},"Zod 基础用法","\u002Fposts\u002F2025\u002Fzod-use","posts\u002F2025\u002Fzod-use","2025-03-15 18:10:39",1789642589935]