> ## Documentation Index
> Fetch the complete documentation index at: https://docs.devin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 在每次 PR 之前发现视觉回归问题

export const UseCaseHero = ({title, description, prompt, category, features, devinUrl, agent, intent, playbookId, type}) => {
  const encodedPrompt = encodeURIComponent(prompt || '');
  const tag = 'docs-use-case-gallery';
  const utm = 'utm_source=docs&utm_medium=use-case-gallery&utm_campaign=hero-cta';
  const agentParams = (agent ? '&agent=' + agent : '') + (intent ? '&intent=' + intent : '') + (playbookId ? '&playbookId=' + playbookId : '');
  const devinHref = type === 'schedule' ? 'https://app.devin.ai/settings/schedules/create?' + utm + agentParams + (prompt ? '&prompt=' + encodedPrompt : '') : type === 'review' ? 'https://app.devin.ai/review?' + utm : agent === 'ada' ? 'https://app.devin.ai/search?' + utm + '&noSubmit=true' + (prompt ? '&prompt=' + encodedPrompt : '') : devinUrl ? devinUrl.includes('?') ? devinUrl + '&' + utm + agentParams : devinUrl + '?' + utm + agentParams : prompt ? 'https://app.devin.ai/?tags=' + tag + '&' + utm + agentParams + '&prompt=' + encodedPrompt : 'https://app.devin.ai/?' + utm + agentParams;
  const buttonLabel = type === 'schedule' ? 'Schedule in Devin ↗' : type === 'review' ? 'Set Up Devin Review ↗' : agent === 'advanced' ? 'Try in Devin ↗' : agent === 'dana' ? 'Try in Dana ↗' : agent === 'ada' ? 'Try in Ask Devin ↗' : 'Try in Devin ↗';
  const featureList = features ? features.split(',').map(f => f.trim()) : [];
  return <div className="uc-hero">
      <div className="uc-hero-inner">
        <div className="uc-hero-left">
          <h1 className="uc-hero-title">{title}</h1>
          <p className="uc-hero-desc">{description}</p>
          <div>
            <a href={devinHref} target="_blank" rel="noopener noreferrer" className="try-in-devin-btn">
              {buttonLabel}
            </a>
          </div>
        </div>
        <div className="uc-hero-meta">
          <div className="uc-meta-item">
            <span className="uc-meta-label">Author</span>
            <span className="uc-meta-value">Cognition</span>
          </div>
          <div className="uc-meta-item">
            <span className="uc-meta-label">Category</span>
            <span className="uc-meta-value">{category}</span>
          </div>
          {featureList.length > 0 && <div className="uc-meta-item">
              <span className="uc-meta-label">Features</span>
              <span className="uc-meta-value">{featureList.join(', ')}</span>
            </div>}
        </div>
      </div>
    </div>;
};

export const PromptBlock = ({children, type, agent, intent, playbookId}) => {
  var utm = 'utm_source=docs&utm_medium=use-case-gallery&utm_campaign=prompt-block';
  var tag = 'docs-use-case-gallery';
  var agentParams = (agent ? '&agent=' + agent : '') + (intent ? '&intent=' + intent : '') + (playbookId ? '&playbookId=' + playbookId : '');
  var label = type === 'schedule' ? 'Schedule in Devin' : type === 'playbook' ? 'Create Playbook' : type === 'knowledge' ? 'Add to Knowledge' : agent === 'advanced' ? 'Try in Devin' : agent === 'dana' ? 'Try in Dana' : agent === 'ada' ? 'Try in Ask Devin' : 'Try in Devin';
  var buildUrl = function (text) {
    var encoded = encodeURIComponent(text);
    if (type === 'schedule') return 'https://app.devin.ai/settings/schedules/create?' + utm + agentParams + '&prompt=' + encoded;
    if (type === 'playbook') return 'https://app.devin.ai/settings/playbooks/create?' + utm + '&body=' + encoded;
    if (type === 'knowledge') return 'https://app.devin.ai/knowledge?' + utm + '&body=' + encoded;
    if (agent === 'ada') return 'https://app.devin.ai/search?' + utm + '&noSubmit=true&prompt=' + encoded;
    return 'https://app.devin.ai/?tags=' + tag + '&' + utm + agentParams + '&prompt=' + encoded;
  };
  const ref = React.useRef(null);
  const [href, setHref] = React.useState('#');
  React.useEffect(() => {
    if (!ref.current) return;
    var codeEl = ref.current.querySelector('pre code');
    if (codeEl) {
      var text = codeEl.textContent.trim();
      if (text) setHref(buildUrl(text));
    }
    var header = ref.current.querySelector('[data-component-part="code-block-header"]');
    if (header && !header.querySelector('.prompt-block-devin-link')) {
      var link = document.createElement('a');
      link.href = href;
      link.target = '_blank';
      link.rel = 'noopener noreferrer';
      link.className = 'prompt-block-devin-link';
      link.style.cssText = 'display:inline-flex;align-items:center;gap:6px;text-decoration:none;color:#fff;font-size:11px;font-weight:500;padding:4px 10px;border-radius:6px;white-space:nowrap;background:#317CFF;transition:background 0.2s;margin-left:8px;';
      link.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg> ' + label;
      link.onmouseenter = function () {
        link.style.background = '#2968D9';
      };
      link.onmouseleave = function () {
        link.style.background = '#317CFF';
      };
      header.appendChild(link);
    }
    var existingLink = ref.current.querySelector('.prompt-block-devin-link');
    if (existingLink && href !== '#') existingLink.href = href;
  });
  return <div className="prompt-block" ref={ref}>{children}</div>;
};

