关于 mobile-workflow-to-playwright
mobile-workflow-to-playwright 是一项将移动端工作流文档转换为 Playwright 端到端测试项目的技能,面向 QA 自动化。它还生成移动 UX 检查,并配置双浏览器测试、认证脚手架和 CI。
主要功能
- 01解析移动端工作流文档
- 02生成 Playwright 测试项目
- 03添加移动 UX 断言
- 04配置 Chromium 和 WebKit
- 05提供认证与 CI 脚手架
适用场景
- 01将工作流文档转为可运行测试
- 02跨浏览器引擎检查移动流程
- 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:读取并解析
读取整个工作流文件。对于每个工作流,提取:
- 工作流编号——来自
## Workflow [N]:标题 - 工作流名称——编号后面的描述性名称
- 身份验证要求——来自
或 - 优先级——来自
,, 或 `` - 预计步骤数——来自 ``
- 已弃用标记——来自 ``(跳过已弃用的工作流)
- 前置条件——
**Preconditions:**下的项目符号列表 - 步骤——每个编号步骤及其验证子步骤
- 后置条件——
**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
});
分组规则
- 顶级分组——
test.describe('Core Workflows', ...)、test.describe('Feature Workflows', ...)和test.describe('Edge Case Workflows', ...)与工作流文件中的优先级层级对应。 - 工作流块——每个工作流都作为
test.describe('Workflow N: Name', ...),嵌套在相应的优先级分组中。 - 每个工作流包含两个测试——每个工作流会生成两个
test()调用:- 功能测试:按顺序包含工作流的所有步骤(整体判定通过或失败)。
- UX 反模式测试:检查该工作流访问页面的移动端易用性。
- 步骤注释——工作流中的每个步骤前都要添加一条注释(
// Step N: ...),并添加对应的验证注释(// Verify: ...)。
UX 反模式断言
每个生成的工作流都会附带一个 UX 测试,用于验证该工作流访问页面是否符合移动端最佳实践。这些断言要放在 test.step() 块中,以便清晰地报告结果。对于每个工作流,确定访问过的所有不重复页面(检查导航步骤和 URL 变化),然后为该工作流生成一个 UX 测试,导航到主页面并运行全部四项检查。
四项必需的检查:
-
触控目标 >= 44px——遍历所有
getByRole('button')和getByRole('link')元素。对每个元素调用boundingBox(),并断言width >= 44且height >= 44。这可确保符合 Apple HIG 和 WCAG 2.5.8 规定的最小点击目标尺寸。 -
输入框字号 >= 16px——遍历所有
input, textarea, select元素。对每个元素,计算parseFloat(window.getComputedStyle(el).fontSize)并断言结果>= 16。这可避免 iOS Safari 在聚焦时自动缩放。 -
不依赖仅悬停交互——检查页面样式表。对于每条包含
:hover的 CSS 规则,查找匹配的元素,并检查它们是否也带有点击/轻触处理程序(即元素是<button>、<a>,或具有role="button")。收集违规项并断言列表为空。 -
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 代码 |
|---|---|
| 导航至 /dashboard | await 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 包含 /settings | await 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 包含 /dashboard | await 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 }) |
| 验证元素的数量为 N | await 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 步:写入所有文件
按顺序写入每个文件:
e2e/mobile/playwright.config.tse2e/mobile/package.jsone2e/mobile/tsconfig.jsone2e/mobile/tests/auth.setup.tse2e/mobile/tests/workflows.spec.tse2e/mobile/.gitignore.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)以及后续步骤:
cd e2e/mobile && npm install && npx playwright install chromium webkitnpx playwright test --headed(本地有头模式)npx playwright test --project=mobile-chromium或--project=mobile-webkit(单个引擎)- 配置 GitHub 密钥:
TEST_EMAIL、TEST_PASSWORD、VERCEL_AUTOMATION_BYPASS_SECRET - 推送代码,以便在下次 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 | 完成(显示摘要) |
恢复任务时,务必告知用户:说明已完成的内容、接下来要进行的阶段,并提供从头开始的选项。
处理更新
当用户选择“更新”模式(修改现有测试以匹配变更后的工作流)时,请遵循以下规则。
更新现有测试的规则
-
保留自定义修改 —— 如果用户手动编辑了生成的测试(添加了自定义辅助函数、更改了选择器、添加了额外断言),请保留这些修改。留意
// CUSTOM:之类的注释,或任何与生成模式不符的代码。 -
将工作流与测试块匹配 —— 使用
test.describe('Workflow N: ...')命名约定,将现有测试块与其来源工作流匹配。这也是保持命名一致至关重要的原因。 -
更新已变更的工作流 —— 如果某个工作流的步骤自上次生成后发生变化,则仅重新生成该工作流的
test.describe块(包括功能测试和 UX 测试)。保留该块在文件中的位置。 -
添加新工作流 —— 将新工作流添加到相应的优先级组(
Core Workflows、Feature Workflows、Edge Case Workflows),并追加到其所属组的末尾。 -
标记已移除的工作流 —— 如果某个工作流自上次生成后已弃用,请注释掉其测试块,而不要删除:
// 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', () => { ... });
-
重新生成配置文件 —— 始终根据模板重新生成
playwright.config.ts、package.json、.gitignore和 CI 工作流(这些文件不应包含自定义修改)。 -
保留
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 字符串。