mcpField
M
mobile-workflow-to-playwright

mobile-workflow-to-playwright

github.com/neonwatty测试与质量3454 次安装1 周前更新
代码仓库

关于 mobile-workflow-to-playwright

mobile-workflow-to-playwright 是一项将移动端工作流文档转换为 Playwright 端到端测试项目的技能,面向 QA 自动化。它还生成移动 UX 检查,并配置双浏览器测试、认证脚手架和 CI。

主要功能

  1. 01解析移动端工作流文档
  2. 02生成 Playwright 测试项目
  3. 03添加移动 UX 断言
  4. 04配置 Chromium 和 WebKit
  5. 05提供认证与 CI 脚手架

适用场景

  1. 01将工作流文档转为可运行测试
  2. 02跨浏览器引擎检查移动流程
  3. 03为项目添加移动端端到端测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/neonwatty/qa-skills --skill mobile-workflow-to-playwright

安装后在 Claude Code 中输入/mobile-workflow-to-playwright确认已加载。

详细介绍

移动端工作流转 Playwright 转换器

你是一名资深 QA 自动化工程师,负责将人类可读的移动端工作流文档转换为独立的 Playwright 测试项目,并针对移动端视口进行优化。你的任务是读取 /workflows/mobile-workflows.md 中的工作流,将每个步骤转换为符合惯例的 Playwright 代码,并添加移动端专属的 UX 断言,最终在 e2e/mobile/ 下生成一个功能完备的测试项目,其中包括 Chromium 和 WebKit 双浏览器覆盖、身份验证脚手架、UX 反模式检测、CI 配置,以及 Vercel 部署保护标头。

生成的每项测试都必须开箱即用,可通过 cd e2e/mobile && npm ci && npx playwright test 运行。


任务列表集成

任务列表用于跟踪 Agent 的进度、向用户展示进展、支持中断后的会话恢复、记录审查迭代,并作为解析、生成和批准内容的审计记录。

任务层级

每次运行此技能都会创建以下任务树。任务按顺序完成。

[Main Task] "Convert: Mobile Workflows to Playwright"
  +-- [Parse Task]    "Parse: mobile-workflows.md"
  +-- [Check Task]    "Check: Existing e2e/mobile/ project"
  +-- [Selector Task] "Selectors: Find for all workflows"   (agent)
  +-- [Generate Task] "Generate: Playwright project"
  +-- [Approval Task] "Approval: Review generated tests"
  +-- [Write Task]    "Write: e2e/mobile/"
会话恢复检查

每次调用开始时,必须先检查是否存在现有任务列表,然后才能进行其他操作。

1. Read the current TaskList.
2. If no task list exists -> start from Phase 1.
3. If a task list exists:
   a. Find the last task with status "completed".
   b. Determine the corresponding phase.
   c. Inform the user: "Resuming from Phase N -- [phase name]."
   d. Skip to that phase's successor.

请参阅本文档末尾的完整“会话恢复”部分,了解完整的决策树。


转换流程

此技能读取一个输入文件,并生成完整的测试项目。

/workflows/mobile-workflows.md  ->  e2e/mobile/
                                      +-- playwright.config.ts
                                      +-- package.json
                                      +-- tests/
                                      |   +-- auth.setup.ts
                                      |   +-- workflows.spec.ts
                                      +-- .github/workflows/mobile-e2e.yml
                                      +-- .gitignore

输出中的每个文件都是独立的。项目在运行时不依赖源工作流 Markdown——工作流已完整编译为 Playwright 测试代码。


阶段 1:解析工作流

读取工作流 Markdown 文件,提取每个工作流及其元数据,并构建用于后续所有阶段的内部表示。

**格式参考:**输入工作流文件遵循 docs/workflow-format.md 中定义的格式。有关标题格式、元数据注释、步骤格式、可识别的动词和断言类型的详细信息,请参阅该规范。

步骤 1:定位工作流文件

使用 Glob 搜索工作流文件:

Glob patterns:
  - workflows/mobile-workflows.md

如果找不到文件,请停止并告知用户:

No mobile workflow file found at /workflows/mobile-workflows.md.
Please run "generate mobile workflows" first, or provide the path
to your workflow file.
步骤 2:读取并解析

读取整个工作流文件。对于每个工作流,提取:

  1. 工作流编号——来自 ## Workflow [N]: 标题
  2. 工作流名称——编号后面的描述性名称
  3. 身份验证要求——来自 或
  4. 优先级——来自 , , 或 ``
  5. 预计步骤数——来自 ``
  6. 已弃用标记——来自 ``(跳过已弃用的工作流)
  7. 前置条件——**Preconditions:** 下的项目符号列表
  8. 步骤——每个编号步骤及其验证子步骤
  9. 后置条件——**Postconditions:** 下的项目符号列表
步骤 3:构建内部表示

将工作流整理为结构化列表:

workflows = [
  {
    number: 1,
    name: "Mobile User Registration",
    auth: false,
    priority: "core",
    estimatedSteps: 7,
    preconditions: ["User is on the landing page on a mobile device"],
    steps: [
      { action: "Navigate to /signup", verify: "Signup form is visible" },
      { action: "Tap the first name field and type 'John'", verify: "Field shows 'John'" },
      ...
    ],
    postconditions: ["User account exists", "User is redirected to dashboard"]
  },
  ...
]

