test-utils
VuePress 的单元测试工具。
vuepress/client 会在模块作用域引用生成模块(@internal/*)与编译期 define(__VUEPRESS_DEV__ 等),因此无法在单元测试中直接导入。该包会替换它们,使插件或主题的客户端代码可以在单元测试中被渲染与交互,而无需运行沉重的 e2e 测试。
Usage
npm i -D @vuepress/test-utils@next @vue/test-utils happy-domimport { vuepressTestPlugin } from '@vuepress/test-utils'
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [vuepressTestPlugin()],
})若要测试 .vue 单文件组件,按常规在 plugins 中加入 Vue 插件(如 @vitejs/plugin-vue)即可。
若你的包使用了 resolve.alias 无法表达的模块别名(如 @vuepress/theme-default 的 @theme/*,它可能落到不同目录),请给插件传入 resolve 函数。
export default defineConfig({
plugins: [
vue(),
vuepressTestPlugin({
resolve: (id) =>
id.startsWith('@theme/') ? resolveThemeFile(id) : undefined,
}),
],
})Guide
渲染客户端代码
renderVuePress() 将客户端 app 渲染为字符串,可在 node 环境下使用,是断言各种选项组合的渲染结果最轻量的方式。
import { renderVuePress } from '@vuepress/test-utils/client'
const html = await renderVuePress({
content: MyComponent,
page: { path: '/', title: 'Home' },
site: { title: 'My Site' },
})mountVuePress() 通过 @vue/test-utils 挂载客户端 app,需要 DOM 环境:
// @vitest-environment happy-dom
import { mountVuePress } from '@vuepress/test-utils/client'
const wrapper = await mountVuePress({
content: MyComponent,
page: { path: '/', title: 'Home' },
})
await wrapper.find('.my-button').trigger('click')提示
请把 // @vitest-environment happy-dom 放在文件最顶部、所有 import 之前,否则 Vitest 不会读取它。renderVuePress() 不需要它,因此不需要与 DOM 交互的测试请优先使用它。
两者的选项与 createTestClient() 相同。createTestClient() 返回的客户端可以多次渲染与挂载,并暴露客户端数据与路由:
const client = await createTestClient({ page: { path: '/', title: 'Home' } })
expect(await client.renderToString()).toContain('Home')
await client.router.push('/other/')content 选项是当前页面的内容(即 page chunk),由布局渲染它。vuepress/client 的 Content 组件就是默认布局,因此未指定布局时内容会被直接渲染。若想不经过布局与客户端配置的根组件、直接渲染单个组件,请改用 rootComponent 选项。
clientConfigs 接受插件与主题的客户端配置,并应用它们的 setup、enhance、layouts 与 rootComponents。当某个组合式 API 依赖客户端配置提供的 provider 时,这是必须的。例如 @vuepress/theme-default 的主题数据由 useThemeData() 读取,而它由主题数据插件提供,因此必须把该插件的客户端配置传给 clientConfigs。
测试选项组合
插件选项是通过 define 全局变量而非 props 传给客户端的。collectClientDefines() 从真实插件中读取它们,stubClientDefines() 把它们应用到 globalThis,与 bundler 的行为完全一致。
import {
collectClientDefines,
createTestApp,
stubClientDefines,
} from '@vuepress/test-utils'
import { mountVuePress } from '@vuepress/test-utils/client'
import myClientConfig from '../src/client/config.js'
import { myPlugin } from '../src/node/index.js'
const app = await createTestApp({ plugins: [myPlugin({ duration: 3000 })] })
const restore = stubClientDefines(await collectClientDefines(app))
const wrapper = await mountVuePress({
clientConfigs: [myClientConfig],
content: '...',
})
restore()提示
在模块作用域读取 define 的模块,必须在 stubClientDefines() 之后导入。此时请使用 vi.resetModules() 配合动态 import。
为生成的模块打桩
VuePress 会在构建期生成插件引用的客户端模块:@internal/* 模块,以及由生成的客户端配置创建的 @temp/* 模块(如 @vuepress/plugin-revealjs 的 @temp/revealjs/index.js)。它们在单元测试中并不存在,因此引用它们的模块完全无法导入。stubModule() 会注册这类模块的导出值并使其可解析,在 node 与 happy-dom 环境下均可工作。
import { stubModule, stubTempModule } from '@vuepress/test-utils'
const restoreNotice = stubModule('@internal/noticeOptions', {
NOTICE_OPTIONS: [{ path: '/', title: 'Notice' }],
})
const restoreReveal = stubTempModule('revealjs', {
useRevealJs: () => Promise.resolve([Reveal]),
})
vi.resetModules()
const { useNoticeOptions } =
await import('../src/client/composables/useNoticeOptions.js')
restoreNotice()
restoreReveal()stubTempModule(name, ...) 是 stubModule('@temp/<name>/index.js', ...) 的快捷方式。任意模块 id 都可以打桩,包括仅在构建期存在的子路径别名,如 @vuepress/plugin-comment 的 @vuepress/plugin-comment/service。
模块 id 会映射到临时目录中的一个生成文件:把 id 追加到该目录,当 id 不以 .js 结尾时补上 .js 扩展名。id、导出名与引用它的模块都必须与真实生成模块一致。由于生成模块会重新导出它们,导出名只支持标识符与 default。
注意
Vite 在模块被转换时解析导入,而这发生在测试运行之前。因此引用被打桩模块的模块必须在 stubModule() 之后动态导入,若它可能已被缓存还需配合 vi.resetModules()。静态导入的模块无法被打桩。
对于必须静态导入的模块,请在仓库配置中通过 vuepressTestPlugin() 的 resolve 选项注册别名:
const CLIENT_ALIASES: Record<string, string> = {
// 由插件在构建期创建的子路径别名
'@vuepress/plugin-comment/service':
'./plugins/blog/plugin-comment/src/client/components/WalineComment.js',
}
export default defineConfig({
plugins: [vuepressTestPlugin({ resolve: (id) => CLIENT_ALIASES[id] })],
})测试颜色模式
setColorMode() 会伪造 @vuepress/theme-default 用来解析夜间模式的两处来源:(prefers-color-scheme: dark) 媒体查询与 vuepress-color-scheme 本地存储键,同时设置 <html> 上的 data-theme。其他媒体查询会转发给真实的 window.matchMedia()。
// @vitest-environment happy-dom
import { setColorMode } from '@vuepress/test-utils/client'
const restore = setColorMode('dark')
const wrapper = await mountVuePress({ rootComponent: MyComponent })
expect(wrapper.find('.my-button').classes()).toContain('dark')
restore()传入 { storage: 'auto' } 可在媒体查询仍报告夜间模式的同时把存储值保持为 auto,从而覆盖 useDarkMode() 的 prefers-color-scheme 分支。
测试 Node 侧代码
createTestApp() 使用临时 source 目录、空主题与空 bundler 构建 VuePress app,因此无需真实站点即可测试插件钩子、页面解析与 markdown 扩展。
import {
createTestApp,
createTestMarkdown,
mockLogger,
} from '@vuepress/test-utils'
const app = await createTestApp({
plugins: [myPlugin()],
files: { 'README.md': '# Home' },
})
expect(app.pages.map(({ path }) => path)).toContain('/')
app.cleanup()createTestMarkdown() 使用 VuePress 默认选项创建 markdown-it 实例,createTestPage() 创建无需源文件的页面,mockLogger() 静默 VuePress 日志并返回间谍函数以便断言。
Options
vuepressTestPlugin
string'/'__VUEPRESS_BASE__ 的值。
boolean__VUEPRESS_DEV__ 的值。开发模式会启用 HMR 分支,它会访问单元测试中不存在的 __VUE_HMR_RUNTIME__。
string__VUEPRESS_VERSION__ 的值。默认读取已安装的 vuepress 包。
(id: string) => string | undefined解析 resolve.alias 无法表达的模块 id,如 @vuepress/theme-default 的 @theme/*,或仅在构建期存在的子路径别名(如 @vuepress/plugin-comment/service)。它会对每次导入调用,需返回文件路径或 undefined。通过 stubModule() 注册的模块会先于它被解析。
setColorMode options
由 setColorMode() 使用。
string'vuepress-color-scheme'保存颜色模式的本地存储键。
'auto' | 'light' | 'dark'存储在 storageKey 下的值。默认为 colorMode 参数。
Test client options
由 createTestClient()、renderVuePress() 与 mountVuePress() 共用。
Component | string当前页面的内容,即 page chunk。字符串会作为原始 HTML 渲染。
Component根组件。提供时会跳过布局与客户端配置的根组件。
TestPageOptions当前页面。
string'/'页面的路由路径。
string''页面的标题。
string''页面的语言。它会回退到站点语言环境的语言。
PageFrontmatter页面的 frontmatter。
Record<string, unknown>额外的页面数据,如 filePathRelative。
string当前页面的路由路径。默认为 page.path。
Partial<SiteData>站点数据。例如它的 locales 字段会根据路由路径切换站点语言环境数据。
Record<string, Component>布局组件。Layout 与 NotFound 默认为 Content。
Record<string, TestRouteOptions>额外的路由。每个路由接受 component 与 pageData。
Record<string, string>重定向。
Record<string, unknown>主题数据,由 @vuepress/plugin-theme-data/client 的 useThemeData() 读取。
ClientConfig[]需要应用的客户端配置。它们的 setup、enhance、layouts 与 rootComponents 都会生效。
boolean是否模拟 SSR 模式。renderToString() 默认 true,mount() 默认 false。
Test app options
由 createTestApp() 使用。它同时接受 VuePress app 配置的其余字段。
Record<string, string>需要在 source 目录中生成的 Markdown 文件。键为相对路径。
stringsource 目录。默认为临时目录,会被 app.cleanup() 删除。
ThemeemptyThemeapp 的主题。
boolean是否初始化 app。
boolean是否在初始化后准备 app。