<UseCaseHero title="在每次 PR 之前捕捉视觉回归问题" description="一种仓库技能，可让 Devin 在打开 PR 之前，对每个受影响的页面进行截图并标记布局错乱问题。" prompt="在 .agents/skills/visual-regression/visual-regression.md 中创建一个仓库技能（repository skill），告诉 Devin 如何启动我们的 Next.js 应用，遍历当前 diff 涉及的每个页面，在桌面端和移动端视口宽度下进行截图，并标记任何布局问题——在打开 PR 之前完成这些操作。" category="功能开发" features="Skills" />

<div className="uc-detail-wrapper">
  <Tip>不想手动设置这些内容？把此页面的链接粘贴到 Devin 会话中，让它帮你完成全部设置。</Tip>

  <Steps>
    <Step title="问题：CSS Bug 容易逃过单元测试">
      你的测试套件全部通过，CI 全绿，PR 也已经合并 —— 但现在设置页面在移动端出现文字重叠，结账按钮被页脚挡住了。单元测试和 linter 无法捕捉这类视觉问题，于是问题被带到生产环境，最后由客户来反馈。

      一个仓库级 skill 可以解决这个问题。Skills 是你提交到任意仓库中 `.agents/skills/<your-skill>/` 路径下的 markdown 文件。Devin 能看到所有已连接仓库中的全部 skills —— 你可以手动触发它们，或者当 Devin 检测到相关场景时，由 Devin 自动选择触发。这个 skill 会精确告诉 Devin 如何启动你的应用、如何访问受 diff 影响的页面，并在多个视口宽度下为这些页面截图 —— 每一次、在每个 PR 之前都会执行。
    </Step>

    <Step title="将 visual-regression 技能添加到你的代码库">
      在 `.agents/skills/visual-regression/visual-regression.md` 中提交一个文件，将你团队的手动 QA 检查清单编写为 Devin 可以遵循的步骤：

      ```markdown theme={null}
      # 视觉回归检查

      ## 描述
      在提交任何 PR（拉取请求）之前，请在本地启动应用程序，并对当前差异所影响的每个页面分别在桌面端和移动端宽度下截图。标记发现的任何布局问题。

      ## 前提条件
      - Docker 正在运行（用于数据库）
      - 端口 3000 可用

      ## 设置
      1. 安装依赖项：`npm install`
      2. 启动服务：`docker-compose up -d postgres redis`
      3. 运行迁移：`npx prisma migrate dev`
      4. 写入测试数据：`npm run db:seed`
      5. 启动开发服务器：`npm run dev`
      6. 等待终端中出现 "Ready on http://localhost:3000"

      ## 视觉检查
      1. 读取当前 git diff，确定哪些页面受到影响
      2. 对每个受影响的页面：
         a. 在浏览器中打开 http://localhost:3000/{route}
         b. 在 1280px 宽度（桌面端）下截图
         c. 将浏览器调整为 375px 宽度（移动端）
         d. 在移动端宽度下截图
         e. 检查以下问题：文字重叠、元素被其他元素遮挡、出现水平滚动条、按钮或链接无法点击、图片或图标缺失、控制台报错
      3. 如发现任何问题，请记录页面 URL、视口宽度及问题描述

      ## 提交 PR 前
      1. 运行 `npm run lint` 并修复所有问题
      2. 运行 `npm test` 并确认所有测试通过
      3. 在 PR 描述中附上所有截图
      4. 如发现问题，请在 PR 正文顶部列出
      ```

      此文件会随你的代码一起分发。一旦提交，Devin 就会将其视为一项可用技能——每当有会话涉及此仓库中的 UI 文件时，Devin 都可以自动触发视觉回归检查，或者你也可以在任意时间点手动执行这些检查。
    </Step>

    <Step title="使检查针对你的各个页面">
      像 “check the page looks right” 这样的笼统指令会产生模糊的结果。这个技能中最有价值的部分，是告诉 Devin 在你应用的每个部分中，什么才算是「正确」的表现。

      在你的技能中添加针对特定路由的单独小节：

      ```markdown theme={null}
      ## Route-Specific Checks

      ### Dashboard (`/dashboard`)
      - The metric cards should display in a 3-column grid on desktop
      - Cards should stack to a single column on mobile
      - The chart should be fully visible without horizontal scroll

      ### Checkout (`/checkout`)
      - The "Place Order" button must be visible without scrolling
        on both desktop and mobile
      - The order summary sidebar should collapse into an accordion on mobile
      - The price breakdown should show subtotal, tax, and total on separate lines

      ### Settings (`/settings`)
      - All form labels should be left-aligned with their inputs
      - The "Save" button must be reachable at the bottom of the form
      - Tab navigation between sections should update the URL hash
      ```

      Devin 会读取 diff，识别哪些路由受到影响，并跟进对应部分——从而让检查更有针对性，而不是泛泛而为。
    </Step>

    <Step title="看看它如何发现实际的回归问题">
      假设一名工程师为了实现响应式布局，把仪表盘上的 CSS grid 从 `grid-template-columns: repeat(3, 1fr)` 改成了 `repeat(auto-fit, minmax(200px, 1fr))`。单元测试全部通过——逻辑没有任何改动。但在移动端，这些卡片现在会溢出视口。

      当 Devin 完成这次代码修改后，它会自动按该技能执行以下步骤：

      1. **启动应用** —— 安装依赖、启动 Docker、运行数据库迁移、灌入数据、启动开发服务器
      2. **阅读 diff** —— 发现 `src/components/Dashboard.tsx` 被修改，将其映射到 `/dashboard` 路由
      3. **在 1280px 截图** —— 3 列网格渲染正确
      4. **在 375px 截图** —— 发现水平溢出：卡片在视口内放不下
      5. **标记问题** —— 报告 “Horizontal scroll detected on /dashboard at 375px width — metric cards overflow the viewport”
      6. **修复 CSS** —— 添加 `overflow-x: hidden`，并把 `minmax` 的最小值调整为 `150px`
      7. **重新截图** —— 在两个宽度下都确认修复生效
      8. **打开 PR** —— 在描述中附上修复前/修复后的截图和变更说明

      你可以在你的[会话工作区](/zh/work-with-devin/devin-session-tools)中，通过浏览器标签页实时看到整个过程。
    </Step>

    <Step title="扩展到你的技术栈">
      根据你的项目，扩展该技能以支持额外的工具和验证步骤：

      <PromptBlock>
        ```txt Add Storybook component checks theme={null}
        将 visual regression 技能更新为还会运行
        `npm run storybook`，打开 http://localhost:6006，并为当前 diff 中
        修改过的每个组件对应的 story 截图。标记所有出现渲染错误或画布为空白的 story。
        ```
      </PromptBlock>

      <PromptBlock>
        ```txt Add dark mode verification theme={null}
        将 visual regression 技能更新为还会将应用切换到深色模式
        （点击右上角的主题切换按钮），并重新为所有受影响的页面截图。
        标记所有在深色背景下变得难以辨认的文本，以及所有边框消失的元素。
        ```
      </PromptBlock>
    </Step>
  </Steps>
</div>