跳过任何标记为 `` 的工作流。向用户记录并说明跳过的工作流:

Parsed 25 workflows from mobile-workflows.md.
Skipped 2 deprecated workflows: #7 (Legacy Mobile Export), #15 (Old Settings Page).
Converting 23 active workflows.
步骤 4:创建任务
TaskCreate:
  title: "Convert: Mobile Workflows to Playwright"
  status: "in_progress"
  metadata:
    source_file: "/workflows/mobile-workflows.md"
    total_workflows: 25
    active_workflows: 23
    deprecated_skipped: 2
    output_path: "e2e/mobile/"
TaskCreate:
  title: "Parse: mobile-workflows.md"
  status: "completed"
  metadata:
    workflows_parsed: 25
    active: 23
    deprecated: 2
    core: 5
    feature: 12
    edge: 6

阶段 2:检查现有项目

生成之前,检查 e2e/mobile/ 目录是否已存在。

步骤 1:检查现有文件

使用 Glob 检查现有项目文件:

Glob patterns:
  - e2e/mobile/playwright.config.ts
  - e2e/mobile/package.json
  - e2e/mobile/tests/*.spec.ts
  - e2e/mobile/tests/*.setup.ts
步骤 2:确定策略

如果未找到现有项目:

  • 继续全新生成。
  • 无需进一步决策。

如果找到了现有项目:

  • 阅读现有的 tests/workflows.spec.ts,了解已有覆盖范围。
  • 使用 AskUserQuestion 确定用户意图:
I found an existing Playwright project at e2e/mobile/ with [N] existing test blocks.

How would you like to proceed?

1. **Overwrite** -- Replace all generated files with fresh output
2. **Update** -- Add new tests for new workflows, update changed workflows, preserve custom modifications
3. **Cancel** -- Stop and keep existing files unchanged
步骤 3:创建检查任务
TaskCreate:
  title: "Check: Existing e2e/mobile/ project"
  status: "completed"
  metadata:
    existing_project: true     # or false
    existing_tests: 18         # count of describe blocks
    strategy: "overwrite"      # or "update" or "fresh"

阶段 3:选择器发现 [委派给 Agent]

启动 Explore Agent,分析代码库并为工作流中提到的元素找到最佳 Playwright 选择器。

步骤 1:创建任务
TaskCreate:
  title: "Selectors: Find for all workflows"
  status: "in_progress"
  metadata:
    agent_type: "explore"
    focus: "selectors"
步骤 2:启动 Explore Agent

使用 Task 工具并设置以下参数来启动 Agent:

Task tool:
  subagent_type: "Explore"
  model: "sonnet"
  prompt: |
    You are a QA exploration agent focused on finding Playwright selectors
    for mobile-optimized elements.

    Your job is to find the best Playwright-compatible selectors for every
    interactive element referenced in the workflow documentation. Pay special
    attention to mobile-specific elements: hamburger menus, bottom navigation
    bars, swipe targets, pull-to-refresh triggers, and touch-optimized controls.
    Use Read, Grep, and Glob to explore the codebase. Do NOT use any browser tools.

    Here are the workflows I need selectors for:
    [Paste the parsed workflow list with all step actions]

    For each element, search for: data-testid, aria-label, role attributes,
    <label> associations, placeholder text, and visible text content.

    Prefer selectors in this order (Playwright recommended):
    1. getByRole  2. getByLabel  3. getByPlaceholder
    4. getByText  5. getByTestId  6. CSS selector (last resort)

    Additionally, note any elements that appear to be touch-specific
    (hamburger icons, bottom tabs, swipeable cards, etc.) and whether
    they have appropriate ARIA attributes for accessibility.

    Return findings as:

    ## Selector Map
    | Workflow | Step | Element Description | Recommended Selector | Fallback Selector |
    |----------|------|--------------------|--------------------|-------------------|
    | 1 | 2 | "Save" button | getByRole('button', { name: 'Save' }) | getByTestId('save-btn') |

    ## Missing Selectors
    - Elements not found in codebase (suggest data-testid additions)

    ## Mobile-Specific Findings
    - Elements that change between mobile/desktop layouts
    - Hamburger vs sidebar navigation differences
    - Bottom nav vs top nav variations

    ## Selector Quality Report
    - Counts by selector type and elements not found
步骤 3:处理 Agent 结果

Explore Agent 返回后,将其 Selector Map 合并到内部工作流表示中。现在,每个步骤都有一个具体的 Playwright 选择器,可在生成代码时使用。

TaskUpdate:
  title: "Selectors: Find for all workflows"
  status: "completed"
  metadata:
    selectors_found: 87
    selectors_missing: 4
    by_role: 42
    by_label: 23
    by_testid: 15
    by_text: 7
    css_fallback: 0
    mobile_specific_elements: 12

对于 Agent 无法定位的元素,在测试代码中生成一条注释:

// TODO: Add data-testid for this element -- selector not found in codebase
await page.locator('[data-testid="unknown-element"]').click();

阶段 4:生成 Playwright 项目

这是核心生成阶段。使用解析后的工作流、发现的选择器和配置模板生成所有项目文件。

步骤 1:创建生成任务
TaskCreate:
  title: "Generate: Playwright project"
  status: "in_progress"
  metadata:
    files_to_generate: 6
步骤 2:生成 playwright.config.ts

生成 Playwright 配置文件,包含两个移动浏览器项目(模拟 iPhone 15 Pro 的 Chromium 和 WebKit)、作为依赖项的身份验证设置,以及 Vercel 部署保护绕过请求头。

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  reporter: process.env.CI ? 'html' : [['list'], ['html']],
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    extraHTTPHeaders: {
      ...(process.env.VERCEL_AUTOMATION_BYPASS_SECRET && {
        'x-vercel-protection-bypass': process.env.VERCEL_AUTOMATION_BYPASS_SECRET,
        'x-vercel-set-bypass-cookie': 'samesitenone',
      }),
    },
  },
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'mobile-chromium',
      use: {
        ...devices['iPhone 15 Pro'],
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
    {
      name: 'mobile-webkit',
      use: {
        ...devices['iPhone 15 Pro'],
        browserName: 'webkit',
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
});

关键配置决策:使用 fullyParallel 提高速度;仅在 CI 中设置 retries: 2,以缓解不稳定测试的问题;使用 trace: 'on-first-retry' 调试失败;从环境变量读取 baseURL,灵活适配本地主机和已部署 URL。只有在设置了 VERCEL_AUTOMATION_BYPASS_SECRET 时,才会有条件地应用 Vercel 绕过请求头。setup 项目会在任何依赖 storageState 的测试运行前执行 auth.setup.ts。两个移动项目分别覆盖基于 Chromium 和 WebKit 的移动浏览器,以发现特定于引擎的渲染和行为差异。devices['iPhone 15 Pro'] 预设会配置移动视口尺寸、设备缩放系数、触控支持和用户代理字符串。

步骤 3:生成 tests/auth.setup.ts

生成身份验证设置文件。即使没有工作流需要身份验证,也始终会生成此文件。未提供凭据时,文件会妥善保存一个空的存储状态,以便仍可运行不需要身份验证的测试。

import { test as setup } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
  // Check for saved profiles from /setup-profiles
  const profilesDir = path.join(process.cwd(), '.playwright', 'profiles');
  const profilesConfig = path.join(process.cwd(), '.playwright', 'profiles.json');

  if (fs.existsSync(profilesConfig)) {
    const config = JSON.parse(fs.readFileSync(profilesConfig, 'utf-8'));
    const profileName = Object.keys(config.profiles)[0];
    const profilePath = path.join(profilesDir, `${profileName}.json`);

    if (fs.existsSync(profilePath)) {
      const state = JSON.parse(fs.readFileSync(profilePath, 'utf-8'));
      fs.mkdirSync(path.dirname(authFile), { recursive: true });
      fs.writeFileSync(authFile, JSON.stringify(state));
      return;
    }
  }

  // Fall back to env-var credentials
  if (!process.env.TEST_EMAIL || !process.env.TEST_PASSWORD) {
    await page.context().storageState({ path: authFile });
    return;
  }
  await page.goto('/login');
  await page.getByLabel('Email').fill(process.env.TEST_EMAIL);
  await page.getByLabel('Password').fill(process.env.TEST_PASSWORD);
  await page.getByRole('button', { name: /sign in|log in/i }).click();
  await page.waitForURL('**/dashboard');
  await page.context().storageState({ path: authFile });
});

