mcpField
M
multi-user-workflow-to-playwright

multi-user-workflow-to-playwright

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

关于 multi-user-workflow-to-playwright

multi-user-workflow-to-playwright 是面向 QA 自动化工程师的技能,可将多用户工作流文档转换为 Playwright 测试项目。它为各 persona 生成独立认证设置,并使用不同浏览器上下文编写测试。

主要功能

  1. 01解析带 persona 标签的工作流
  2. 02为各 persona 生成认证设置
  3. 03生成多项目 Playwright 配置
  4. 04使用独立浏览器上下文执行测试
  5. 05生成 CI 配置和部署保护头

适用场景

  1. 01将协作流程文档转为端到端测试
  2. 02测试不同用户在同一流程中的交互
  3. 03为每个 persona 配置测试认证
  4. 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.md
translation-example.md
references/
action-mapping.md
agent-prompts.md
multi-context-patterns.md
selector-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:读取并解析

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

  1. 工作流编号——来自 ## Workflow [N]: 标题
  2. 工作流名称——编号后面的描述性名称
  3. 身份验证要求——来自 或
  4. 优先级——来自 , 或 ``
  5. 角色——来自 ``(以逗号分隔的列表)
  6. 预估步骤数——来自 ``
  7. 同步点——来自 ``
  8. 弃用标记——来自 ``(跳过已弃用的工作流)
  9. 前置条件——**Preconditions:** 下的项目符号列表
  10. 步骤——每个带编号的步骤及其 [PersonaName] 标签和验证子步骤
  11. 后置条件——**Postconditions:** 下的项目符号列表
步骤 3:解析角色注册表

在工作流文件顶部附近,提取角色注册表表格并构建角色映射。对于每个角色,推导:

  • contextVar:小写角色名称 + Ctx(例如,adminCtx、guest1Ctx)
  • pageVar:小写角色名称 + Page(例如,adminPage、guest1Page)
  • authFile:playwright/.auth/<lowercase-persona>.json
  • setupFile:<lowercase-persona>.setup.ts
  • emailVar / 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
});
结构规则
  1. 顶层分组——test.describe('Core Workflows', ...)、test.describe('Feature Workflows', ...) 和 test.describe('Edge Case Workflows', ...) 与工作流文件中的优先级层级保持一致。
  2. 工作流块——每个工作流都会成为一个 test.describe('Workflow N: Name', ...),并嵌套在对应的优先级分组中。
  3. 每个工作流只包含一个测试——每个工作流都是一个包含所有连续步骤的 test() 调用。这样可以保证每个工作流具有原子性——要么整体通过,要么整体失败。
  4. 在测试开始时设置上下文——每个测试都会为该工作流涉及的每个角色创建浏览器上下文。只为出现在工作流 `` 列表中的角色创建上下文。
  5. 在测试结束时清理上下文——每个测试都会以确保 finally 安全的模式或在测试结束时关闭所有浏览器上下文。
  6. 步骤注释——工作流中的每个步骤前都要添加注释(// Step N: [Persona] ...),并为其验证添加注释(// Verify: ... 或 // Sync Verification: ...)。
角色标签到上下文的映射

每个工作流步骤中的 [PersonaName] 前缀决定要使用哪个页面变量:

工作流标签上下文变量页面变量
[Admin]adminCtxadminPage
[Host]hostCtxhostPage
[Guest1]guest1Ctxguest1Page
[Guest2]guest2Ctxguest2Page
[Guest3]guest3Ctxguest3Page
[Viewer]viewerCtxviewerPage

一般规则:上下文变量由小写的角色名称加上 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] 导航到 /dashboardawait 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 包含 /settingsawait 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 包含 /documentawait hostPage.waitForURL('**/document')
[Guest1] 上传“file.pdf”await guest1Page.getByLabel('Upload').setInputFiles('file.pdf')
[Host] 按 Escapeawait 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] 同时打开 /docawait 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.

处理更新

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

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

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

  3. 更新已变更的工作流——如果某个工作流的步骤、角色分配或同步时间自上次生成以来发生了变化,只重新生成该工作流的 test.describe 块。保留该块在文件中的位置。

  4. 添加新工作流——将新工作流添加到相应的优先级组(Core Workflows、Feature Workflows、Edge Case Workflows)中。将它们追加到所在组的末尾。

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

// DEPRECATED: Workflow 9 -- Legacy Shared Calendar
// Reason: Calendar feature removed in v3.0
// Date: 2025-01-15
// test.describe('Workflow 9: Legacy Shared Calendar', () => { ... });
  1. 添加新的角色设置文件——如果 Persona Registry 中新增了角色,请生成新的设置文件,并在 playwright.config.ts 和 CI 环境变量中添加相应项目。

  2. 弃用已移除角色的设置文件——不要删除已移除角色的设置文件。请在文件顶部添加 // DEPRECATED 注释。

  3. 重新生成配置文件——始终重新生成 playwright.config.ts、package.json、.gitignore 和 CI 工作流(这些文件不应包含自定义修改)。更新 dependencies 数组和 CI 环境变量,使其与当前角色保持一致。

  4. 保留 auth.setup.ts 自定义设置——如果用户自定义了任何角色设置文件(不同的登录流程、MFA、OAuth),请保留其版本。只有在明确要求时才重新生成。

更新摘要

完成更新操作后,请提供变更摘要,涵盖:保留/更新/弃用/新增的测试块、每个已变更工作流的详情、角色的新增/移除情况、新设置文件,以及重新生成与保留的文件。


选择器策略参考

将工作流步骤转换为 Playwright 代码时,始终优先使用最可靠的可用选择器。下表展示了首选顺序,与 Playwright 官方建议一致。在多用户测试中,适用相同的选择器策略——只有不同角色对应的页面变量会改变。

优先级策略适用场景示例(管理员角色)
1getByRole按钮、链接、标题、复选框、单选按钮,以及任何具有显式 ARIA 角色的元素adminPage.getByRole('button', { name: 'Submit' })
2getByLabel具有关联 <label> 元素或 aria-label 属性的表单输入框hostPage.getByLabel('Email address')
3getByPlaceholder没有标签但带有占位文本的输入框guest1Page.getByPlaceholder('Search...')
4getByText通过可见文本内容识别的非交互元素guest2Page.getByText('Welcome back')
5getByTestId带有 data-testid 属性的元素;其他选择器含糊不清时很有用viewerPage.getByTestId('sidebar-nav')
6CSS 选择器没有可用语义选择器时的最后选择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 的命名约定,以确保所有生成的测试保持一致。

评论

支持 Markdown