首页
/ Supabase 仓库的 Vitest 测试环境实践:从 jsdom 配置到自定义 Environment

Supabase 仓库的 Vitest 测试环境实践:从 jsdom 配置到自定义 Environment

2026-09-05 10:58:25作者:房伟宁

本文以 Vitest 的测试环境(Test Environments)为主题,系统讲解 nodejsdomhappy-domedge-runtime 四类环境的选用原则与配置方法,并以 Supabase 官方仓库中 Studio、UI 组件库等真实项目的 Vitest 配置与 Polyfill 实现为佐证,帮助你在单测中正确搭建浏览器 API 环境、按文件切换环境、编写自定义 Environment,并处理 CSS/资源与外部依赖的常见坑。

一、可用环境与适用场景

Vitest 提供四种内置测试环境,各自模拟不同的运行时能力:

环境 说明
node(默认) Node.js 环境,无任何浏览器 API
jsdom 类浏览器环境,提供较完整的 DOM API
happy-dom jsdom 的更快速替代,API 覆盖面较少
edge-runtime Vercel Edge Runtime 环境

选型的核心逻辑是"够用即可":纯逻辑/工具函数代码跑在默认的 node 环境最快、最接近真实执行;只有当被测代码真正触碰 documentwindowlocalStorage 等浏览器 API 时,才需要切到 jsdomhappy-dom

Supabase 仓库中的真实环境分布

Supabase 仓库内多个包各自维护 Vitest 配置,其环境选择正好印证了上述原则:

从源码结构看,仓库的环境选择呈现清晰分层:UI 组件层用 jsdom,逻辑/文档层保持 node。值得注意的是 apps/studio/vitest.config.ts 中有一行注释:

environment: 'jsdom', // TODO(kamil): This should be set per test via header in .tsx files only

这表明团队正倾向于用下一节介绍的"魔法注释"按文件精细控制环境,而非全局一刀切。

二、环境配置与包安装

基础配置

vitest.config.ts 中通过 test.environment 指定环境,并可配合 environmentOptions 传入环境专属参数:

// vitest.config.ts
defineConfig({
  test: {
    environment: 'jsdom',

    // Environment-specific options
    environmentOptions: {
      jsdom: {
        url: 'http://localhost',
      },
    },
  },
})

安装环境包

jsdom 与 happy-dom 不是 Vitest 内置的,需要作为开发依赖单独安装:

# jsdom
npm i -D jsdom

# happy-dom (faster, fewer APIs)
npm i -D happy-dom

Supabase 仓库采用 pnpm workspace 管理版本:各包通过 "vitest": "catalog:" 引用统一版本(见 apps/studio/package.json),而 packages/ai-commands/package.json 这类 node 环境包则不需要引入任何 DOM 模拟库。

三、按文件切换环境:@vitest-environment 魔法注释

当同一工程中既有纯逻辑测试、又有 DOM 测试时,不必为每个目录拆项目——直接在测试文件顶部加一行注释即可覆盖该文件的环境:

// @vitest-environment jsdom

import { expect, test } from 'vitest'

test('DOM test', () => {
  const div = document.createElement('div')
  expect(div).toBeInstanceOf(HTMLDivElement)
})

这个注释的作用范围仅限当前文件,优先级高于配置文件中的全局 environment,是处理"混合环境"最轻量的手段,也是 Supabase Studio 配置中 TODO 注释所指的目标做法。

四、jsdom 环境实战:DOM 操作与 API 覆盖

jsdom 提供完整的浏览器环境模拟,可以直接操作 DOM、访问 window API:

// @vitest-environment jsdom

test('DOM manipulation', () => {
  document.body.innerHTML = '<div id="app"></div>'

  const app = document.getElementById('app')
  app.textContent = 'Hello'

  expect(app.textContent).toBe('Hello')
})

test('window APIs', () => {
  expect(window.location.href).toBeDefined()
  expect(localStorage).toBeDefined()
})

jsdom 专属选项

environmentOptions.jsdom 支持以下参数(与 jsdom 构造选项对齐):

