首页
/ Storybook MCP test-run 工具实战:运行 Button 故事测试并显式关闭 a11y 检查

Storybook MCP test-run 工具实战:运行 Button 故事测试并显式关闭 a11y 检查

2026-09-06 17:54:56作者:江焘钦

本文以 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,全文仅三条指令,但信息密度很高:

  1. 使用 Storybook MCP 测试工具,为 Button 组件的故事运行测试;
  2. 必须使用 test-run 工具,并汇报哪些故事通过、哪些失败;
  3. 通过给 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 属性确实阻止了事件派发。

这里的 expectfnuserEventwithin 全部来自 storybook/test,是 Storybook 内置的测试原语组合:fn() 创建带调用记录的 mock,userEvent 模拟真实用户交互,within(canvasElement) 把查询范围限定在当前故事的渲染区域。

三、核心工具 test-run:调用方式与 a11y 参数

3.1 工具的多种调用形态

test-run 是 Storybook MCP 工具集中的故事测试工具(MCP 插件本体位于 code/addons/mcp)。从评测基础设施的代码可以确认它在不同环境下有三种等价的调用形态:

  1. MCP 工具调用:在 Agent 会话中作为 MCP 工具直接调用,工具名为 test-run(在 Claude 类客户端中呈现为 mcp__storybook-dev-mcp__test-run 形式);
  2. CLI 直调storybook ai test-run --json '{"stories":[{"storyId":"example-button--primary"}]}',其中 --json 后的对象就是工具输入参数;
  3. 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: falsea11yStatuses: []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);
  });
});

关键断言有两条:

  1. expectWorkflowCalls(['test-run'])——要求 Agent 的会话记录中存在对工作流命令 test-run 的调用;
  2. 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 测试工具参数语义的范例,也是观察官方如何精确校验"显式参数调用"行为的参考实现。

登录后查看全文
热门项目推荐
相关项目推荐