ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue组件库测试体系构建:Vitest + Vue Test Utils + Playwright实战

Vue组件库测试体系构建:Vitest + Vue Test Utils + Playwright实战 1. 先想明白测试组件库和测试业务组件根本不是一回事很多人第一次给 TinyVue 或者其他组件库写测试的时候会直接套用业务项目里的那套思路把组件挂载起来点两下断言数据变了完事。这个思路放在业务页面里没有问题但放在组件库里远远不够。组件库里的组件是给成百上千个业务方复用的你写出来的按钮、弹窗、表格别人会在各种环境、各种业务场景下使用。如果你的测试只覆盖了“正常点击了一次”的路径那这个组件基本等于裸奔。组件库测试要解决的核心问题有三层。第一层是契约稳定性。组件对外暴露的 props、事件、插槽、方法都是你和其他开发者之间的契约。你今天改了size属性的取值范围明天可能就有业务方的样式崩了。测试要把这些契约钉死谁动了就报错。第二层是边界条件。组件库里的组件要处理大量异常输入空数组、超长文本、非法日期、极小的视口、慢网络下的异步状态。这些在业务代码里可能一辈子都触发不了几次但在组件库里是家常便饭。第三层是跨端一致性。TinyVue 这类组件库还需要考虑 Vue 2 和 Vue 3 生态的兼容性不同浏览器对 CSS 和事件的处理差异这些光靠单测是测不出来的必须配合浏览器层面的回归测试。所以给组件库搭测试环境时第一件事不是写测试用例而是先明确你要覆盖哪几层单元测试组件逻辑和渲染、快照测试DOM 结构和样式基线、交互测试用户操作和事件。三层各司其职互相不能替代。这也是为什么组件库项目的测试配置文件通常比业务项目复杂得多——它不是在测一个页面而是在测一个产品。1.1 组件库测试到底在测什么具体到 TinyVue 的组件测试的关注点可以拆成这么几类props 驱动同一个组件在不同 props 组合下要渲染出正确的结构和样式。比如按钮的type从primary切到danger类名和颜色都要跟着变。这种测试是纯函数式的输入 props断言输出 DOM。事件交互用户点击、输入、滚动之后组件要发出正确的事件事件参数要符合文档定义。很多时候 bug 就出在事件参数上多传了一个字段少传了一个字段业务方拿到的数据就不对。插槽机制组件库的插槽是一个很容易被忽略的测试点。默认插槽、具名插槽、作用域插槽不同插槽组合下的渲染结果都要验证。插槽测不到位组件在业务方的真实使用场景里就会出幺蛾子。异步行为比如远程搜索的输入框、防抖的按钮、延迟加载的懒加载组件这些都需要在测试里控制时间戳和异步队列不能靠简单的setTimeout等。样式和主题组件库的样式是另一个产品维度。主题变量改了之后组件的类名和 style 绑定是否正确这些可以用快照测试来兜底。写组件库测试的最佳实践是一个组件一个测试文件按 props、事件、插槽、异步、边界条件分成多个 describe 块。这样跑测试的时候定位问题快别人看你的测试文件也很容易理解这个组件对外提供了什么能力。TinyVue 这种规模的项目每个组件的测试文件实际上也是一份活的文档。1.2 为什么不能把业务项目的测试方案直接搬过来我见过不少团队做业务项目的时候用 Vue Test Utils Jest 写单测等开始做组件库了还是那一套配置直接复制过来结果跑起来各种碰壁。业务项目的测试有几个特点测试环境里有完整的路由、全局状态管理、后端 mock组件可以在一个“模拟业务容器”里运行业务项目不会去验证组件的 API 兼容性和边界条件业务项目通常只跑在当前浏览器和当前 Vue 版本上不用考虑跨版本兼容。组件库完全不一样。它没有业务容器组件是被脱光了衣服扔在一个空荡荡的测试台上的宿主环境给不了它任何帮助。它要自己 mock 掉外部依赖自己构造全局配置自己处理 Vue 版本差异。这就是为什么稍大规模的组件库项目都会自己封装一套测试基座而不是直接在组件测试里裸写 mount。另外业务项目的测试通常只需要覆盖“这个页面能不能正常干活”而组件库测试要覆盖“这个组件在任何情况下会不会爆炸”。所以组件库测试对覆盖率的要求更高对边界条件的枚举更细致对异步时序的控制也更严格。这些差异都会直接影响测试工具和配置方式的选择。2. 测试工具选型思路Vitest Vue Test Utils PlaywrightTinyVue 这类 Vue 组件库的测试体系通常分两层一层是跑在 Node 环境里的单元测试另一层是跑在真实浏览器里的端到端回归测试。两层使用的工具完全不同解决的问题也完全不同。单测层我用的是Vitest Vue Test Utils。TinyVue 面向 Vue 3 的版本Vue Test Utils 是官方提供的组件测试工具库提供了挂载组件、触发事件、断言渲染结果等能力。Vitest 是基于 Vite 的测试运行器和 Vite 天然是一套生态配置起来非常顺。至于 Vue 2 版本官方对应的测试工具是vue/test-utils1这两个版本 API 差异不小如果组件库要同时维护 Vue 2 和 Vue 3 两套测试建议把测试配置文件分开不要混在一起。回归层我用的是Playwright。它启动真实 Chromium 浏览器可以对组件库的 demo 页面做端到端测试验证真实浏览器环境下的渲染和交互行为。单测和回归测试的分工很明确单测管逻辑正确性和 API 稳定性回归测试管浏览器环境和渲染结果。2.1 单测层为什么是 Vitest 而不是 Jest现在还有不少项目在用 Jest 给 Vue 组件写单测。Jest 本身很成熟生态也丰富但它和 Vite 的搭配一直有点别扭需要配一堆转换插件来处理 ESM 和 TypeScript。Vitest 则是在 Vite 之上做的测试框架它直接吃 Vite 的配置不需要额外构建启动和热更新都明显更快。两张对比一下主要差异对比项VitestJest配置复杂度低直接复用 Vite 配置高需要单独配 moduleNameMapper、transform启动速度快基于 Vite 的 dev server满需要完整构建一遍TypeScript 支持原生支持需要额外 babel 或 ts-jest模块 mock支持vi.mock写法简洁支持jest.mock功能和 Vitest 基本对齐文件监听内置 watch 模式内置 watch 模式Vue 单文件组件支持通过 Vite 插件自动解决需要vue-jest插件性能和报错信息都不太理想我自己用一个真实工程做过对比同样的测试集Vitest 启动只需 1 到 2 秒Jest 需要 5 秒以上测试执行速度相差更多尤其是在几十个组件测试文件同时跑的时候。更关键的是Vitest 对import.meta.env、ESM、TypeScript的原生支持让组件库这种大量使用现代前端特性的项目不需要再套一层转译工具配置少了一半。如果你是从 Jest 迁移过来的大部分jest的 API 在 Vitest 里都有对应describe、it、expect这些全局函数不用改jest.fn()对应vi.fn()jest.mock()对应vi.mock()。迁移成本其实很低。2.2 组件挂载与交互测试Vue Test UtilsVue Test Utils 是 Vue 官方的组件测试工具库目前稳定版本是 v2对应 Vue 3。它提供的核心 API 有mount、shallowMount、attachTo、trigger、setProps、emitted、find、findAll等覆盖了组件渲染、交互、断言三个环节。在组件库项目中我推荐用mount而不是shallowMount来写绝大多数测试。shallowMount会 stub 掉所有子组件对于业务项目的单元测试来说可以减少干扰但在组件库里子组件往往也是你自己写的组件它们之间存在 props 和事件联动stub 掉之后反而测不到真实行为。比如一个TinyForm组件内部挂了TinyFormItem和TinyInput你用 shallowMount 挂载的话表单校验逻辑根本不会执行测试等于白写。Vue Test Utils 的trigger方法可以触发 DOM 事件比如wrapper.find(button).trigger(click)。这个方法的底层是dispatchEvent所以触发的 click 事件会经过 Vue 的事件系统能正常触发组件里绑定的click处理器。如果你要测试的事件涉及异步更新或者下一帧渲染需要配合await nextTick()或者flushPromises()来等待更新。组件库测试里还有一个高频场景测试 props 变化后组件是否响应更新。Vue Test Utils 提供了setProps方法可以在挂载后动态修改 props然后断言 DOM 变化。这在测试受控组件的时候特别有用。2.3 回归层为什么要上 Playwright单测跑在 jsdom 环境里jsdom 只是一个模拟 DOM 的 JavaScript 实现它的样式计算、布局、滚动、事件冒泡等行为和真实浏览器有差异。组件库里凡是跟布局和滚动相关的组件比如表格的固定列、下拉框的虚拟滚动、弹窗的层级管理单测根本测不准必须上真实浏览器。Playwright 是我个人比较推荐的选择。它支持 Chromium、Firefox、WebKit 三种内核可以一套代码同时跑多个浏览器它内置了自动等待机制元素出现之前不会急着点击省去了一堆手工 sleep它还支持移动端模拟、网络请求拦截、屏幕截图对比这些对组件库的回归测试都很有价值。实操中我会给 Playwright 配一份独立的playwright.config.ts针对组件库 demo 页面跑端到端验证。每个组件在开发时都会有一个示例页面比如examples/button.vuePlaywright 就去打开这些页面检查按钮是否能点击、弹窗是否能开关、表格是否能滚动。这类测试不需要覆盖每个 props 组合那是单测的事它只需要保证真实的浏览器环境里组件能正常用就行。3. 测试配置落地从零配置一套组件库测试环境配置组件库的测试环境是这套工具链里最琐碎也最考验经验的部分。配置得不好后面写用例的时候会天天跟环境问题搏斗。下面是我在 TinyVue 工程里实际使用的一套配置方案你可以直接参考着改。3.1 vitest.config.ts 核心配置拆解先看一份典型的配置文件。TinyVue 的工程是 monorepo 结构组件和工具函数都在 packages 目录里这套配置针对这种结构做了一些专门处理。// vitest.config.ts import { defineConfig } from vitest/config import vue from vitejs/plugin-vue import { resolve } from path export default defineConfig({ plugins: [vue()], resolve: { alias: { : resolve(__dirname, packages), tiny-vue: resolve(__dirname, packages/vue/src), }, }, test: { environment: jsdom, globals: true, setupFiles: [./scripts/test-setup.ts], include: [packages/**/__tests__/**/*.spec.ts], exclude: [**/node_modules/**, **/dist/**, **/e2e/**], coverage: { provider: v8, reporter: [text, html], include: [packages/**/src/**/*.{ts,vue}], exclude: [**/style/**, **/locale/**, **/types/**], }, deps: { inline: [vue/test-utils], }, }, })逐项解释一下每个配置为什么这么写。environment: jsdom指定测试环境为 jsdom。这是组件单测的标准环境它有 DOM API 但不是真实浏览器。如果你有组件底层依赖浏览器特有的 API比如ResizeObserver、matchMediajsdom 里可能不存在后面会在 setup 文件里 mock 掉。globals: true表示在测试文件里可以直接用describe、it、expect这些全局函数不用显式 import。这个选项能少写很多模板代码但代价是编辑器可能不认识这些全局函数需要在 tsconfig 里加上types: [vitest/globals]。setupFiles指向一个测试启动之前会执行的脚本通常用来注册全局组件、mock 浏览器 API、设置测试辅助函数。这个文件是测试环境的心脏后面单独讲。include和exclude定义了哪些文件算测试文件。这里用的是packages/**/__tests__/**/*.spec.ts也就是说每个包下面可以有独立的__tests__目录这个目录只放测试相关的东西不会被打进发布产物里。coverage配置了覆盖率统计的方式和范围。组件库里通常不需要统计样式文件的覆盖率那些是纯 CSS没有逻辑可言类型定义文件也不用统计它们不参与运行。把这些排除掉覆盖率数字才更有参考意义。deps.inline指定某些依赖需要在测试环境里被内联处理。vue/test-utils推荐在这里做 inline因为它的 ESM 产物在 jsdom 里有时会有兼容问题内联之后可以避免一些奇奇怪怪的报错。3.2 测试 setup 文件里都做了什么test-setup.ts这个文件虽然只是启动前的初始化脚本但它的内容直接决定了你写测试的时候顺不顺。我见过很多团队忽略这个文件结果每个测试文件里都在重复写 mock 代码又乱又容易漏。以下是我始终保留在 setup 文件里的几类内容// scripts/test-setup.ts import { config } from vue/test-utils import { vi } from vitest // 1. 全局注册组件 import { TinyButton } from ../packages/vue/src/button // ... 其他组件 config.global.stubs { transition: false, transition-group: false, } config.global.components { TinyButton, // ... 其他全局组件 } // 2. mock 浏览器 API if (!window.matchMedia) { window.matchMedia vi.fn().mockImplementation((query: string) ({ matches: false, media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), })) } if (!window.ResizeObserver) { window.ResizeObserver vi.fn().mockImplementation(() ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(), })) } // 3. 清理 DOM afterEach(() { document.body.innerHTML })全局注册组件的目的是让你在写测试的时候不用每次都 import 组件再手动挂载而是直接通过模板字符串使用组件名。这在测试组件之间联动的场景里非常方便。mock 浏览器 API 是组件库测试中一定会遇到的。jsdom 没有实现matchMedia和ResizeObserver但是很多组件在挂载时就会调用它们比如使用了响应式布局的组件会监听视口大小变化。如果不 mock所有涉及这些组件的测试都会在挂载阶段抛异常而且报错信息晦涩难懂。afterEach清理 DOM 也很重要。Vue Test Utils 的mount会把组件挂载到一个 div 上如果测试之间没有清理上一个测试的 DOM 还留在页面上会产生互相干扰。这个清理逻辑放在 setup 文件里所有测试文件都能继承到。3.3 TypeScript、模块别名与组件类型声明组件库项目通常是用 TypeScript 写的测试文件自然也是 TS。但 TS 在测试环境里的类型配置和源码工程有所不同处理不好会有大量红色波浪线报错。Vitest 基于 Vite所以它天生支持 TS 文件的转译不需要额外配置ts-jest。但 Vite 在转译 TS 时是逐文件的不做全量类型检查也就是说类型错误不会导致测试失败只会在编辑器里标红。要真正检查类型需要在 CI 里单独跑一条vue-tsc --noEmit命令。这是和 Jest ts-jest 差异比较大的地方。在 tsconfig 里要给vitest/globals加上类型声明{ compilerOptions: { types: [vitest/globals, types/node], paths: { /*: [packages/*] } } }如果你用的是globals: true这些全局测试函数的类型就来自vitest/globals。paths则是让 TS 的模块别名解析和 Vite 保持一致否则你在测试文件里写import { TinyButton } from /vue/src/button时TS 会报找不到模块。还有一个容易被忽略的是.vue文件的类型声明。TS 默认不认识.vue文件需要加一个env.d.ts声明文件// env.d.ts declare module *.vue { import type { DefineComponent } from vue const component: DefineComponent{}, {}, any export default component }不加这个声明你在测试文件里 import 一个.vue组件时TS 会报“找不到模块”。这是所有 Vue TS 项目的通用配置。3.4 package.json 脚本与持续集成测试工具链最终要跑在 CI 里所以package.json的 scripts 也要设计好。单独跑某一种测试很简单留给 CI 的是一整套流程。{ scripts: { test: vitest run, test:watch: vitest, test:coverage: vitest run --coverage, test:e2e: playwright test, test:all: npm run test npm run test:e2e } }vitest run表示执行一次测试后退出适合 CI 环境。本地开发时跑vitest会进入 watch 模式代码一变自动重跑配合热更新很爽。在 CI 里我会把单测和端到端测试拆成两步而不是合成一条命令。原因是单测跑得快可以在提交时就跑端到端测试要下载和启动浏览器耗时更长可以让它在合并前跑。GitHub Actions 或 GitLab CI 里大概是这样# .github/workflows/test.yml示例片段 jobs: unit-test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 20 cache: npm - run: npm ci - run: npm run test e2e-test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - run: npm ci - run: npx playwright install --with-deps chromium - run: npm run test:e2e如果测试文件数量多了单测的耗时也会上去。Vitest 自身支持多线程并行执行默认就会用满 CPU 核数。你还可以在 CI 里启用缓存把 node_modules 和 Playwright 的浏览器缓存起来能省下不少时间。另外提交前测试是防止烂代码进仓库的第一道闸。配合 husky 和 lint-staged可以在pre-commit钩子里只跑变更文件相关的测试。对于组件库这种大工程全量测试跑起来可能要几分钟每次提交都跑全量不现实。只跑变更的测试文件速度会快很多。4. 实操记录为 TinyVue 按钮组件补一组测试配置搭好之后具体怎么给组件写测试才是见真章的地方。我以 TinyVue 的按钮组件为例从最基础的挂载测试到组件库特有的主题测试逐步展示一个真实的测试文件是怎么长出来的。4.1 基础挂载与快照测试按钮组件是最简单的组件之一但即使是它也有 props、插槽、事件、继承属性等多个维度要测。第一个测试永远是最基础的一个确认它能挂载、渲染结构不出错。// packages/vue/src/button/__tests__/button.spec.ts import { describe, it, expect } from vitest import { mount } from vue/test-utils import TinyButton from ../src/button.vue describe(TinyButton 组件, () { it(能正常挂载并渲染默认插槽内容, () { const wrapper mount(TinyButton, { slots: { default: 点击我, }, }) expect(wrapper.text()).toContain(点击我) expect(wrapper.find(button).exists()).toBe(true) }) it(生成符合规范的基础类名, () { const wrapper mount(TinyButton) expect(wrapper.classes()).toContain(tiny-button) expect(wrapper.element.tagName).toBe(BUTTON) }) it(overlay 行为不设置 class 时使用组件默认类名, () { const wrapper mount(TinyButton, { slots: { default: 确定 }, attrs: { id: confirm-btn }, }) expect(wrapper.attributes(id)).toBe(confirm-btn) expect(wrapper.classes()).toContain(tiny-button) }) })第一个测试挂载按钮并断言文本内容。第二个测试验证类名。第三个测试验证组件能正常继承外部传入的id等属性这是组件库组件需要满足的一个基本约定——属性透传。如果你的工程里启用了快照测试可以加一个快照用例。快照的核心价值在于防止无意的结构变更但快照也会产生很多噪音一个无关紧要的 class 调整就会让快照失效。我的建议是快照只截取结构最稳定的那部分组件比如基础按钮、基础输入框这种。复杂的组件比如表格、树控件就不要用快照了DOM 结构变动太频繁快照维护成本会高到让你想删掉它。4.2 事件与交互行为测试按钮组件最重要的交互是点击。点击后要触发什么事件事件参数是什么这是契约的一部分必须测。describe(TinyButton 组件交互, () { it(点击按钮时触发 click 事件, async () { const wrapper mount(TinyButton, { slots: { default: 保存 }, }) await wrapper.trigger(click) expect(wrapper.emitted(click)).toHaveLength(1) }) it(loading 状态下点击不触发 click 事件, async () { const wrapper mount(TinyButton, { props: { loading: true }, slots: { default: 提交中 }, }) await wrapper.trigger(click) expect(wrapper.emitted(click)).toBeUndefined() }) it(disabled 状态下点击不触发 click 事件, async () { const wrapper mount(TinyButton, { props: { disabled: true }, }) await wrapper.trigger(click) expect(wrapper.emitted(click)).toBeUndefined() }) })wrapper.emitted(click)是 Vue Test Utils 提供的方法用来获取组件触发过的所有 click 事件数组。toHaveLength(1)断言点击一次只会触发一次事件。loading和disabled状态是按钮组件的常见边界条件。很多按钮组件在这两种状态下会阻止点击事件的冒泡但具体实现方式不同。有的组件通过pointer-events: none在样式层面屏蔽点击有的在事件处理器里提前 return。无论哪种方式测试断言的核心都是同一个事件没有发出去。这个测试保护了按钮组件最重要的行为约定。如果你测试的是自定义事件比如一个 Switch 组件切换时触发change事件断言方式稍有不同。你需要先触发 DOM 交互然后通过emitted(change)获取事件数据并检查事件参数const [eventPayload] wrapper.emitted(change)![0] expect(eventPayload).toBe(true)组件库的事件参数测试尤其重要因为业务方订阅事件时依赖的是事件参数的准确结构。参数从{ value: true }变成true都是破坏性变更。4.3 异步更新与动态 props 测试按钮组件涉及异步更新的场景不多但表单类组件非常典型。为了演示异步测试的写法我把一个远程搜索输入框的场景简化一下展示flushPromises和nextTick的配合。import { flushPromises } from vue/test-utils describe(异步行为测试, () { it(输入内容后触发防抖搜索事件, async () { vi.useFakeTimers() const wrapper mount(TinyInput, { props: { modelValue: , debounce: 300, }, }) const input wrapper.find(input) await input.setValue(组件库) vi.advanceTimersByTime(300) await flushPromises() expect(wrapper.emitted(search)).toHaveLength(1) expect(wrapper.emitted(search)![0][0]).toBe(组件库) vi.useRealTimers() }) it(modelValue 更新时组件响应渲染, async () { const wrapper mount(TinyInput, { props: { modelValue: 初始值 }, }) const input wrapper.find(input) expect(input.element.value).toBe(初始值) await wrapper.setProps({ modelValue: 更新值 }) expect(input.element.value).toBe(更新值) }) })vi.useFakeTimers()是 Vitest 提供的假定时器机制可以让你手动控制时间流逝不用真的等 300 毫秒。这种方式写涉及防抖、节流的组件测试是唯一靠谱的方案真实的等待不仅慢而且容易造成测试不稳定。flushPromises来自 Vue Test Utils它会等待当前所有 Promise 链执行完毕。很多组件库用 Promise 来处理异步状态比如远程搜索的接口返回、表单校验的异步规则、组件加载的异步分包。在测试里你发出一个异步操作之后需要await flushPromises()来让 Promise 链走完然后才能断言最终状态。setProps是测试受控组件的好帮手。组件库里的value和modelValue都是受控的外部传入什么值组件内部就渲染什么。测试 setProps 后 DOM 是否响应更新能验证组件的“受控一致性”。4.4 组件库特有的测试点全局配置、主题与国际化组件库和业务组件还有一个大差异它有一个全局配置系统。TinyVue 支持通过ConfigProvider或者 app 级别的配置来改变组件的行为和外观。测试这种全局配置注入需要借助 Vue 的global.provide。举一个实际场景TinyVue 的按钮组件支持自定义主题变量业务方可以通过全局配置注入一套深色主题。测试要验证的是注入主题配置之后按钮的 style 变量是否正确。describe(TinyButton 全局配置测试, () { it(通过全局配置注入主题变量, () { const wrapper mount(TinyButton, { slots: { default: 主题按钮 }, global: { provide: { tinyConfig: { theme: { --tiny-color-primary: #7b3ff2, }, }, }, }, }) const buttonStyle wrapper.find(button).attributes(style) || expect(buttonStyle).toContain(--tiny-color-primary: #7b3ff2) }) it(未配置主题时使用默认主题变量, () { const wrapper mount(TinyButton, { slots: { default: 默认按钮 }, }) const buttonStyle wrapper.find(button).attributes(style) || expect(buttonStyle).not.toContain(--tiny-color-primary) }) })global.provide是 Vue Test Utils 里在挂载时注入依赖的方法。组件内部通过inject来读取这些全局配置。这是组件库测试里很重要的一个模拟手段因为你测的不是组件的孤立行为而是组件在宿主应用配置下的行为。国际化的测试也是类似的思路。TinyVue 的组件有内置的文本比如分页组件的“前往”按钮、上传组件的“点击上传”文案。测试时通过全局配置切换语言断言组件渲染出对应的文案。这类测试虽然简单但对多语言业务方来说是刚需必须保证切语言不乱。写到这里你应该发现了组件库单测的颗粒度不是“能不能用”而是“契约是否被遵守”。每一个测试用例都在为某一条组件契约做一个担保积少成多就是整套组件库的信任基石。5. 常见问题与排查技巧实录配置测试环境时踩过的坑比写测试本身多得多。这些坑很多是 jsdom 和真实浏览器的差异导致的也有一些是工具链版本衔接的细节。我把实操中最常遇到的几类问题整理成速查列表方便你对照排查。5.1 jsdom 里缺失的浏览器 API这是最普遍的坑。组件挂载时调用了 jsdom 没有实现的浏览器 API测试直接报错而且报错信息通常不直观。最常见的缺失 API 有window.matchMedia组件的响应式断点逻辑会用到。TinyVue 的布局组件和栅格组件一定会调用。ResizeObserver容器尺寸监测。表格、滚动条、下拉框这类组件都会用到。IntersectionObserver懒加载、吸顶、无限滚动组件会用到。scrollIntoView下拉框展开后自动滚动到选中项代码里很常见但 jsdom 没有实现。HTMLCanvasElement.prototype.getContext图表类组件和签名组件会用到。解决方式就是 setup 文件里统一 mock。注意 mock 的时候要实现完整的实例方法比如ResizeObserver的observe、unobserve、disconnect都要有不然组件代码在调disconnect的时候会继续报错。mock 之后还要确认组件代码在首次挂载时不会因为拿不到尺寸而崩溃有些组件的渲染逻辑依赖entry.contentRect.width这样的数据如果 mock 返回的内容不完整组件会渲染成错误状态。5.2 异步更新导致断言失败这个问题在刚接触 Vue Test Utils 的人身上几乎是必然出现的。你触发了一个点击事件然后立刻断言 DOM结果发现断言失败。原因不是代码有 bug而是 Vue 的响应式更新是异步的。// 错误写法 wrapper.find(button).trigger(click) expect(wrapper.find(.message).text()).toBe(已提交) // 正确写法 await wrapper.find(button).trigger(click) expect(wrapper.find(.message).text()).toBe(已提交)trigger返回的是一个 Promise必须 await 它让组件内部的响应式更新跑完一轮。同理setProps、setValue都需要 await。一旦你习惯了这个模式异步断言的问题就少了一大半。另一种情况是组件的异步函数比如setTimeout或者接口请求。这时候需要vi.useFakeTimers()加上advanceTimersByTime手动控制时间。但要注意一点flushPromises和vi.useFakeTimers()有时会打架。Flush 的时候 Vue 内部可能还有微任务队列在等待如果遇到flushPromises不生效的情况可以试试await new Promise(resolve setTimeout(resolve, 0))手动让出一次宏任务。这个方法简单粗暴但很有效。5.3 快照测试不稳定的处理快照测试最烦的不是内容不一致而是内容变动太频繁导致你每次提交都得更新快照。时间戳、随机 id、依赖的第三方库版本升级都可能是快照变动的来源。组件库里最典型的场景是组件内部生成了一个>
返回列表