Storybook MCP test-run 工具实战:运行 Button 故事测试并显式关闭 a11y 检查
本文以 Storybook 官方仓库中的 Agent 评测用例 910-run-tests-without-a11y-explicit 为核心,讲解如何用 Storybook 的 MCP 测试工具 test-run 针对指定组件故事运行自动化测试,以及如何通过 { a11y: false } 参数显式关闭 a11y(无障碍)检查。读完后你将掌握 test-run 工具的完整调用方式(MCP 工具名与 CLI 形态)、a11y 参数的作用机制,以及官方评测如何校验 Agent 是否正确地"带参调用"了该工具。
一、评测任务原文:三句话构成的明确指令
该评测的指令文档位于 agent-eval/evals/910-run-tests-without-a11y-explicit/PROMPT.md,全文仅三条指令,但信息密度很高:
- 使用 Storybook MCP 测试工具,为 Button 组件的故事运行测试;
- 必须使用
test-run工具,并汇报哪些故事通过、哪些失败; - 通过给
test-run传入{ a11y: false }来忽略 a11y 违规。
从评测命名可以看出设计意图:without-a11y-explicit 表示"显式要求关闭 a11y"。这条指令的考点不在于"能否跑测试",而在于 Agent 是否把 a11y: false 作为工具输入参数显式传出去——这正是区别于默认行为的"显式调用"能力。
二、评测工作区:一个最小化的 React Storybook 项目
评测项目由三部分组成,整体基于 reshaped-storybook 模板搭建(见 package.json 中的 "template": "reshaped-storybook",模板源文件位于 agent-eval/templates/reshaped-storybook)。
2.1 被测组件 Button
Button.tsx 是一个极简的受控按钮组件:
type ButtonProps = {
label: string;
onClick?: () => void;
disabled?: boolean;
};
export default function Button({ label, onClick, disabled = false }: ButtonProps) {
return (
<button type="button" onClick={onClick} disabled={disabled} data-testid="button-component">
{label}
</button>
);
}
注意 disabled 属性会透传到原生 <button> 上——这直接导致禁用态按钮存在可访问性语义问题(无响应的 disabled 控件),也正是"关闭 a11y 检查"这条指令存在的原因。
2.2 带 play 函数的故事文件
Button.stories.tsx 定义了两个故事,且 meta 中带 tags: ['test'] 标记,声明这些故事参与测试运行:
import type { Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, within } from 'storybook/test';
import Button from '../src/components/Button';
const meta = {
title: 'Example/Button',
component: Button,
tags: ['test'],
args: {
label: 'Click me',
onClick: fn(),
disabled: false,
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button', { name: 'Click me' });
await userEvent.click(button);
await expect(args.onClick).toHaveBeenCalledTimes(1);
},
};
export const Disabled: Story = {
args: {
label: 'Disabled',
disabled: true,
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button', { name: 'Disabled' });
await userEvent.click(button);
await expect(args.onClick).not.toHaveBeenCalled();
},
};
两个故事各有一个 play 函数:
- Default:在 canvas 内按可访问性角色
button定位名为 "Click me" 的按钮,用userEvent.click模拟点击,断言onClick(由fn()创建的 mock 函数)恰好被调用 1 次; - Disabled:将按钮设为禁用态,模拟点击后断言
onClick没有被调用——验证disabled属性确实阻止了事件派发。
这里的 expect、fn、userEvent、within 全部来自 storybook/test,是 Storybook 内置的测试原语组合:fn() 创建带调用记录的 mock,userEvent 模拟真实用户交互,within(canvasElement) 把查询范围限定在当前故事的渲染区域。
三、核心工具 test-run:调用方式与 a11y 参数
3.1 工具的多种调用形态
test-run 是 Storybook MCP 工具集中的故事测试工具(MCP 插件本体位于 code/addons/mcp)。从评测基础设施的代码可以确认它在不同环境下有三种等价的调用形态:
- MCP 工具调用:在 Agent 会话中作为 MCP 工具直接调用,工具名为
test-run(在 Claude 类客户端中呈现为mcp__storybook-dev-mcp__test-run形式); - CLI 直调:
storybook ai test-run --json '{"stories":[{"storyId":"example-button--primary"}]}',其中--json后的对象就是工具输入参数; - npx 拉起:
npx storybook ai --port 6006 test-run,--port指向已运行的 Storybook 开发服务器。
这些形态均出自评测的解析器与单测,可参见 agent-eval/lib/shell-parse.ts(其中 test-run 被列为受识别的工作流命令)以及 agent-eval/lib/test-utils.test.ts 中的用例,例如:
'storybook ai test-run --json \'{"stories":[{"storyId":"example-button--primary"}],"a11y":false}\''
3.2 { a11y: false } 参数的作用
test-run 的输入参数中,a11y 字段控制是否在故事测试执行时同时进行 a11y 检查。传 { a11y: false } 即只运行故事本身的断言(play 函数、交互断言),跳过无障碍规则检测。
为什么评测场景要显式关闭 a11y?结合上文第 2.1 节:Disabled 故事渲染出一个原生 disabled 按钮,这类控件容易触发 a11y 规则告警(如禁用控件的可聚焦性/可达性问题)。在只关心"功能行为是否正确"的测试中,把 a11y 维度显式关掉可以让测试结果聚焦于交互断言,避免 a11y 违规掩盖或混淆功能失败的判定。这也解释了评测标题中 "without-a11y" 的用意。
在 Storybook 的测试实现层,a11y 结果被建模为独立的状态类型。例如 code/addons/vitest/src/constants.ts 中定义了 STATUS_TYPE_ID_A11Y = 'storybook/a11y',并给出了 a11y: false、a11yStatuses: []、a11yReports: {} 等默认空态结构——从源码结构看,a11y 维度与功能测试维度是分开存储和汇报的,关闭 a11y 后相关状态即保持空态,这为"只报功能结果"提供了实现层面的印证。
四、评测如何校验 Agent 的行为:EVAL.ts 逐行解析
真正的"考题答案"校验逻辑在 EVAL.ts 中,它基于 vitest 断言 Agent 会话的工作流调用记录:
import { describe, expect, test } from 'vitest';
import { expectWorkflowCalls, getWorkflowCalls, type StorybookWorkflowCall } from '#test-utils';
describe('running Button story tests with a11y disabled via an explicit prompt', () => {
function disablesA11y(call: StorybookWorkflowCall): boolean {
return call.input.a11y === false;
}
test('runs Storybook story tests with a11y disabled', () => {
expectWorkflowCalls(['test-run']);
expect(getWorkflowCalls('test-run').some(disablesA11y)).toBe(true);
});
});
关键断言有两条:
expectWorkflowCalls(['test-run'])——要求 Agent 的会话记录中存在对工作流命令test-run的调用;getWorkflowCalls('test-run').some(disablesA11y)——要求至少有一次test-run调用的输入参数满足call.input.a11y === false(严格等于false,而不是未传该字段)。
第二条是精髓:如果 Agent 只运行了 test-run 但没传 a11y: false,断言 a11y === false 会因为字段为 undefined 而失败。也就是说,评测区分了"默认行为下 a11y 恰好没报错"与"按指令显式声明关闭 a11y"两种情况,后者才是符合 PROMPT.md 要求的正确做法。这套解析工具(#test-utils,源码见 agent-eval/lib/test-utils.ts)能从多种会话日志形式(shell 命令、MCP 工具调用行等)中还原出结构化的工作流调用及输入参数,是评测判定"带参调用"的事实来源。
五、实操要点与常见误区
基于该评测及其周边源码,整理出使用 test-run 时的要点:
- 指定故事范围:通过
stories参数传入storyId(如example-button--primary格式),而不是全量运行,这在评测用例中被一致采用; - 显式传参:需要改变默认测试维度(如 a11y)时,必须把参数写进工具输入(
{ a11y: false })或 CLI 的--json载荷中;"默认不触发 a11y" 与 "显式a11y: false" 在评测判定中是两回事; - 结果汇报:PROMPT.md 要求"汇报哪些故事通过/失败",即 Agent 的职责不止于触发测试,还包括解析测试输出并给出逐故事结论;
- 前置条件:
test-run依赖一个可连接的 Storybook 服务(CLI 形态下通过--port指定,如--port 6006),工作区需像本评测的模板一样已配置好storybook/test所需依赖与tags: ['test']标记的故事。
六、小结
910-run-tests-without-a11y-explicit 虽然指令只有三句话,却完整覆盖了 Agent 使用 Storybook 测试工具链的一条关键路径:定位目标故事 → 调用 test-run → 按指令显式注入 a11y: false 参数 → 汇报逐故事结果。配合 agent-eval/evals/910-run-tests-without-a11y-explicit 的断言实现与 agent-eval/lib/test-utils.ts 的调用解析机制,它既是理解 Storybook MCP 测试工具参数语义的范例,也是观察官方如何精确校验"显式参数调用"行为的参考实现。
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 StartedRust0623
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