在 Storybook 交互测试中使用 `storybook/test`:eslint-plugin-storybook 的 use-storybook-testing-library 规则完全指南
本指南围绕
eslint-plugin-storybook中use-storybook-testing-library规则展开,讲解为什么 Story 文件的 play 函数必须从storybook/test引入 Testing Library 辅助函数,而不是直接使用@testing-library/*的原始包;同时结合仓库源码分析该规则的实现原理、自动修复能力、内置配置与覆盖/禁用方式,帮助你在 Storybook 项目里写出可被 addon-interactions 逐步调试的交互测试代码。
规则解决什么问题
Storybook 在 storybook/test 核心包(仓库内位于 code/core/src/test,早期版本发布在独立的 @storybook/testing-library 库中)中提供了一个经过插桩(instrumented)的 Testing Library 版本。
storybook/test 并不是对 Testing Library 的简单重新导出。查看实现 code/core/src/test/testing-library.ts 可以看到,它会通过 storybook/internal/instrumenter 的 instrument 函数包装完整的 @testing-library/dom API,并对 fireEvent、以 find / waitFor 开头的方法进行拦截;fireEvent 被处理为可 Promise 化的形式。这套插桩机制正是 addon-interactions 能够"看见"每次用户交互、并允许你在调试面板中逐步回放的前提。
因此,当你在 Story 的 play 函数中编写交互时,必须使用来自 storybook/test 的辅助函数,而非直接 import 原始 Testing Library。规则 use-storybook-testing-library(规则名全称 storybook/use-storybook-testing-library)就是为了杜绝后一种用法而存在。
错误写法与正确写法
根据规则文档 code/lib/eslint-plugin/docs/rules/use-storybook-testing-library.md,下面是被判为错误的代码:
// wrong import!
import { within } from '@testing-library/react';
Default.play = async (context) => {
const canvas = within(context.canvasElement);
};
问题不在于 within 本身,而在于它来自未被插桩的裸包 @testing-library/react。这样的调用 addon-interactions 无法拦截,交互过程也就无法被记录与逐步调试。
被判为正确的代码:
// correct import.
import { within } from 'storybook/test';
// or this, which is now considered legacy
import { within } from '@storybook/testing-library';
Default.play = async (context) => {
const canvas = within(context.canvasElement);
};
两种合规写法对应了 storybook/test 演进过程中的两个阶段:storybook/test 是当前推荐的统一入口,而 @storybook/testing-library 已被视为 legacy,仅作兼容保留。在实际项目里应优先使用 storybook/test。
规则的实现原理
规则完整实现位于 code/lib/eslint-plugin/src/rules/use-storybook-testing-library.ts,整体通过 createStorybookRule(见 utils/create-storybook-rule.ts)构建。其元信息明确声明:
- 规则类型为
suggestion; - 可通过
fixer自动修复(fixable: 'code'),同时提供可选的suggest修复建议; - 默认严重级别为
error; - 归属分类为
addon-interactions与recommended(对应 utils/constants.ts 中的CategoryId.ADDON_INTERACTIONS/CategoryId.RECOMMENDED)。
监听 ImportDeclaration
规则核心逻辑并不分析调用关系,而是只检查 ImportDeclaration 节点:
ImportDeclaration(node) {
if (node.source.value.includes('@testing-library')) {
context.report({
node,
messageId: 'dontUseTestingLibraryDirectly',
data: {
library: node.source.value,
},
// ...
});
}
}
也就是只要 import 的模块源字符串包含 @testing-library(例如 @testing-library/dom、@testing-library/user-event、@testing-library/react),就会触发报告。对应报错信息为:
Do not use
{{library}}directly in the story. You should import the functions fromstorybook/testinstead.
自动修复:包名替换 + 默认导入扁平化
自动修复包含两步操作:
- 替换包名:利用
getRangeWithoutQuotes去掉字符串两端的引号后,把源码从@testing-library/*改写为storybook/test; - 扁平化默认导入:由于
storybook/test只暴露命名导出,原先的默认导入必须改写成命名导入。getSpecifiers会先基于 specifier 的 range 抓取导入片段(实现上还处理了 specifier range 不包含右花括号}这一边界情况),再由fixSpecifiers去掉{}并把连续空白压缩,最终重新拼成{ ... }的命名导入形式。
例如修复器会把:
import userEvent, { foo, bar as Bar } from '@testing-library/user-event';
自动改写为:
import { userEvent, foo, bar as Bar } from 'storybook/test';
默认导入 userEvent、命名导入 foo、重命名导入 bar as Bar 都完整保留,只是全部收敛进花括号。
测试用例佐证
单元测试见 code/lib/eslint-plugin/src/rules/use-storybook-testing-library.test.ts,其中 valid 用例仅有一个:import { within } from 'storybook/test'。三类 invalid 用例分别覆盖:
@testing-library/dom的命名导入:import { within } from '@testing-library/dom'→import { within } from 'storybook/test';@testing-library/user-event的默认导入:import userEvent from '@testing-library/user-event'→import { userEvent } from 'storybook/test';- 混合默认导入 + 命名导入 + 重命名导入的复杂场景,验证扁平化逻辑对多种 specifier 的兼容性。
每个非法用例都同时断言了报错信息(dontUseTestingLibraryDirectly + library 数据)以及 updateImports 建议与自动修复后的输出结果。
规则被哪些配置启用
规则文档头部明确列出,use-storybook-testing-library 包含在以下四种配置中:
addon-interactionsflat/addon-interactionsrecommendedflat/recommended
以 legacy 配置 src/configs/addon-interactions.ts 与 flat 配置 src/configs/flat/addon-interactions.ts 为例,两处都对该规则配置为 'error',并且规则只作用于两类 Story 文件通配:
**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)
**/*.story.@(ts|tsx|js|jsx|mjs|cjs)
即在默认情况下,普通组件源码文件不会受到该规则影响,只有 *.stories.* / *.story.* 才会被检查。
在项目中启用规则
use-storybook-testing-library 属于 eslint-plugin-storybook 内置规则,无需单独安装依赖,启用该插件并挂载对应配置即可。
legacy .eslintrc 用法
首先安装插件(插件自带了所需的 CSF 辅助代码,不需要仅为加载 ESLint 插件而额外安装 storybook,这一点对 monorepo 中共享的 ESLint preset 尤其方便):
npm install eslint-plugin-storybook --save-dev
# 或
yarn add eslint-plugin-storybook --dev
然后在 .eslintrc.* 的 extends 中加入包含该规则的配置,例如 plugin:storybook/recommended:
{
"extends": ["plugin:storybook/recommended"]
}
flat config(ESLint v9 / v8.57+)用法
在 eslint.config.[c|m]?js 中导入插件并展开 flat/recommended:
import storybook from 'eslint-plugin-storybook';
export default [
// ...其他规则集
...storybook.configs['flat/recommended'],
];
如果配合 tseslint.config 之类的工具函数使用,注意不要解构,应整体传入:
import storybook from 'eslint-plugin-storybook';
import tseslint from 'typescript-eslint';
export default tseslint.config(
somePlugin,
storybook.configs['flat/recommended'] // notice that it is not destructured
);
关于插件与 ESLint 的版本对应关系,仓库内插件 README code/lib/eslint-plugin/README.md 给出的表格为:ESLint ^9.0.0 或 ^8.57.0 对应插件 ^0.10.0,ESLint ^7.0.0 对应插件 ~0.9.0(具体版本请以你安装的版本为准)。
什么时候不应该启用它(覆盖与禁用)
规则文档明确提示:这条规则不应该被应用在普通测试文件中,请确保 Storybook 相关的规则只作用于 Story 文件。如果某些文件确实需要直接从 @testing-library/* 导入(例如独立的组件测试、非 Story 场景下的单测),应当通过 overrides 把规则关掉或按文件匹配范围隔离。
.eslintrc.* 覆盖写法
{
"overrides": [
{
"files": ["**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)"],
"rules": {
// 其他文件下禁用,仅故事文件保持 error
"storybook/use-storybook-testing-library": "off"
}
}
]
}
files 通配只需匹配你在 .storybook/main.js 中声明的 stories 扫描范围即可。
flat config 覆盖写法
import storybook from 'eslint-plugin-storybook';
export default [
...storybook.configs['flat/recommended'],
{
files: ['**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)'],
rules: {
'storybook/use-storybook-testing-library': 'off',
},
},
];
补充:eslintignore 与配置文件
插件 README 还建议在 .eslintignore 中加入 !.storybook(flat config 下使用 globalIgnores(['!.storybook'], ...)),让插件也能检查 .storybook/main.* 配置文件,从而避免拼错 addon 名导致的问题;只有 .storybook/main.* 文件才会被 storybook/no-uninstalled-addons 规则检查。此外需要留意,本插件不支持 MDX 文件。
关联的姊妹规则
use-storybook-testing-library 与另一条规则 storybook/use-storybook-expect 是同一思路下的"左右手":前者强制交互辅助函数来自 storybook/test,后者强制断言函数 expect 同样来自 storybook/test(或 @storybook/jest)。两者同属于 addon-interactions 与 recommended 配置,共同保证 play 函数中的每一步操作与每一次断言都处于 addon-interactions 的插桩可见范围内,从而在调试面板里获得完整的"操作—断言"逐步回放体验。
小结
- 强制约束:Story 的 play 函数中不要直接 import 任何
@testing-library/*包,统一从storybook/test引入within、fireEvent、userEvent等辅助函数; - 自动保障:规则不仅能以
error级别拦截非法 import,还能通过自动修复一键完成包名替换与默认导入扁平化,迁移成本极低; - 默认开启:
addon-interactions、flat/addon-interactions、recommended、flat/recommended四种配置均默认启用,且只扫描*.stories.*/*.story.*文件; - 按需关闭:对普通测试文件可通过
overrides精准关闭,避免误伤非 Story 场景下合法的 Testing Library 用法。
坚持使用 storybook/test,是让交互测试具备可调试性的关键一步——只有当辅助函数经过 Storybook 插桩,addon-interactions 才能捕捉到它们,你也才能在 UI 调试面板中真正"看到"并逐步走完每一次用户交互。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00