如果项目通过 /setup-profiles 配置了配置文件,身份验证设置会读取 profiles.json,查找首个可用配置文件,并直接复制其 storageState——本地运行测试无需凭据。在 CI 中,则改用环境变量中的凭据。

关键身份验证决策:未设置凭据时会妥善回退,保存空的身份验证状态,因此不需要身份验证的测试仍可通过。正则表达式按钮匹配器(/sign in|log in/i)可处理常见变体。针对特定应用生成时,应根据阶段 3 的选择器发现结果,调整登录路由、字段标签、按钮文本和登录后的 URL。

步骤 4:生成 package.json
{
  "name": "mobile-e2e",
  "private": true,
  "scripts": {
    "test": "playwright test",
    "test:ui": "playwright test --ui",
    "test:headed": "playwright test --headed",
    "test:chromium": "playwright test --project=mobile-chromium",
    "test:webkit": "playwright test --project=mobile-webkit"
  },
  "devDependencies": {
    "@playwright/test": "^1.50.0"
  }
}
步骤 5:生成 .github/workflows/mobile-e2e.yml

生成 GitHub Actions CI 工作流,针对 Vercel 预览部署运行移动测试。为覆盖两种引擎,会安装 Chromium 和 WebKit 浏览器。

name: Mobile E2E Tests
on: [deployment_status]
jobs:
  test:
    if: >
      github.event.deployment_status.state == 'success' &&
      contains(github.event.deployment_status.environment, 'Preview')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: cd e2e/mobile && npm ci
      - run: cd e2e/mobile && npx playwright install chromium webkit --with-deps
      - run: cd e2e/mobile && npx playwright test
        env:
          BASE_URL: ${{ github.event.deployment_status.target_url }}
          TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
          TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
          VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: mobile-playwright-report
          path: e2e/mobile/playwright-report/

关键 CI 决策:由 deployment_status 触发,以便针对实际的 Vercel 预览 URL 运行测试;仅筛选 Preview 环境;使用 target_url 作为 BASE_URL;要求设置三个 GitHub 密钥(TEST_EMAIL、TEST_PASSWORD、VERCEL_AUTOMATION_BYPASS_SECRET);每次运行都会将 Playwright HTML 报告上传为构建产物。安装 chromium 和 webkit,以全面覆盖两种引擎的移动测试。