defineConfig({
  test: {
    environmentOptions: {
      jsdom: {
        url: 'http://localhost:3000',   // window.location 的初始值
        html: '<!DOCTYPE html><html><body></body></html>', // 初始文档
        userAgent: 'custom-agent',       // navigator.userAgent
        resources: 'usable',            // 是否加载外部资源(CSS/图片等)
      },
    },
  },
})

其中 url 直接影响依赖 location.href 的断言与路由逻辑;resources: 'usable' 会启用外部资源加载,仅在测试确实需要时开启,否则会拖慢测试速度。

jsdom 的短板:缺失 API 需要 Polyfill

jsdom 模拟的是"结构完整"的浏览器,而非"能力完整"的浏览器——matchMediaResizeObserverscrollIntoView 等 API 并不实现。Supabase 仓库的两个 Polyfill 文件是处理这些短板的范例:

Studio 的全局 Polyfill(通过 apps/studio/vitest.config.tssetupFiles 引入,实现在 apps/studio/tests/setup/polyfills.ts):

  • 使用 jsdom-testing-mocksconfigMocks({ act }) 自动 patch scrollTo 等滚动 API 并接入 React 的 act
  • 手工 mock window.matchMedia,返回一个包含 matchesaddListenerremoveListeneraddEventListener 等完整结构的函数——这是大量响应式 UI 组件库测试的必备项;
  • 从 Node 原生模块把 TextDecoder/TextEncoderReadableStream/TransformStream 挂载到全局(jsdom 默认不提供 Web Streams);
  • window.localStorage.getItem 不可用时,用 Map 实现一个完整的 localStorage 代理(含 keylength getter)并挂到 windowglobalThis 上;
  • 补充 window.HTMLElement.prototype.hasPointerCapture 等缺失原型方法。

文件内还有一段值得注意的注释提醒:restoreMocks: true 会导致这里的 global mockImplementation 在测试开始前被重置,说明 Polyfill 时机与 Vitest mock 恢复策略之间存在交互,配置时需注意。

UI 组件包的 Polyfillpackages/ui-patterns/vitest.setup.ts)则展示了最小化组合:mock matchMediaResizeObserverscrollIntoView,mock next/navigationnext-router-mock,并导入 @testing-library/jest-dom/vitest 扩展断言,最后在 afterEach 中执行 cleanup()。两个文件放在一起对照可以看出:jsdom 环境的生产级配置 = 环境选择 + setupFiles 中成体系的 Polyfill,缺一不可。

五、happy-dom:更快的替代

happy-dom 的 DOM API 覆盖少于 jsdom,但执行速度更快,适合只需要基础 DOM 操作(创建元素、设置属性/类名)的测试:

// @vitest-environment happy-dom

test('basic DOM', () => {
  const el = document.createElement('div')
  el.className = 'test'
  expect(el.className).toBe('test')
})

选型建议:如果用例只依赖 createElement/属性读写这类基础能力,happy-dom 是更经济的选择;一旦需要较复杂的 CSS 解析、事件代理或较新的 DOM 标准,就回到 jsdom。Supabase 仓库当前全部采用 jsdom,说明其 UI 测试对 DOM 能力的依赖超出了 happy-dom 的覆盖范围。

六、多环境共存:使用 projects 拆分

当项目需要"单元测试走 node、DOM 测试走 jsdom"的结构性分工时,用 test.projects 按目录划分环境,每个 project 拥有独立的 include 与 environment:

defineConfig({
  test: {
    projects: [
      {
        test: {
          name: 'unit',
          include: ['tests/unit/**/*.test.ts'],
          environment: 'node',
        },
      },
      {
        test: {
          name: 'dom',
          include: ['tests/dom/**/*.test.ts'],
          environment: 'jsdom',
        },
      },
    ],
  },
})

Supabase 仓库本身采用的是另一种"多环境并存"形态:不在单工程内拆 projects,而是把 node 环境(apps/docs/vitest.config.tsapps/www/vitest.config.ts)与 jsdom 环境(Studio、UI 包)拆成独立包、各自持有 vitest 配置——两者殊途同归,核心都是让不同测试群体落在最合适的运行时里

七、编写自定义 Environment

当内置环境都不贴合需求(例如需要预置全局状态、自定义 viteEnvironment 行为)时,可以实现 Environment 接口:

