DSH / Atlas
2026-07-30implementedtesting

Keep browser storage owned by jsdom in Vitest

在 Vitest 中将浏览器存储交由 jsdom 管理

The supported Node range includes releases that reserve a process-wide `globalThis.localStorage`. Node 26 exposes that property as `undefined` without `--localstorage-file`; Vitest sees the reserved key and does not project jsdom's isolated `Storage` object over it. Component suites then fail before exercising product behavior, while the primary Node 24 coverage lane remains green because that runtime does not reserv

English

Problem

The supported Node range includes releases that reserve a process-wide globalThis.localStorage. Node 26 exposes that property as undefined without --localstorage-file; Vitest sees the reserved key and does not project jsdom's isolated Storage object over it. Component suites then fail before exercising product behavior, while the primary Node 24 coverage lane remains green because that runtime does not reserve the key by default.

Decision

Vitest workers disable Node's process-wide Web Storage when the runtime advertises the --webstorage flag. The configuration passes --no-webstorage through each test project's execArgv; runtimes without that flag receive no argument. Node-environment suites therefore stay browser-free, and files selecting jsdom through @vitest-environment jsdom receive jsdom's isolated localStorage.

The Node compatibility aggregate runs a dedicated jsdom smoke on every advertised compatibility line. It asserts both the conditional worker argument and usable storage, so a future Node or Vitest change cannot leave the primary Node 24 suite as the only signal.

Alternatives considered

  • Set NODE_OPTIONS=--no-webstorage in package scripts or CI. Rejected because it leaks test-runner policy into subprocesses and misses direct pnpm exec vitest invocations.
  • Pass --localstorage-file to Node. Rejected because one process-wide persistent store has different ownership and isolation semantics from browser storage created per jsdom environment.
  • Patch globalThis.localStorage in setup code or guard every component test. Rejected because setup would depend on Vitest's private jsdom projection details, while per-test guards hide a broken browser environment and duplicate policy across suites.
  • Pin tests to Node 24. Rejected because the package engine advertises newer even Node lines and the compatibility matrix exists to expose their runtime changes.

Consequences

The same pnpm test command works on Node releases with and without built-in Web Storage. Test workers deliberately cannot exercise Node's process-wide Web Storage; a future product need for that API requires a separate explicit test configuration rather than weakening jsdom isolation. The compatibility lane adds one focused Vitest process instead of duplicating the complete unit inventory on every Node version.

中文

问题

受支持的 Node 版本范围包含会预留进程级 globalThis.localStorage 的版本。未设置 --localstorage-file 时,Node 26 将该属性暴露为 undefined;Vitest 检测到这个预留键后,不会用 jsdom 的隔离 Storage 对象覆盖该属性。因此,组件测试套件尚未验证产品行为便会失败,而主要的 Node 24 覆盖率通道仍能通过,因为该运行时默认不会预留此键。

决策

当运行时声明支持 --webstorage 标志时,Vitest worker 会禁用 Node 的进程级 Web Storage。配置通过每个测试项目的 execArgv 传入 --no-webstorage;未声明该标志的运行时则不传入此参数。因此,Node 环境测试套件不加载浏览器环境,而通过 @vitest-environment jsdom 选择 jsdom 的文件会获得 jsdom 隔离的 localStorage

Node 兼容性汇总任务会在每条声明支持的兼容版本线上运行专用的 jsdom 冒烟测试。该测试同时断言 worker 参数按条件传入且存储可用,因此未来 Node 或 Vitest 的变化不会让主要的 Node 24 测试套件成为唯一检测信号。

曾考虑的替代方案

  • 在包脚本或 CI 中设置 NODE_OPTIONS=--no-webstorage 否决:这会将测试运行器策略传播到子进程,也无法覆盖直接调用 pnpm exec vitest 的情况。
  • 向 Node 传入 --localstorage-file 否决:单个进程级持久化存储与每个 jsdom 环境分别创建的浏览器存储具有不同的归属和隔离语义。
  • 在初始化代码中修改 globalThis.localStorage,或为每个组件测试增加保护逻辑。 否决:初始化逻辑会依赖 Vitest 私有的 jsdom 映射细节,而逐测试添加的保护逻辑会掩盖浏览器环境损坏,并在多个测试套件中重复该策略。
  • 将测试固定在 Node 24。 否决:包的引擎范围声明支持更新的偶数 Node 版本线,而兼容性矩阵正是为了暴露这些版本的运行时变化。

后果

同一条 pnpm test 命令在有无内置 Web Storage 的 Node 版本上均可运行。测试 worker 被有意禁止使用 Node 的进程级 Web Storage;未来若产品需要该 API,必须使用独立且显式的测试配置,而不能削弱 jsdom 隔离。兼容性通道只增加一个专项 Vitest 进程,无需在每个 Node 版本上重复整套单元测试。