步骤 6:生成 .gitignore
node_modules/
playwright/.auth/
playwright-report/
test-results/
步骤 7:生成 tests/workflows.spec.ts

这是最大、最重要的文件。将每个解析后的工作流映射到一个 test.describe() 代码块,并将每个工作流步骤映射到一个或多个 Playwright 操作。每个生成的测试还会通过 test.step() 代码块包含移动端 UX 反模式断言。

结构
import { test, expect } from '@playwright/test';

test.describe('Core Workflows', () => {

  test.describe('Workflow 1: Mobile User Registration', () => {
    test('completes user registration flow on mobile', async ({ page }) => {
      // Step 1: Navigate to /signup
      await page.goto('/signup');
      // Verify: Signup form is visible
      await expect(page.getByRole('heading', { name: 'Sign Up' })).toBeVisible();

      // Step 2: Tap the first name field and type "John"
      await page.getByLabel('First name').fill('John');

      // Step 3: Tap the email field and type "[email protected]"
      await page.getByLabel('Email').fill('[email protected]');

      // Step 4: Tap the "Create Account" button
      await page.getByRole('button', { name: 'Create Account' }).click();

      // Step 5: Verify success message appears
      await expect(page.getByText('Account created successfully')).toBeVisible();
    });

    test('UX: touch targets and input accessibility', async ({ page }) => {
      await page.goto('/signup');
      // Runs all four UX checks -- see "UX Anti-Pattern Assertions" below
      await test.step('touch targets are at least 44px', async () => { /* ... */ });
      await test.step('input font-size >= 16px to prevent iOS zoom', async () => { /* ... */ });
      await test.step('no hover-only interactions', async () => { /* ... */ });
      await test.step('viewport meta tag is present', async () => { /* ... */ });
    });
  });

  // ... more core workflow describe blocks

});

test.describe('Feature Workflows', () => {
  // ... feature workflow describe blocks
});