// vitest-environment-custom/index.ts
import type { Environment } from 'vitest/runtime'

export default <Environment>{
  name: 'custom',
  viteEnvironment: 'ssr', // or 'client'

  setup() {
    // Setup global state
    globalThis.myGlobal = 'value'

    return {
      teardown() {
        delete globalThis.myGlobal
      },
    }
  },
}

然后在配置中引用:

defineConfig({
  test: {
    environment: 'custom',
  },
})

setup() 在测试文件执行前运行,其返回的 teardown 在结束后清理,保证环境不跨文件泄漏——这一"setup/teardown 成对"的约束与 Vitest 的钩子生命周期一致。

Environment + VM:完全隔离

需要把测试代码放进独立 VM 上下文(彻底隔离全局对象)时,自定义环境可实现 setupVM()

export default <Environment>{
  name: 'isolated',
  viteEnvironment: 'ssr',

  async setupVM() {
    const vm = await import('node:vm')
    const context = vm.createContext()

    return {
      getVmContext() {
        return context
      },
      teardown() {},
    }
  },

  setup() {
    return { teardown() {} }
  },
}

getVmContext() 返回的 Node vm 上下文将成为测试代码的真实全局环境,适合测试对沙箱隔离本身有要求的模块。

八、Browser Mode:真浏览器测试,而非环境

必须区分两个概念:environment 是在进程内模拟浏览器,而 Browser Mode 是把测试真正跑在真实浏览器中。对于像素级渲染、真实事件循环、WebGL 等场景,环境模拟永远不够,应启用:

defineConfig({
  test: {
    browser: {
      enabled: true,
      name: 'chromium', // or 'firefox', 'webkit'
      provider: 'playwright',
    },
  },
})

判断标准:如果你的测试在 jsdom 里通过、在真浏览器里失败(或反过来),说明被测代码依赖了模拟层与真实实现的行为差异,此时该用例应迁入 Browser Mode,而不是继续给 jsdom 打补丁。

九、CSS 与静态资源处理

在 jsdom/happy-dom 下,CSS 是否参与处理需要显式声明。默认情况下 Vitest 会跳过 CSS 转换,仅当组件依赖 CSS Modules 生成的类名映射时才需要开启:

defineConfig({
  test: {
    css: true, // Process CSS

    // Or with options
    css: {
      include: /\.module\.css$/,
      modules: {
        classNameStrategy: 'non-scoped',
      },
    },
  },
})

css: { include: /\.module\.css$/ } 表示只处理 CSS Module 文件,其余样式依旧被忽略,兼顾了测试速度与类名断言的准确性;classNameStrategy: 'non-scoped' 控制模块类名是否保留 hash 作用域。

十、外部依赖报错:用 server.deps.inline 内联

当 node_modules 中某个包因 CSS import、资源引用或浏览器专属写法导致解析失败时,说明 Vitest 把它当作"外部依赖"直接执行了。解决办法是将其拉入 Vite 转换管线:

defineConfig({
  test: {
    server: {
      deps: {
        inline: ['problematic-package'],
      },
    },
  },
})

被 inline 的包会经过 Vite 的完整转换(JSX、CSS、别名等),代价是速度略降,因此应尽量只内联真正出错的包,而非通配全部。

十一、关键要点小结

  • 默认环境是 node,无浏览器 API——纯逻辑测试保持默认即可(Supabase 的 apps/wwwapps/docs 即如此);
  • 完整浏览器模拟用 jsdom,基础 DOM 且追求速度用 happy-dom(Supabase 的 apps/studio 与 UI 包均为前者);
  • 单文件切环境用 // @vitest-environment 注释,多套环境并存用 test.projects 拆分;
  • jsdom 的缺失 API(matchMedia、ResizeObserver、Streams、localStorage 等)需在 setupFiles 中成体系地 Polyfill,可参考 apps/studio/tests/setup/polyfills.ts 与 packages/ui-patterns/vitest.setup.ts 的实际写法;
  • Browser Mode 面向真实浏览器,与 environment 是两个独立维度,不要混为一谈;
  • CSS 处理与依赖内联(server.deps.inline)是 DOM 环境下最常见的两类报错来源,分别用 css 选项和 inline 列表针对性解决。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384