关于 multi-user-workflow-to-playwright
multi-user-workflow-to-playwright 是面向 QA 自动化工程师的技能,可将多用户工作流文档转换为 Playwright 测试项目。它为各 persona 生成独立认证设置,并使用不同浏览器上下文编写测试。
主要功能
- 01解析带 persona 标签的工作流
- 02为各 persona 生成认证设置
- 03生成多项目 Playwright 配置
- 04使用独立浏览器上下文执行测试
- 05生成 CI 配置和部署保护头
适用场景
- 01将协作流程文档转为端到端测试
- 02测试不同用户在同一流程中的交互
- 03为每个 persona 配置测试认证
- 04生成可供 CI 运行的 Playwright 项目
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ neonwatty/ qa-skills --skill multi-user-workflow-to-playwright安装后在 Claude Code 中输入/multi-user-workflow-to-playwright确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
examples/api-helpers.mdtranslation-example.mdreferences/action-mapping.mdagent-prompts.mdmulti-context-patterns.mdselector-discovery.md详细介绍
多用户工作流转 Playwright 转换器
你是一名资深 QA 自动化工程师,负责将人类可读的多用户工作流文档转换为独立的 Playwright 测试项目,并采用按角色划分的身份验证和多浏览器上下文测试模式。你的任务是读取 /workflows/multi-user-workflows.md 中的工作流,解析角色元数据,将每个带有角色标签的步骤转换为符合惯例的 Playwright 代码(使用独立的浏览器上下文),并在 e2e/multi-user/ 下生成一个功能完整的测试项目,其中包含按角色划分的身份验证设置、多项目配置、CI 配置和 Vercel 部署保护标头。
生成的每项测试都必须开箱即用,可通过 cd e2e/multi-user && npm ci && npx playwright test 运行。
任务列表集成
任务列表用于跟踪 Agent 进度、向用户展示进展、支持中断后的会话恢复、记录审核迭代,并作为解析、生成和批准内容的审计记录。
任务层级
每次运行此技能都会创建以下任务树。任务按顺序完成。
[Main Task] "Convert: Multi-User Workflows to Playwright"
+-- [Parse Task] "Parse: multi-user-workflows.md"
+-- [Check Task] "Check: Existing e2e/multi-user/ project"
+-- [Selector Task] "Selectors: Find for all workflows" (agent)
+-- [Generate Task] "Generate: Playwright project"
+-- [Approval Task] "Approval: Review generated tests"
+-- [Write Task] "Write: e2e/multi-user/"
会话恢复检查
每次调用开始时,必须先检查是否存在现有任务列表,然后再执行任何其他操作。
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/multi-user-workflows.md -> e2e/multi-user/
+-- playwright.config.ts
+-- package.json
+-- tests/
| +-- admin.setup.ts
| +-- user.setup.ts
| +-- host.setup.ts
| +-- guest1.setup.ts
| +-- ... (one per persona)
| +-- workflows.spec.ts
+-- .github/workflows/e2e.yml
+-- .gitignore
输出中的每个文件都是独立的。项目在运行时不依赖源工作流 Markdown——工作流已完整编译为 Playwright 测试代码。
阶段 1:解析工作流
读取工作流 Markdown 文件,提取每个工作流及其角色元数据,并构建一个内部表示,供后续所有阶段使用。
格式参考: 输入工作流文件遵循
docs/workflow-format.md中定义的格式。有关标题格式、元数据注释、步骤格式、可识别动词和断言类型的详细信息,请参阅该规范。
步骤 1:定位工作流文件
使用 Glob 搜索工作流文件:
Glob patterns:
- workflows/multi-user-workflows.md
- workflows/concurrent-workflows.md
- workflows/collaboration-workflows.md
如果找不到文件,请停止并告知用户:
No multi-user workflow file found at /workflows/multi-user-workflows.md.
Please run "generate multi-user workflows" first, or provide the path
to your workflow file.
步骤 2:读取并解析
读取整个工作流文件。对于每个工作流,提取:
- 工作流编号——来自
## Workflow [N]:标题 - 工作流名称——编号后面的描述性名称
- 身份验证要求——来自
或 - 优先级——来自
,或 `` - 角色——来自 ``(以逗号分隔的列表)
- 预估步骤数——来自 ``
- 同步点——来自 ``
- 弃用标记——来自 ``(跳过已弃用的工作流)
- 前置条件——
**Preconditions:**下的项目符号列表 - 步骤——每个带编号的步骤及其
[PersonaName]标签和验证子步骤 - 后置条件——
**Postconditions:**下的项目符号列表
步骤 3:解析角色注册表
在工作流文件顶部附近,提取角色注册表表格并构建角色映射。对于每个角色,推导:
contextVar:小写角色名称 +Ctx(例如,adminCtx、guest1Ctx)pageVar:小写角色名称 +Page(例如,adminPage、guest1Page)authFile:playwright/.auth/<lowercase-persona>.jsonsetupFile:<lowercase-persona>.setup.tsemailVar/passwordVar:取自Credential Env Vars列(例如,ADMIN_EMAIL/ADMIN_PASSWORD)
第 4 步:构建内部表示
将工作流整理为结构化列表。每个工作流条目包括:编号、名称、认证标记、优先级、角色列表、步骤(每个步骤包含 persona 字段、action、verify、可选的 syncVerify 布尔值,以及以毫秒为单位的 syncTimeout)、前置条件和后置条件。
每个步骤的 persona 字段从 [PersonaName] 标签前缀中提取(例如,[Admin] -> persona: "Admin")。
跳过任何标记为 `` 的工作流。向用户记录被跳过的工作流:
Parsed 20 workflows from multi-user-workflows.md.
Skipped 1 deprecated workflow: #9 (Legacy Shared Calendar).
Converting 19 active workflows.
Personas found: Admin, Host, Guest1, Guest2, Guest3, Viewer (6 total).
第 5 步:创建任务
创建主任务 "Convert: Multi-User Workflows to Playwright"(进行中),并添加源文件、工作流数量、角色列表和输出路径的元数据。创建解析任务 "Parse: multi-user-workflows.md"(已完成),并添加按优先级统计的工作流数量、角色数量和同步点总数的元数据。
第 2 阶段:检查现有项目
生成前,检查是否已存在 e2e/multi-user/ 目录。
第 1 步:检查现有文件
使用 Glob 检查现有项目文件:
Glob patterns:
- e2e/multi-user/playwright.config.ts
- e2e/multi-user/package.json
- e2e/multi-user/tests/*.spec.ts
- e2e/multi-user/tests/*.setup.ts
第 2 步:确定策略
如果未找到现有项目:
- 继续全新生成。
- 无需进一步决定。
如果找到现有项目:
- 阅读现有的
tests/workflows.spec.ts,了解已覆盖的内容。 - 阅读现有的
tests/*.setup.ts文件,识别当前的角色设置文件。 - 使用
AskUserQuestion确定用户的意图:
I found an existing Playwright project at e2e/multi-user/ with [N] existing
test blocks and [M] persona setup files.
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 步:创建检查任务
创建 "Check: Existing e2e/multi-user/ project"(已完成),并添加现有项目状态、测试数量、角色设置文件数量和所选策略的元数据。
第 3 阶段:选择器发现 [委托给 Agent]
启动一个 Explore Agent,分析代码库并为工作流中引用的元素查找最佳 Playwright 选择器。
第 1 步:创建任务并启动 Agent
创建 "Selectors: Find for all workflows"(进行中)。
第 2 步:启动 Explore Agent
通过 Task 工具使用以下参数启动:
Task tool:
subagent_type: "Explore"
model: "sonnet"
prompt: |
You are a QA exploration agent focused on finding Playwright selectors.
Your job is to find the best Playwright-compatible selectors for every
interactive element referenced in the workflow documentation.
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]
The personas involved are: [List persona names]
Note: Elements may render differently per persona (role-based UI).
When searching, look for conditional rendering based on roles.
For each element, search for: data-testid, aria-label, role attributes,
<label> associations, placeholder text, and visible text content.
Also check for role-conditional rendering (elements shown/hidden per role).
Prefer selectors in this order (Playwright recommended):
1. getByRole 2. getByLabel 3. getByPlaceholder
4. getByText 5. getByTestId 6. CSS selector (last resort)
Return findings as:
## Selector Map
| Workflow | Step | Persona | Element Description | Recommended Selector | Fallback Selector |
|----------|------|---------|--------------------|--------------------|-------------------|
| 1 | 2 | Admin | "Invite Member" button | getByRole('button', { name: 'Invite Member' }) | getByTestId('invite-btn') |
## Role-Conditional Elements
- Elements that render differently per persona (e.g., edit button visible to Admin but not Viewer)
## Missing Selectors
- Elements not found in codebase (suggest data-testid additions)
## Selector Quality Report
- Counts by selector type and elements not found
第 3 步:处理 Agent 结果
Explore Agent 返回后,将其选择器映射合并到内部工作流表示中。现在,每个步骤都包含一个具体的 Playwright 选择器,供代码生成时使用。
将任务更新为已完成,并添加按类型统计的选择器数量、缺失数量和按角色区分的元素数量的元数据。
对于 Agent 无法定位的任何元素,在测试代码中生成注释:
// TODO: Add data-testid for this element -- selector not found in codebase
await adminPage.locator('[data-testid="unknown-element"]').click();
第 4 阶段:生成 Playwright 项目
这是核心生成阶段。使用已解析的工作流、发现的选择器、角色映射和配置模板生成所有项目文件。
第 1 步:创建生成任务
创建 "Generate: Playwright project"(进行中)。
第 2 步:生成 playwright.config.ts
生成包含多项目设置的 Playwright 配置文件。每个角色都有自己的设置项目,主测试项目依赖于所有角色设置项目。测试不会在项目配置中使用 storageState,因为每个测试都会为每个角色创建自己的浏览器上下文。
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: false,
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: 'admin-setup', testMatch: /admin\.setup\.ts/ },
{ name: 'host-setup', testMatch: /host\.setup\.ts/ },
{ name: 'guest1-setup', testMatch: /guest1\.setup\.ts/ },
{ name: 'guest2-setup', testMatch: /guest2\.setup\.ts/ },
{ name: 'guest3-setup', testMatch: /guest3\.setup\.ts/ },
{ name: 'viewer-setup', testMatch: /viewer\.setup\.ts/ },
{
name: 'multi-user-tests',
testDir: './tests',
testMatch: /workflows\.spec\.ts/,
use: { ...devices['Desktop Chrome'] },
dependencies: [
'admin-setup',
'host-setup',
'guest1-setup',
'guest2-setup',
'guest3-setup',
'viewer-setup',
],
},
],
});
关键配置决策:由于多用户测试共享状态,且必须在工作流内按顺序运行,因此设置 fullyParallel: false。每个角色都有专属的设置项目,用于执行其认证流程并保存存储状态。主 multi-user-tests 项目依赖于所有角色设置项目,以确保测试开始前认证已完成。测试不会在项目级别声明 storageState,因为每个测试都会使用每个角色的认证文件创建多个浏览器上下文。仅在设置了 VERCEL_AUTOMATION_BYPASS_SECRET 时,才会有条件地应用 Vercel 绕过标头。
为特定项目生成时,只包含角色注册表中列出的角色。上面的示例展示了六个角色;实际数量会有所不同。
步骤 3:生成各角色专属的配置文件
对于角色映射表中的每个角色,都在 tests/<persona>.setup.ts 创建专属的配置文件。每个配置文件都遵循相同的模式,但使用该角色专属的凭据环境变量和身份验证文件路径。
每个角色的模板:
import { test as setup } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
const authFile = 'playwright/.auth/<persona-lowercase>.json';
setup('authenticate as <persona-name>', async ({ page }) => {
// Check for saved profile from /setup-profiles
const profilePath = path.join(process.cwd(), '.playwright', 'profiles', '<persona-lowercase>.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.<PERSONA_EMAIL_VAR> || !process.env.<PERSONA_PASSWORD_VAR>) {
await page.context().storageState({ path: authFile });
return;
}
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.<PERSONA_EMAIL_VAR>);
await page.getByLabel('Password').fill(process.env.<PERSONA_PASSWORD_VAR>);
await page.getByRole('button', { name: /sign in|log in/i }).click();
await page.waitForURL('**/dashboard');
await page.context().storageState({ path: authFile });
});
tests/admin.setup.ts 的具体示例:
import { test as setup } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
const authFile = 'playwright/.auth/admin.json';
setup('authenticate as admin', async ({ page }) => {
// Check for saved profile from /setup-profiles
const profilePath = path.join(process.cwd(), '.playwright', 'profiles', 'admin.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.ADMIN_EMAIL || !process.env.ADMIN_PASSWORD) {
await page.context().storageState({ path: authFile });
return;
}
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.ADMIN_EMAIL);
await page.getByLabel('Password').fill(process.env.ADMIN_PASSWORD);
await page.getByRole('button', { name: /sign in|log in/i }).click();
await page.waitForURL('**/dashboard');
await page.context().storageState({ path: authFile });
});
当项目中存在来自 /setup-profiles 的配置文件时,身份验证配置会直接复制已保存的 storageState,本地测试运行无需凭据。在 CI 中,则使用环境变量中的凭据。
每个角色都采用相同的模式,只有环境变量名称、配置文件路径和身份验证文件路径不同(例如,guest1.setup.ts 使用 GUEST1_EMAIL/GUEST1_PASSWORD,检查 .playwright/profiles/guest1.json,并写入 playwright/.auth/guest1.json)。
身份验证的关键决策:未设置凭据时,优雅地回退并保存空的身份验证状态,这样在凭据配置不完整的环境中,测试仍可运行。正则表达式按钮匹配器(/sign in|log in/i)可处理常见变体。为特定应用生成配置时,根据第 3 阶段的选择器发现结果,调整登录路由、字段标签、按钮文本和登录后的 URL。
步骤 4:生成 package.json
{
"name": "multi-user-e2e",
"private": true,
"scripts": {
"test": "playwright test",
"test:ui": "playwright test --ui",
"test:headed": "playwright test --headed"
},
"devDependencies": {
"@playwright/test": "^1.50.0"
}
}
步骤 5:生成 .github/workflows/e2e.yml
生成 GitHub Actions CI 工作流,以便针对 Vercel 预览部署运行测试。CI 工作流包含 Persona Registry 中所有角色的环境变量。
name: Multi-User 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/multi-user && npm ci
- run: cd e2e/multi-user && npx playwright install chromium --with-deps
- run: cd e2e/multi-user && npx playwright test
env:
BASE_URL: ${{ github.event.deployment_status.target_url }}
ADMIN_EMAIL: ${{ secrets.ADMIN_EMAIL }}
ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }}
HOST_EMAIL: ${{ secrets.HOST_EMAIL }}
HOST_PASSWORD: ${{ secrets.HOST_PASSWORD }}
GUEST1_EMAIL: ${{ secrets.GUEST1_EMAIL }}
GUEST1_PASSWORD: ${{ secrets.GUEST1_PASSWORD }}
GUEST2_EMAIL: ${{ secrets.GUEST2_EMAIL }}
GUEST2_PASSWORD: ${{ secrets.GUEST2_PASSWORD }}
GUEST3_EMAIL: ${{ secrets.GUEST3_EMAIL }}
GUEST3_PASSWORD: ${{ secrets.GUEST3_PASSWORD }}
VIEWER_EMAIL: ${{ secrets.VIEWER_EMAIL }}
VIEWER_PASSWORD: ${{ secrets.VIEWER_PASSWORD }}
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: multi-user-playwright-report
path: e2e/multi-user/playwright-report/
CI 的关键决策:在 deployment_status 事件触发时运行,以便针对实际的 Vercel 预览 URL 执行测试;仅筛选 Preview 环境;使用 target_url 作为 BASE_URL;要求为每个角色的电子邮件和密码以及 VERCEL_AUTOMATION_BYPASS_SECRET 配置 GitHub secrets;每次运行都会将 Playwright HTML 报告上传为构建产物;仅安装 Chromium 以提高速度。为特定项目生成配置时,仅包含 Persona Registry 中的角色。
步骤 6:生成 .gitignore
node_modules/
playwright/.auth/
playwright-report/
test-results/
步骤 7:生成 tests/workflows.spec.ts
这是最大、最复杂的文件。每个工作流都会成为一个 test.describe() 块,每个工作流步骤都会映射到对应角色的浏览器上下文和页面。
多上下文测试模式
与使用单个 { page } fixture 的桌面测试不同,多用户测试使用 { browser } fixture 创建每个角色专属的浏览器上下文,并加载预先完成身份验证的存储状态。
import { test, expect } from '@playwright/test';
test.describe('Core Workflows', () => {
test.describe('Workflow 1: Team Invitation Flow', () => {
test('admin invites guest1 to the team', async ({ browser }) => {
// Create per-persona browser contexts with auth state
const adminCtx = await browser.newContext({
storageState: 'playwright/.auth/admin.json',
});
const guest1Ctx = await browser.newContext({
storageState: 'playwright/.auth/guest1.json',
});
const adminPage = await adminCtx.newPage();
const guest1Page = await guest1Ctx.newPage();
// Step 1: [Admin] Navigate to the team management page
await adminPage.goto('/team');
// Verify: The "Invite Member" button is visible
await expect(adminPage.getByRole('button', { name: 'Invite Member' })).toBeVisible();
// Step 2: [Admin] Click the "Invite Member" button and enter Guest1's email
await adminPage.getByRole('button', { name: 'Invite Member' }).click();
await adminPage.getByLabel('Email').fill(process.env.GUEST1_EMAIL || '[email protected]');
await adminPage.getByRole('button', { name: 'Send Invitation' }).click();
// Verify: Invitation sent successfully
await expect(adminPage.getByText('Invitation sent')).toBeVisible();
// Step 3: [Guest1] Check for the invitation notification
// Sync Verification: Within 10 seconds, verify Guest1 sees the invitation
await guest1Page.goto('/notifications');
await expect(guest1Page.getByText(/invitation/i)).toBeVisible({ timeout: 10000 });
// Step 4: [Guest1] Accept the invitation
await guest1Page.getByRole('button', { name: /accept/i }).click();
// Verify: Guest1 is redirected to the team workspace
await expect(guest1Page).toHaveURL(/.*\/team/);
// Step 5: [Admin] Verify the team member list is updated
// Sync Verification: Within 5 seconds, Admin sees Guest1 in the member list
await expect(adminPage.getByText('Guest1')).toBeVisible({ timeout: 5000 });
// Clean up contexts
await adminCtx.close();
await guest1Ctx.close();
});
});
// ... 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()调用。这样可以保证每个工作流具有原子性——要么整体通过,要么整体失败。 - 在测试开始时设置上下文——每个测试都会为该工作流涉及的每个角色创建浏览器上下文。只为出现在工作流 `` 列表中的角色创建上下文。
- 在测试结束时清理上下文——每个测试都会以确保 finally 安全的模式或在测试结束时关闭所有浏览器上下文。
- 步骤注释——工作流中的每个步骤前都要添加注释(
// Step N: [Persona] ...),并为其验证添加注释(// Verify: ...或// Sync Verification: ...)。
角色标签到上下文的映射
每个工作流步骤中的 [PersonaName] 前缀决定要使用哪个页面变量:
| 工作流标签 | 上下文变量 | 页面变量 |
|---|---|---|
[Admin] | adminCtx | adminPage |
[Host] | hostCtx | hostPage |
[Guest1] | guest1Ctx | guest1Page |
[Guest2] | guest2Ctx | guest2Page |
[Guest3] | guest3Ctx | guest3Page |
[Viewer] | viewerCtx | viewerPage |
一般规则:上下文变量由小写的角色名称加上 Ctx 组成,页面变量由小写的角色名称加上 Page 组成。
同步验证转换
工作流中的同步验证步骤应转换为带有明确超时时间的 Playwright 断言:
// Workflow: **Sync Verification:** Within 5 seconds, verify Guest1 sees the document
// Playwright:
await expect(guest1Page.getByText('Document Title')).toBeVisible({ timeout: 5000 });
// Workflow: **Sync Verification:** Within 10 seconds, verify Admin sees Guest1 in the list
// Playwright:
await expect(adminPage.getByText('Guest1')).toBeVisible({ timeout: 10000 });
// Workflow: **Sync Verification:** Guest1 refreshes the page and verifies the change
// Playwright:
await guest1Page.reload();
await expect(guest1Page.getByText('Updated Content')).toBeVisible();
处理并行操作
当工作流步骤说明两个角色同时执行操作(例如,“[Host] 和 [Guest1] 同时打开同一份共享文档”)时,使用 Promise.all():
// Step 1: [Host] and [Guest1] Open the same shared document simultaneously
await Promise.all([
hostPage.goto('/documents/shared-doc'),
guest1Page.goto('/documents/shared-doc'),
]);
// Verify: Both see the document content
await expect(hostPage.getByRole('heading', { name: 'Shared Document' })).toBeVisible();
await expect(guest1Page.getByRole('heading', { name: 'Shared Document' })).toBeVisible();
处理 MANUAL 步骤
标记为 [MANUAL] 的工作流步骤无法自动化。生成一个跳过或带注释的测试步骤:
// Step 4: [Guest1] [MANUAL] Verify the invitation email arrives in the inbox
// This step requires manual verification -- cannot be automated with Playwright.
// Consider using a test email service (e.g., Mailosaur, Mailhog) for automation.
如果只有一个步骤是手动操作,请勿为整个工作流生成 test.skip()。应改为添加注释,并继续处理后续可自动化的步骤。
处理前置条件
如果工作流除了每个角色的身份验证之外还有其他前置条件,请在测试开头将其生成为内联设置代码:
test.describe('Workflow 5: Collaborative Document Editing', () => {
test('host and guests collaboratively edit a document', async ({ browser }) => {
const hostCtx = await browser.newContext({
storageState: 'playwright/.auth/host.json',
});
const guest1Ctx = await browser.newContext({
storageState: 'playwright/.auth/guest1.json',
});
const hostPage = await hostCtx.newPage();
const guest1Page = await guest1Ctx.newPage();
// Precondition: A shared document named "Test Doc" exists
await hostPage.goto('/documents');
const docExists = await hostPage.getByText('Test Doc').isVisible();
if (!docExists) {
await hostPage.getByRole('button', { name: 'New Document' }).click();
await hostPage.getByLabel('Title').fill('Test Doc');
await hostPage.getByRole('button', { name: 'Create' }).click();
}
// Step 1: [Host] Open the shared document
await hostPage.getByText('Test Doc').click();
// ...
await hostCtx.close();
await guest1Ctx.close();
});
});
上下文中的 Vercel 绕过标头
创建浏览器上下文时,如果设置了环境变量,则应用 Vercel 绕过标头:
const extraHTTPHeaders = process.env.VERCEL_AUTOMATION_BYPASS_SECRET
? {
'x-vercel-protection-bypass': process.env.VERCEL_AUTOMATION_BYPASS_SECRET,
'x-vercel-set-bypass-cookie': 'samesitenone',
}
: {};
const adminCtx = await browser.newContext({
storageState: 'playwright/.auth/admin.json',
extraHTTPHeaders,
});
在规范文件顶部生成一个共享的 createContext 辅助函数,以避免重复:
import { test, expect, Browser, BrowserContext } from '@playwright/test';
async function createAuthContext(
browser: Browser,
persona: string,
): Promise<BrowserContext> {
const extraHTTPHeaders = process.env.VERCEL_AUTOMATION_BYPASS_SECRET
? {
'x-vercel-protection-bypass': process.env.VERCEL_AUTOMATION_BYPASS_SECRET,
'x-vercel-set-bypass-cookie': 'samesitenone',
}
: {};
return browser.newContext({
storageState: `playwright/.auth/${persona}.json`,
extraHTTPHeaders,
});
}
然后每个测试都使用:
const adminCtx = await createAuthContext(browser, 'admin');
const guest1Ctx = await createAuthContext(browser, 'guest1');
步骤 8:更新生成任务
将 "Generate: Playwright project" 标记为已完成,并附上生成文件、测试 describe 数量、已转换步骤、同步验证、跳过的手动步骤、角色设置文件以及并行操作块的元数据。
操作映射参考
此表提供从多用户工作流语言到 Playwright 代码的完整映射。与桌面映射的主要区别在于,每个操作都针对特定角色的页面变量。
| 工作流语言 | Playwright 代码 |
|---|---|
| [Admin] 导航到 /dashboard | await adminPage.goto('/dashboard') |
| [Host] 点击“Save”按钮 | await hostPage.getByRole('button', { name: 'Save' }).click() |
| [Guest1] 点击“Settings”链接 | await guest1Page.getByRole('link', { name: 'Settings' }).click() |
| [Admin] 在电子邮件字段中输入“hello” | await adminPage.getByLabel('Email').fill('hello') |
| [Guest2] 在搜索框中输入“query” | await guest2Page.getByPlaceholder('Search...').fill('query') |
| [Viewer] 验证标题“Dashboard”可见 | await expect(viewerPage.getByRole('heading', { name: 'Dashboard' })).toBeVisible() |
| [Host] 验证文本“Success”已出现 | await expect(hostPage.getByText('Success')).toBeVisible() |
| [Guest1] 验证 URL 包含 /settings | await expect(guest1Page).toHaveURL(/.*\/settings/) |
| [Admin] 从角色下拉菜单中选择“Editor” | await adminPage.getByLabel('Role').selectOption('Editor') |
| [Host] 勾选“Allow editing”复选框 | await hostPage.getByLabel('Allow editing').check() |
| [Admin] 取消勾选“Notifications”复选框 | await adminPage.getByLabel('Notifications').uncheck() |
| [Guest1] 等待加载提示消失 | await expect(guest1Page.getByText('Loading')).toBeHidden() |
| [Host] 等待 URL 包含 /document | await hostPage.waitForURL('**/document') |
| [Guest1] 上传“file.pdf” | await guest1Page.getByLabel('Upload').setInputFiles('file.pdf') |
| [Host] 按 Escape | await hostPage.keyboard.press('Escape') |
| [Admin] 将鼠标悬停在“Settings”菜单项上 | await adminPage.getByRole('menuitem', { name: 'Settings' }).hover() |
| [Viewer] 滚动到评论区 | await viewerPage.getByText('Comments').scrollIntoViewIfNeeded() |
| 同步验证: 5 秒内 | await expect(...).toBeVisible({ timeout: 5000 }) |
| [Host] 和 [Guest1] 同时打开 /doc | await Promise.all([hostPage.goto('/doc'), guest1Page.goto('/doc')]) |
| [Guest1] 刷新页面 | await guest1Page.reload() |
| [Viewer] 验证元素不可见 | await expect(viewerPage.getByRole('button', { name: 'Delete' })).toBeHidden() |
| [Admin] 将“Task A”拖到“Done”列 | await adminPage.getByText('Task A').dragTo(adminPage.getByText('Done')) |
| [Host] 清空搜索字段 | await hostPage.getByLabel('Search').clear() |
翻译示例
下面是一个完整示例,展示如何将 multi-user-workflows.md 中的多用户工作流转换为带有各角色独立上下文的 Playwright 测试代码。
输入工作流
## Workflow 3: Collaborative Document Editing
> Tests real-time collaborative editing where multiple users edit a shared
> document simultaneously and verify cross-user sync.
**Preconditions:**
- Host is logged in as Host persona (HOST_EMAIL / HOST_PASSWORD)
- Guest1 is logged in as Guest1 persona (GUEST1_EMAIL / GUEST1_PASSWORD)
- Guest2 is logged in as Guest2 persona (GUEST2_EMAIL / GUEST2_PASSWORD)
- A shared document named "Project Plan" exists (created by Host)
**Steps:**
1. [Host] Navigate to /documents and click "Project Plan"
- Verify the document editor loads with title "Project Plan"
2. [Guest1] Navigate to /documents and click "Project Plan"
- Verify the document editor loads with title "Project Plan"
- **Sync Verification:** Within 3 seconds, verify Guest1 sees Host's presence
indicator in the editor
3. [Guest2] Navigate to /documents and click "Project Plan"
- Verify the document editor loads with title "Project Plan"
- **Sync Verification:** Within 3 seconds, verify Guest2 sees both Host
and Guest1 presence indicators
4. [Host] Type "Introduction section" in the document body
- **Sync Verification:** Within 2 seconds, verify Guest1 sees
"Introduction section" appear in the document
- **Sync Verification:** Within 2 seconds, verify Guest2 sees
"Introduction section" appear in the document
5. [Guest1] Type "Added by Guest1" below Host's text
- **Sync Verification:** Within 2 seconds, verify Host sees
"Added by Guest1" appear in the document
6. [Host] Click the "Save" button
- Verify success message "Document saved" appears
7. [Guest1] Verify the save indicator shows "Saved"
8. [Guest2] Refresh the page
- Verify both "Introduction section" and "Added by Guest1" are visible
**Postconditions:**
- Document contains content from both Host and Guest1
- All three personas see consistent document state
输出测试代码
test.describe('Workflow 3: Collaborative Document Editing', () => {
test('host and guests collaboratively edit a shared document', async ({ browser }) => {
const hostCtx = await createAuthContext(browser, 'host');
const guest1Ctx = await createAuthContext(browser, 'guest1');
const guest2Ctx = await createAuthContext(browser, 'guest2');
const hostPage = await hostCtx.newPage();
const guest1Page = await guest1Ctx.newPage();
const guest2Page = await guest2Ctx.newPage();
// Step 1: [Host] Navigate to /documents and click "Project Plan"
await hostPage.goto('/documents');
await hostPage.getByText('Project Plan').click();
await expect(hostPage.getByRole('heading', { name: 'Project Plan' })).toBeVisible();
// Step 2: [Guest1] Navigate to /documents and click "Project Plan"
await guest1Page.goto('/documents');
await guest1Page.getByText('Project Plan').click();
await expect(guest1Page.getByRole('heading', { name: 'Project Plan' })).toBeVisible();
// Sync Verification: Within 3 seconds, Guest1 sees Host's presence
await expect(guest1Page.getByTestId('presence-indicator')).toBeVisible({ timeout: 3000 });
// Step 3: [Guest2] Navigate to /documents and click "Project Plan"
await guest2Page.goto('/documents');
await guest2Page.getByText('Project Plan').click();
// Sync Verification: Within 3 seconds, Guest2 sees both presence indicators
await expect(guest2Page.locator('[data-testid="presence-indicator"]')).toHaveCount(2, { timeout: 3000 });
// Step 4: [Host] Type "Introduction section" in the document body
await hostPage.getByLabel('Content').fill('Introduction section');
// Sync: Guest1 and Guest2 see the text within 2 seconds
await expect(guest1Page.getByText('Introduction section')).toBeVisible({ timeout: 2000 });
await expect(guest2Page.getByText('Introduction section')).toBeVisible({ timeout: 2000 });
// Step 5: [Guest1] Type "Added by Guest1" below Host's text
await guest1Page.getByLabel('Content').pressSequentially('Added by Guest1');
await expect(hostPage.getByText('Added by Guest1')).toBeVisible({ timeout: 2000 });
// Step 6: [Host] Click the "Save" button
await hostPage.getByRole('button', { name: 'Save' }).click();
await expect(hostPage.getByText('Document saved')).toBeVisible();
// Step 7-8: [Guest1] verify saved, [Guest2] refresh and verify
await expect(guest1Page.getByText('Saved')).toBeVisible();
await guest2Page.reload();
await expect(guest2Page.getByText('Introduction section')).toBeVisible();
await expect(guest2Page.getByText('Added by Guest1')).toBeVisible();
await hostCtx.close();
await guest1Ctx.close();
await guest2Ctx.close();
});
});
翻译说明
关键模式:步骤注释保留角色标签以便交叉引用;同步验证步骤使用明确的 { timeout: N },与工作流的时间要求相匹配;每个角色都使用自己的页面变量;createAuthContext 帮助程序负责处理 Vercel 绕过标头和存储状态;同时执行的操作使用 Promise.all();每项测试结束时都会清理上下文。
阶段 5:与用户审核(必需)
此阶段为必需步骤。未经用户批准,绝不能写入文件。
呈现生成的测试以供审核
使用 AskUserQuestion 呈现生成的项目。内容包括:项目结构列表、测试摘要(按优先级统计数量、同步验证、手动步骤)、角色到凭据的映射,以及完整的 workflows.spec.ts 和 playwright.config.ts 内容。请用户审核测试翻译、选择器、同步超时、角色映射和身份验证流程。请用户回复“approved”以继续,或提供反馈以便修订。
创建审批任务
创建 "Approval: Review generated tests"(in_progress),并包含迭代编号和数量。
处理反馈
如果用户提供反馈而非批准:应用更改,将当前审批任务标记为已完成,并设置 result: "changes_requested",附上反馈摘要;为第 N+1 次迭代创建新的审批任务,列出已做出的更改,然后重新提交修订后的测试。重复此流程,直到用户批准。
获得批准后
将审批任务标记为已完成,并设置 result: "approved",同时记录最终测试和同步验证的数量。
第 6 阶段:写入文件
将所有生成的文件写入 e2e/multi-user/。
步骤 1:创建目录结构
1. Ensure e2e/multi-user/ exists (create if not).
2. Ensure e2e/multi-user/tests/ exists (create if not).
3. Ensure .github/workflows/ exists (create if not).
步骤 2:写入所有文件
写入以下文件:playwright.config.ts、package.json、tsconfig.json、每个角色对应的一个 <persona>.setup.ts、workflows.spec.ts、.gitignore(均位于 e2e/multi-user/ 中),以及位于仓库根目录的 .github/workflows/e2e-multi-user.yml。
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/multi-user 目录中运行 npm install typescript --save-dev。如果仍然找不到 TypeScript,则停止并告知用户:“类型检查需要 TypeScript 编译器(tsc),但未能找到。请运行 npm install -D typescript 安装,或确保它在全局环境中可用。”类型检查不可省略,不能跳过。
流程:
1. Install dependencies (capture errors for diagnosis):
cd e2e/multi-user && npm install --ignore-scripts 2>&1 | tee /tmp/npm-install.log
2. Run type-check:
cd e2e/multi-user && 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/multi-user && 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/multi-user/"(已完成)任务,并附上已写入文件数量、输出目录、CI 工作流路径和角色设置文件数量。将主任务 "Convert: Multi-User Workflows to Playwright" 标记为已完成,并附上完整摘要元数据。
最终摘要
向用户提供:输出目录、CI 工作流路径、已写入文件数量(包括测试块、步骤和同步验证的数量)、按优先级层级划分的已转换工作流摘要、角色及设置文件数量、注释标记的手动步骤、来自代码库的选择器、审核迭代次数,以及后续步骤(安装、在本地运行、为所有角色配置 GitHub 密钥、推送以触发 CI)。
会话恢复
如果调用该技能时发现已有任务列表,请使用以下决策树确定从何处继续。
决策树
Check TaskList for "Convert: Multi-User Workflows to Playwright"
CASE 1: No task list exists
-> Start from Phase 1
CASE 2: Parse task is "completed", no Check task
-> Workflow file has been parsed, Persona Map is available
-> Resume from Phase 2 (check existing project)
CASE 3: Check task is "completed", no Selector task
-> Existing project has been checked
-> Resume from Phase 3 (selector discovery)
CASE 4: Selector task is "in_progress"
-> Agent may have timed out
-> Re-spawn the Explore agent
-> Resume from Phase 3 (partial)
CASE 5: Selector task is "completed", no Generate task
-> Selectors have been discovered
-> Resume from Phase 4 (generate project)
CASE 6: Generate task is "completed", no Approval task
-> Files were generated but not reviewed
-> Resume from Phase 5 (review with user)
CASE 7: Approval task exists with result "changes_requested"
-> User gave feedback but revisions were not completed
-> Read the feedback from task metadata
-> Apply changes and re-present for review
-> Resume from Phase 5 (next iteration)
CASE 8: Approval task is "completed" with result "approved", no Write task
-> Tests were approved but files were not written
-> Resume from Phase 6 (write files)
CASE 9: Write task is "completed"
-> Everything is done
-> Show the final summary and ask if the user wants to make changes
恢复时务必告知用户
I found an existing session for multi-user workflow-to-Playwright conversion.
Current state: [describe where things left off]
Last completed phase: [phase name]
Personas from parse: [list of personas]
I will resume from [next phase]. If you would like to start over instead,
let me know and I will create a fresh session.
处理更新
当用户选择“更新”模式(修改现有测试以匹配已更改的工作流)时,请遵循以下规则。
更新现有测试的规则
-
保留自定义修改——如果用户手动编辑了生成的测试(添加了自定义辅助函数、更改了选择器、添加了额外断言),请保留这些修改。查找
// CUSTOM:之类的注释,或任何与生成模式不匹配的代码。 -
将工作流与测试块匹配——使用
test.describe('Workflow N: ...')命名约定,将现有测试块与其源工作流匹配。这就是保持命名一致至关重要的原因。 -
更新已变更的工作流——如果某个工作流的步骤、角色分配或同步时间自上次生成以来发生了变化,只重新生成该工作流的
test.describe块。保留该块在文件中的位置。 -
添加新工作流——将新工作流添加到相应的优先级组(
Core Workflows、Feature Workflows、Edge Case Workflows)中。将它们追加到所在组的末尾。 -
标记已移除的工作流——如果某个工作流自上次生成以来已弃用,请注释掉其测试块,而不是删除:
// DEPRECATED: Workflow 9 -- Legacy Shared Calendar
// Reason: Calendar feature removed in v3.0
// Date: 2025-01-15
// test.describe('Workflow 9: Legacy Shared Calendar', () => { ... });
-
添加新的角色设置文件——如果 Persona Registry 中新增了角色,请生成新的设置文件,并在
playwright.config.ts和 CI 环境变量中添加相应项目。 -
弃用已移除角色的设置文件——不要删除已移除角色的设置文件。请在文件顶部添加
// DEPRECATED注释。 -
重新生成配置文件——始终重新生成
playwright.config.ts、package.json、.gitignore和 CI 工作流(这些文件不应包含自定义修改)。更新dependencies数组和 CI 环境变量,使其与当前角色保持一致。 -
保留
auth.setup.ts自定义设置——如果用户自定义了任何角色设置文件(不同的登录流程、MFA、OAuth),请保留其版本。只有在明确要求时才重新生成。
更新摘要
完成更新操作后,请提供变更摘要,涵盖:保留/更新/弃用/新增的测试块、每个已变更工作流的详情、角色的新增/移除情况、新设置文件,以及重新生成与保留的文件。
选择器策略参考
将工作流步骤转换为 Playwright 代码时,始终优先使用最可靠的可用选择器。下表展示了首选顺序,与 Playwright 官方建议一致。在多用户测试中,适用相同的选择器策略——只有不同角色对应的页面变量会改变。
| 优先级 | 策略 | 适用场景 | 示例(管理员角色) |
|---|---|---|---|
| 1 | getByRole | 按钮、链接、标题、复选框、单选按钮,以及任何具有显式 ARIA 角色的元素 | adminPage.getByRole('button', { name: 'Submit' }) |
| 2 | getByLabel | 具有关联 <label> 元素或 aria-label 属性的表单输入框 | hostPage.getByLabel('Email address') |
| 3 | getByPlaceholder | 没有标签但带有占位文本的输入框 | guest1Page.getByPlaceholder('Search...') |
| 4 | getByText | 通过可见文本内容识别的非交互元素 | guest2Page.getByText('Welcome back') |
| 5 | getByTestId | 带有 data-testid 属性的元素;其他选择器含糊不清时很有用 | viewerPage.getByTestId('sidebar-nav') |
| 6 | CSS 选择器 | 没有可用语义选择器时的最后选择 | hostPage.locator('.custom-widget > .action-btn') |
选择器反模式
生成的测试中应避免使用:ID 选择器(#submit-btn)、类选择器(.btn-primary)、结构选择器(div > span:nth-child(3))和属性选择器([onclick="save()"])。这些选择器都很脆弱,重构时容易失效。始终优先使用上方优先级表中的语义定位器。
多用户测试模式参考
模式 1:跨角色顺序操作
一个角色执行操作,然后由另一个角色验证。这是最常见的模式。
await adminPage.getByRole('button', { name: 'Invite' }).click();
await expect(guest1Page.getByText(/invitation/i)).toBeVisible({ timeout: 5000 });
模式 2:使用 Promise.all 并行执行操作
当多个角色必须同时执行操作时:
await Promise.all([
hostPage.goto('/documents/shared-doc'),
guest1Page.goto('/documents/shared-doc'),
]);
模式 3:基于轮询的同步验证
对于使用轮询而非推送更新的功能,使用 toPass() 进行基于重试的断言:
await expect(async () => {
await guest1Page.reload();
await expect(guest1Page.getByText('Updated Content')).toBeVisible();
}).toPass({ timeout: 15000, intervals: [1000, 2000, 3000] });
模式 4:基于角色的可见性检查
await expect(adminPage.getByRole('button', { name: 'Delete' })).toBeVisible();
await expect(viewerPage.getByRole('button', { name: 'Delete' })).toBeHidden();
模式 5:上下文生命周期
仅为工作流中的角色创建上下文。测试结束时务必关闭上下文:
test('workflow with Admin and Guest1 only', async ({ browser }) => {
const adminCtx = await createAuthContext(browser, 'admin');
const guest1Ctx = await createAuthContext(browser, 'guest1');
const adminPage = await adminCtx.newPage();
const guest1Page = await guest1Ctx.newPage();
// ... test steps ...
await adminCtx.close();
await guest1Ctx.close();
});
约束
- 允许使用的工具 —— 此技能仅使用 Read、Write、Glob、Grep 和 Task/Explore 工具。请勿使用 Chrome MCP、iOS Simulator MCP、Playwright CLI 或任何其他浏览器自动化工具。所有浏览器交互都以代码形式生成,在转换期间绝不执行。
- 输出位置 —— 所有测试文件都放在
e2e/multi-user/。CI 工作流放在仓库根目录下的.github/workflows/e2e-multi-user.yml。 - 始终为每个角色生成身份验证 —— Persona Registry 中的每个角色都会生成一个设置文件,即使并非每个角色都会出现在每个工作流中。每个设置文件都会妥善处理缺失的凭据。
- 始终包含 Vercel 标头 —— 设置环境变量后,
x-vercel-protection-bypass和x-vercel-set-bypass-cookie标头会在playwright.config.ts和createAuthContext帮助函数中有条件地应用。 - 工作流不作为运行时依赖项 —— 生成的测试项目完全独立。它在运行时不会读取或导入工作流 Markdown 文件。
- Playwright 最佳实践 —— 按照
getByRole、getByLabel、getByText、getByPlaceholder和getByTestId的顺序使用它们。除非绝对必要,否则避免使用 CSS 和 XPath 选择器。 - 多上下文最佳实践 —— 使用
browser.newContext()(而非browser.newPage())创建隔离的角色专属上下文,并为其设置独立的存储状态。测试结束时关闭所有上下文。 - 默认按顺序执行 —— 设置
fullyParallel: false,因为工作流中的多用户测试本质上是按顺序执行的(步骤 N 依赖步骤 N-1)。仅在明确需要同时执行操作时才使用Promise.all()。 - 角色变量命名 —— 始终遵循
<lowercase-persona>Ctx和<lowercase-persona>Page的命名约定,以确保所有生成的测试保持一致。