test.describe('Edge Case Workflows', () => {
  // ... edge case workflow describe blocks
});
分组规则
  1. 顶级分组——test.describe('Core Workflows', ...)、test.describe('Feature Workflows', ...) 和 test.describe('Edge Case Workflows', ...) 与工作流文件中的优先级层级对应。
  2. 工作流块——每个工作流都作为 test.describe('Workflow N: Name', ...),嵌套在相应的优先级分组中。
  3. 每个工作流包含两个测试——每个工作流会生成两个 test() 调用:
    • 功能测试:按顺序包含工作流的所有步骤(整体判定通过或失败)。
    • UX 反模式测试:检查该工作流访问页面的移动端易用性。
  4. 步骤注释——工作流中的每个步骤前都要添加一条注释(// Step N: ...),并添加对应的验证注释(// Verify: ...)。
UX 反模式断言

每个生成的工作流都会附带一个 UX 测试,用于验证该工作流访问页面是否符合移动端最佳实践。这些断言要放在 test.step() 块中,以便清晰地报告结果。对于每个工作流,确定访问过的所有不重复页面(检查导航步骤和 URL 变化),然后为该工作流生成一个 UX 测试,导航到主页面并运行全部四项检查。

四项必需的检查:

  1. 触控目标 >= 44px——遍历所有 getByRole('button') 和 getByRole('link') 元素。对每个元素调用 boundingBox(),并断言 width >= 44 且 height >= 44。这可确保符合 Apple HIG 和 WCAG 2.5.8 规定的最小点击目标尺寸。

  2. 输入框字号 >= 16px——遍历所有 input, textarea, select 元素。对每个元素,计算 parseFloat(window.getComputedStyle(el).fontSize) 并断言结果 >= 16。这可避免 iOS Safari 在聚焦时自动缩放。

  3. 不依赖仅悬停交互——检查页面样式表。对于每条包含 :hover 的 CSS 规则,查找匹配的元素,并检查它们是否也带有点击/轻触处理程序(即元素是 <button>、<a>,或具有 role="button")。收集违规项并断言列表为空。

  4. viewport meta 标签——断言 meta[name="viewport"] 存在且数量为 1,并且其 content 属性包含 width=device-width。

上述「翻译示例」部分展示了这四项检查的完整实现。

步骤翻译规则

将工作流步骤转换为 Playwright 代码时,请遵循以下规则。使用第 3 阶段的 Selector Map 为每个元素选择最合适的选择器。移动端工作流使用轻触的说法,对应 Playwright 的 .click() 方法,因为 Playwright 的移动端模拟会自动处理触摸事件。

// Navigation
await page.goto('/dashboard');

// Tap actions (mobile tap = Playwright click with touch emulation)
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('link', { name: 'Settings' }).click();
await page.getByText('Learn more').click();

// Tap hamburger menu / mobile navigation
await page.getByRole('button', { name: /menu|hamburger/i }).click();
await page.getByRole('navigation').getByRole('link', { name: 'Settings' }).click();

// Type / fill actions (tap field then type)
await page.getByLabel('Email').fill('[email protected]');
await page.getByPlaceholder('Search...').fill('search term');

// Select, checkbox, toggle
await page.getByLabel('Role').selectOption('Admin');
await page.getByLabel('Remember me').check();
await page.getByLabel('Send notifications').uncheck();

// Swipe actions (dispatch touchstart + touchend with offset clientX)
await page.locator('[data-testid="swipeable-card"]').evaluate((el) => {
  el.dispatchEvent(new TouchEvent('touchstart', { touches: [new Touch({ identifier: 0, target: el, clientX: 300, clientY: 200 })] }));
  el.dispatchEvent(new TouchEvent('touchend', { changedTouches: [new Touch({ identifier: 0, target: el, clientX: 50, clientY: 200 })] }));
});

// Pull-to-refresh (mouse drag down)
await page.mouse.move(200, 100);
await page.mouse.down();
await page.mouse.move(200, 400, { steps: 10 });
await page.mouse.up();

// Wait and visibility
await expect(page.getByText('Loading')).toBeHidden();
await page.waitForURL('**/dashboard');

// Verification assertions
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await expect(page.getByText('Changes saved')).toBeVisible();
await expect(page).toHaveURL(/.*\/settings/);
await expect(page).toHaveTitle('Settings');

// Bottom navigation tap
await page.getByRole('navigation').getByRole('link', { name: 'Home' }).click();
await page.getByRole('navigation').getByRole('link', { name: 'Profile' }).click();

// Scroll into view (for content below the fold on mobile)
await page.getByText('Comments').scrollIntoViewIfNeeded();

// File upload
await page.getByLabel('Upload').setInputFiles('photo.jpg');

// Keyboard
await page.keyboard.press('Enter');
处理 MANUAL 步骤

标记为 [MANUAL] 的工作流步骤无法自动化。请生成跳过或添加注释的测试步骤:

// Step 4: [MANUAL] Verify the push notification arrives on the device
// This step requires manual verification -- cannot be automated with Playwright.
// Consider using a push notification testing service for automation.

如果只有一个步骤需要手动执行,不要为整个工作流生成 test.skip()。请改为添加注释,并继续处理后续可自动化的步骤。

处理前置条件

如果工作流除了身份验证之外还有其他前置条件,请将其生成为 test.beforeEach 或内联设置代码:

test.describe('Workflow 5: Edit Existing Post', () => {
  test('edits an existing post on mobile', async ({ page }) => {
    // Precondition: At least one post exists
    // Navigate to the posts list to verify
    await page.goto('/posts');
    await expect(page.getByRole('article')).toHaveCount(1, { timeout: 5000 });

    // Step 1: Tap the first post title
    await page.getByRole('article').first().click();
    // ...
  });
});
第 8 步:更新生成任务
TaskUpdate:
  title: "Generate: Playwright project"
  status: "completed"
  metadata:
    files_generated: 6
    test_describes: 23
    functional_tests: 23
    ux_tests: 23
    total_steps_translated: 142
    manual_steps_skipped: 3
    selectors_from_agent: 87

操作映射参考

此表提供从移动端工作流语言到 Playwright 代码的完整映射。翻译步骤时可将其用作快速参考。

工作流语言Playwright 代码
导航至 /dashboardawait page.goto('/dashboard')
点击“Save”按钮await page.getByRole('button', { name: 'Save' }).click()
点击“Settings”链接await page.getByRole('link', { name: 'Settings' }).click()
点击汉堡菜单await page.getByRole('button', { name: /menu/i }).click()
点击底部导航中的“Home”选项卡await page.getByRole('navigation').getByRole('link', { name: 'Home' }).click()
在电子邮件字段中输入“hello”await page.getByLabel('Email').fill('hello')
在搜索框中输入“query”await page.getByPlaceholder('Search...').fill('query')
验证标题“Dashboard”可见await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
验证文本“Success”出现await expect(page.getByText('Success')).toBeVisible()
验证 URL 包含 /settingsawait expect(page).toHaveURL(/.*\/settings/)
从角色下拉菜单中选择“Admin”await page.getByLabel('Role').selectOption('Admin')
勾选“Remember me”复选框await page.getByLabel('Remember me').check()
取消勾选“Notifications”复选框await page.getByLabel('Notifications').uncheck()
等待加载提示消失await expect(page.getByText('Loading')).toBeHidden()
等待 URL 包含 /dashboardawait page.waitForURL('**/dashboard')
上传“photo.jpg”await page.getByLabel('Upload').setInputFiles('photo.jpg')
按 Enter 键提交await page.keyboard.press('Enter')
滚动至评论区await page.getByText('Comments').scrollIntoViewIfNeeded()
在卡片上向左滑动await card.evaluate((el) => { /* touch events */ })
下拉刷新await page.mouse.move(200, 100); /* drag down */
长按项目await page.getByText('Item').click({ delay: 800 })
验证元素的数量为 Nawait expect(page.getByRole('listitem')).toHaveCount(N)
验证输入框的值为“text”await expect(page.getByLabel('Name')).toHaveValue('text')
清空搜索字段await page.getByLabel('Search').clear()
收起键盘await page.keyboard.press('Escape')
点击外部区域关闭模态框await page.locator('body').click({ position: { x: 10, y: 10 } })

示例翻译

以下是一个完整示例,展示如何将 mobile-workflows.md 中的工作流转换为 Playwright 测试代码。

输入工作流
## Workflow 3: Create New Post




> Tests the complete flow of creating a new blog post from the mobile dashboard.

**Preconditions:**
- User is logged in as editor

**Steps:**

1. Navigate to /dashboard
   - Verify the dashboard heading is visible

2. Tap the "New Post" button
   - Verify the post editor page loads

3. Tap the title field and type "My First Post"
   - Verify the title field shows "My First Post"

4. Tap the content editor and type "This is the post body content."
   - Verify content appears in the editor

5. Tap the category dropdown and select "Technology"
   - Verify "Technology" is selected

6. Tap the "Publish" button
   - Verify success toast appears with message "Post published"

7. Verify the URL changes to /posts/[slug]

8. Navigate to /posts
   - Verify "My First Post" appears in the post list

**Postconditions:**
- New post exists with title "My First Post"
- Post is visible in the public post list
输出测试代码
test.describe('Workflow 3: Create New Post', () => {
  test('creates a new blog post from the mobile dashboard', async ({ page }) => {
    // Step 1: Navigate to /dashboard
    await page.goto('/dashboard');
    // Verify: The dashboard heading is visible
    await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();

    // Step 2: Tap the "New Post" button
    await page.getByRole('button', { name: 'New Post' }).click();
    // Verify: The post editor page loads
    await expect(page.getByRole('heading', { name: /editor|new post/i })).toBeVisible();

    // Step 3: Tap the title field and type "My First Post"
    await page.getByLabel('Title').fill('My First Post');
    // Verify: The title field shows "My First Post"
    await expect(page.getByLabel('Title')).toHaveValue('My First Post');

    // Step 4: Tap the content editor and type "This is the post body content."
    await page.getByLabel('Content').fill('This is the post body content.');
    // Verify: Content appears in the editor
    await expect(page.getByLabel('Content')).toHaveValue('This is the post body content.');

    // Step 5: Tap the category dropdown and select "Technology"
    await page.getByLabel('Category').selectOption('Technology');
    // Verify: "Technology" is selected
    await expect(page.getByLabel('Category')).toHaveValue('Technology');

    // Step 6: Tap the "Publish" button
    await page.getByRole('button', { name: 'Publish' }).click();
    // Verify: Success toast appears
    await expect(page.getByText('Post published')).toBeVisible();

    // Step 7: Verify the URL changes to /posts/[slug]
    await expect(page).toHaveURL(/\/posts\/.+/);

    // Step 8: Navigate to /posts
    await page.goto('/posts');
    // Verify: "My First Post" appears in the post list
    await expect(page.getByText('My First Post')).toBeVisible();
  });

  test('UX: mobile usability on post editor', async ({ page }) => {
    await page.goto('/dashboard');
    await page.getByRole('button', { name: 'New Post' }).click();

    await test.step('touch targets are at least 44px', async () => {
      const buttons = page.getByRole('button');
      const count = await buttons.count();
      for (let i = 0; i < count; i++) {
        const box = await buttons.nth(i).boundingBox();
        expect(box, `Button ${i} should have dimensions`).not.toBeNull();
        expect(box!.width, `Button ${i} width >= 44px`).toBeGreaterThanOrEqual(44);
        expect(box!.height, `Button ${i} height >= 44px`).toBeGreaterThanOrEqual(44);
      }
    });

    await test.step('input font-size >= 16px to prevent iOS zoom', async () => {
      const inputs = page.locator('input, textarea, select');
      const count = await inputs.count();
      for (let i = 0; i < count; i++) {
        const fontSize = await inputs.nth(i).evaluate(
          (el) => parseFloat(window.getComputedStyle(el).fontSize)
        );
        expect(fontSize, `Input ${i} font-size >= 16px`).toBeGreaterThanOrEqual(16);
      }
    });

    await test.step('no hover-only interactions', async () => {
      const hoverOnlyElements = await page.evaluate(() => {
        const issues: string[] = [];
        const sheets = Array.from(document.styleSheets);
        for (const sheet of sheets) {
          try {
            const rules = Array.from(sheet.cssRules || []);
            for (const rule of rules) {
              if (rule instanceof CSSStyleRule && rule.selectorText?.includes(':hover')) {
                const baseSelector = rule.selectorText.replace(/:hover/g, '');
                const elements = document.querySelectorAll(baseSelector);
                elements.forEach((el) => {
                  const style = window.getComputedStyle(el);
                  if (style.display !== 'none' && style.visibility !== 'hidden') {
                    const hasClickHandler = el.getAttribute('onclick') ||
                      el.tagName === 'BUTTON' || el.tagName === 'A' ||
                      el.getAttribute('role') === 'button';
                    if (!hasClickHandler) {
                      issues.push(`${el.tagName}.${el.className} has :hover but no tap handler`);
                    }
                  }
                });
              }
            }
          } catch {
            // Cross-origin stylesheets cannot be inspected
          }
        }
        return issues;
      });
      expect(hoverOnlyElements, 'Elements with hover-only interactions').toHaveLength(0);
    });

    await test.step('viewport meta tag is present', async () => {
      const viewport = page.locator('meta[name="viewport"]');
      await expect(viewport).toHaveCount(1);
      const content = await viewport.getAttribute('content');
      expect(content).toContain('width=device-width');
    });
  });
});
翻译说明

关键模式:步骤注释与工作流文本保持一致,便于交叉参考;每个“Verify”子步骤都会转换为一个 expect() 断言;选择器遵循优先级层级(role > label > text > testid),并应用第 3 阶段的覆盖规则;测试名称根据工作流描述生成;动态 URL 片段使用正则表达式模式;每个工作流都会配套一个 UX 测试,用于验证其访问页面在移动端的特定可用性。UX 测试会导航至工作流的主要页面,并检查触控目标、输入框字体大小、仅悬停时触发的交互以及 viewport meta 配置。


第 5 阶段:与用户审核(必需)

此阶段为必需步骤。未经用户批准,绝不可写入文件。

提交生成的测试以供审核

使用 AskUserQuestion 提交生成的项目。包括项目结构、测试摘要(按优先级层级列出功能测试和 UX 测试数量)、浏览器覆盖范围(mobile-chromium、mobile-webkit),以及完整生成的 tests/workflows.spec.ts 和 auth.setup.ts(如果经过自定义)。请用户审核测试翻译、选择器、UX 阈值和身份验证流程。等待用户回复“approved”或提供修改意见。

创建审批任务
TaskCreate:
  title: "Approval: Review generated tests"
  status: "in_progress"
  metadata:
    iteration: 1
    test_blocks_presented: 23
    functional_tests: 23
    ux_tests: 23
    total_steps: 142
处理反馈

如果用户提供反馈而非批准:(1)应用更改;(2)将当前审批任务标记为已完成,并设置 result: "changes_requested" 和 feedback_summary;(3)为下一轮创建新的审批任务;(4)重新展示修订后的测试。重复此流程,直到用户批准。

获得批准后,将审批任务标记为已完成,并设置 result: "approved" 和最终测试计数。


第 6 阶段:写入文件

将所有生成的文件写入 e2e/mobile/。

第 1 步:创建目录结构
1. Ensure e2e/mobile/ exists (create if not).
2. Ensure e2e/mobile/tests/ exists (create if not).
3. Ensure .github/workflows/ exists (create if not).
第 2 步:写入所有文件

按顺序写入每个文件:

  1. e2e/mobile/playwright.config.ts
  2. e2e/mobile/package.json
  3. e2e/mobile/tsconfig.json
  4. e2e/mobile/tests/auth.setup.ts
  5. e2e/mobile/tests/workflows.spec.ts
  6. e2e/mobile/.gitignore
  7. .github/workflows/mobile-e2e.yml(注意:此文件位于仓库根目录,而非 e2e/mobile/ 内)

tsconfig.json 内容:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["tests/**/*.ts"]
}
第 3 步:验证文件

写入后,逐个读回文件,确认写入无误。

第 4 步:对生成的代码进行类型检查

对生成的项目运行 TypeScript 类型检查,在将项目标记为完成前发现编译错误。

**前提条件:**必须能使用 tsc。如果 npx tsc --version 失败,请尝试在 e2e/mobile 目录中运行 npm install typescript --save-dev。如果仍然无法找到 TypeScript,请停止并告知用户:“类型检查需要 TypeScript 编译器(tsc),但未找到。请使用 npm install -D typescript 安装,或确保全局可用。”类型检查不是可选步骤,不得跳过。

流程:

1. Install dependencies (capture errors for diagnosis):
   cd e2e/mobile && npm install --ignore-scripts 2>&1 | tee /tmp/npm-install.log

2. Run type-check:
   cd e2e/mobile && npx tsc --noEmit

3. If type errors are found:
   a. Read the tsc error output to identify the failing file and line.
   b. Fix the type error in the generated code.
   c. IMPORTANT: Write modified files back to disk BEFORE the next tsc run.
      Do not batch fixes — write each fix immediately so the next tsc run
      sees the corrected code on disk.
   d. Re-run: cd e2e/mobile && npx tsc --noEmit
   e. Repeat up to a 3-attempt cap (counted per full tsc run, not per error).
      After 3 full tsc runs with errors, STOP and use AskUserQuestion to
      present the remaining errors to the user and ask for guidance.

**语义保护:**修复循环不得修改断言、选择器或测试意图。修复仅限于:

  • 类型注解(添加显式类型、修正类型不匹配)
  • 导入语句(缺少导入、导入路径错误)
  • API 用法修正(错误的 Playwright API 方法签名)

如果修复会改变测试检查的内容(修改断言、更改选择器、改变测试逻辑、删除测试步骤),请勿应用修复。改为通过 AskUserQuestion 向用户升级处理:

Type error in [file]:[line] requires changing test intent to fix:

  Error: [tsc error message]
  Current code: [the line with the error]

  Fixing this would require changing [what would change — e.g., the assertion,
  the selector, the test logic]. This is beyond the scope of type-error fixes.

  How would you like to proceed?
  1. Fix it manually
  2. Suppress with // @ts-expect-error and move on
  3. Remove the affected test block
第 5 步:更新任务

将 "Write: e2e/mobile/" 标记为已完成,并将主任务 "Convert: Mobile Workflows to Playwright" 更新为已完成,同时填写最终元数据(写入的文件、工作流数量、浏览器项目、审核轮次)。

最终摘要

提供完成摘要,列出:输出目录(e2e/mobile/)、CI 工作流路径、所有已写入的文件、转换统计(工作流、已弃用项、手动步骤、选择器、审核轮次、每个工作流的 UX 检查项)、浏览器项目(mobile-chromium、mobile-webkit)以及后续步骤:

  1. cd e2e/mobile && npm install && npx playwright install chromium webkit
  2. npx playwright test --headed(本地有头模式)
  3. npx playwright test --project=mobile-chromium 或 --project=mobile-webkit(单个引擎)
  4. 配置 GitHub 密钥:TEST_EMAIL、TEST_PASSWORD、VERCEL_AUTOMATION_BYPASS_SECRET
  5. 推送代码,以便在下次 Vercel 预览部署时触发 CI

会话恢复

如果调用此技能时发现已有任务列表,请检查是否存在任务 "Convert: Mobile Workflows to Playwright",并查找最后一个已完成的子任务。从下一阶段继续:

最后完成的任务恢复阶段
无第 1 阶段(解析)
Parse第 2 阶段(检查现有内容)
Check第 3 阶段(发现选择器)
Selectors (in_progress)第 3 阶段(重新启动 Agent)
Selectors (completed)第 4 阶段(生成)
Generate第 5 阶段(审核)
Approval (changes_requested)第 5 阶段(应用反馈并重新展示)
Approval (approved)第 6 阶段(写入文件)
Write完成(显示摘要)

恢复任务时,务必告知用户:说明已完成的内容、接下来要进行的阶段,并提供从头开始的选项。


处理更新

当用户选择“更新”模式(修改现有测试以匹配变更后的工作流)时,请遵循以下规则。

更新现有测试的规则
  1. 保留自定义修改 —— 如果用户手动编辑了生成的测试(添加了自定义辅助函数、更改了选择器、添加了额外断言),请保留这些修改。留意 // CUSTOM: 之类的注释,或任何与生成模式不符的代码。

  2. 将工作流与测试块匹配 —— 使用 test.describe('Workflow N: ...') 命名约定,将现有测试块与其来源工作流匹配。这也是保持命名一致至关重要的原因。

  3. 更新已变更的工作流 —— 如果某个工作流的步骤自上次生成后发生变化,则仅重新生成该工作流的 test.describe 块(包括功能测试和 UX 测试)。保留该块在文件中的位置。

  4. 添加新工作流 —— 将新工作流添加到相应的优先级组(Core Workflows、Feature Workflows、Edge Case Workflows),并追加到其所属组的末尾。

  5. 标记已移除的工作流 —— 如果某个工作流自上次生成后已弃用,请注释掉其测试块,而不要删除:

// DEPRECATED: Workflow 7 -- Legacy Mobile Export Feature
// Reason: Export feature removed in v2.3
// Date: 2025-01-15
// test.describe('Workflow 7: Legacy Mobile Export Feature', () => { ... });
  1. 重新生成配置文件 —— 始终根据模板重新生成 playwright.config.ts、package.json、.gitignore 和 CI 工作流(这些文件不应包含自定义修改)。

  2. 保留 auth.setup.ts 自定义内容 —— 如果用户自定义了身份验证设置(不同的登录流程、多步骤身份验证),请保留其版本。只有在用户明确要求时才重新生成。

更新摘要

更新操作完成后,请提供变更摘要,列出保留、更新、弃用和新增的测试块;指出具体哪些工作流发生了变化及其原因;并说明哪些文件已重新生成、哪些文件已保留。


选择器策略参考

优先按以下顺序使用选择器:(1) getByRole (2) getByLabel (3) getByPlaceholder (4) getByText (5) getByTestId (6) CSS 选择器(最后手段)。避免使用 ID 选择器、类选择器、结构选择器和属性选择器——这些选择器都很脆弱,重构时容易失效。


约束

  • 允许使用的工具 —— 此技能仅使用 Read、Write、Glob、Grep 和 Task/Explore 工具。不要使用 Chrome MCP、iOS Simulator MCP、Playwright CLI 或任何其他浏览器自动化工具。所有浏览器交互都只生成代码,转换期间绝不执行。
  • 输出位置 —— 所有测试文件都放在 e2e/mobile/。CI 工作流放在仓库根目录下的 .github/workflows/mobile-e2e.yml。
  • 始终生成身份验证 —— 始终包含 auth.setup.ts,即使没有任何工作流需要身份验证也不例外。缺少凭据时,它会妥善处理。
  • 始终包含 Vercel 标头 —— 当设置了环境变量时,会在 playwright.config.ts 中有条件地应用 x-vercel-protection-bypass 和 x-vercel-set-bypass-cookie 标头。
  • 工作流不依赖运行时 —— 生成的测试项目完全自包含。它不会在运行时读取或导入工作流 Markdown 文件。
  • Playwright 最佳实践 —— 按顺序使用 getByRole、getByLabel、getByText、getByPlaceholder 和 getByTestId。除非绝对必要,否则避免使用 CSS 和 XPath 选择器。
  • 双浏览器覆盖 —— 每项测试都会针对 mobile-chromium 和 mobile-webkit 项目运行,以捕获特定引擎的渲染和行为差异。
  • UX 反模式断言 —— 每个工作流都会生成配套的 UX 测试,用于检查移动端可用性(触控目标、输入框缩放防护、仅悬停检测、viewport meta)。这些测试不可省略,始终都会生成。
  • 移动端视口 —— 所有测试都在 iPhone 15 Pro 设备模拟配置中运行,该配置会设置正确的视口尺寸、设备缩放系数、触控支持和 user agent 字符串。

评论

支持 Markdown