首页
/ Appsmith Playwright E2E 测试编写与自动验证工作流:从环境配置、认证链到失败分诊的完整实践

Appsmith Playwright E2E 测试编写与自动验证工作流:从环境配置、认证链到失败分诊的完整实践

2026-09-05 18:32:47作者:尤峻淳Whitney

本文围绕 Appsmith 仓库内置的 Cursor Skill write-and-verify-pw-test 展开,系统讲解如何针对一个运行中的 Appsmith 部署(DP)编写 Playwright E2E 测试并完成闭环验证:配置目标环境与凭据、遵循项目测试规范(选择器优先级、自动重试断言、禁止硬等待)、按 smoke/sanity/regression/ee 分层确定测试位置与 project、通过 signup-setup → setup 认证链自动完成登录态持久化、以及失败后的"测试缺陷 vs 产品缺陷"分诊与最多 3 次自动修复循环。读完本文,你可以照此流程在真实部署上落地一条可重复运行的 E2E 测试,并理解 Appsmith Playwright 测试基建(fixtures、setup project、状态文件、feature flag 分层覆盖)的设计原理。

工作流总览

该 Skill 定义了一个八步闭环,目标是让"从一句自然语言提示词到一条验证通过的 E2E 测试"的过程完全自动化:

步骤 内容 关键动作
1 配置环境 configure-env.js 合并写入 app/client/playwright/.env
2 阅读项目规范 内化 .cursor/rules/playwright.mdc 的选择器、断言、等待策略
3 确定落点 选层级(tier)、决定复用/新建 project、必要时拆分 spec
4 编写 Spec 按 smoke 简单模板或 fixture 模板编写,必要时建 POM
5 Lint 检查 npx eslint <spec-file> --ext .ts,先修再跑
6 运行测试 npx playwright test <spec> --project=<project>,依赖认证链
7 重试循环 最多 3 次,先分诊失败原因再决定是否修 spec
8 输出摘要 PASS/FAIL 结构化报告

前置条件只有一个:确保 bundled Chromium 已安装(--ui 模式依赖它,不会探测系统浏览器,环境变量如 PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH--ui 模式无效):

cd app/client && npx playwright install chromium 2>/dev/null

Step 1:环境配置——configure-env.js 的合并写入机制

测试目标部署、凭据、数据源地址等信息统一落在 app/client/playwright/.env,由 playwright.config.ts 通过 dotenv 加载(dotenv.config({ path: path.resolve(__dirname, "playwright/.env") }))。Skill 不让你手改这个文件,而是调用 Skill 自带的脚本:

node .cursor/skills/write-and-verify-pw-test/scripts/configure-env.js \
  --PLAYWRIGHT_BASE_URL=https://target-dp.appsmith.com \
  --USERNAME=user@example.com \
  --PASSWORD=secret

阅读 configure-env.js 源码可以看到它的行为细节:

  • 脚本以相对自身位置解析出仓库根目录(path.resolve(__dirname, "../../../..")),定位到 app/client/playwright/.env
  • 参数格式为 --KEY=VALUE,逐个剥离 -- 前缀并按第一个 = 拆分;没有任何变量时打印用法并 exit(1)
  • .env 已存在,逐行解析(跳过空行与 # 注释行)装入 Map,再把本次传入的键值对覆盖合并进去——即"只传调用方提供的变量,未覆盖的既有值全部保留";
  • 写出时若目录不存在会 mkdirSync({ recursive: true }) 自动创建,并逐行打印写入结果,其中 PASSWORD 的值显示为 **** 做脱敏。

支持的变量及是否必填:

变量 必填 说明
PLAYWRIGHT_BASE_URL 目标部署 URL(config 中缺省回落到 https://dev.appsmith.com
USERNAME .env 时必填 登录凭据,供 signup/auth setup 使用
PASSWORD .env 时必填 登录凭据
DATASOURCE_HOST 数据源测试用(migration setup 缺省回落到 host.docker.internal
GITEA_BASE_URL Git 测试用
GITEA_API_TOKEN Git 测试用
GIT_CLONE_URL Git 测试用(缺省 git@host.docker.internal:Cypress
PW_FLAG_OVERRIDES JSON 字符串,如 '{"flag": true}',用于 feature flag 覆盖

PW_FLAG_OVERRIDES 既可以经脚本写入 .env,也可以在运行测试时内联传入(见 Step 6)。

Step 2:项目规范——在写 Spec 之前必须内化的七条铁律

Skill 要求动笔前显式阅读 .cursor/rules/playwright.mdc(该规则文件带 globs: app/client/playwright/**/*.ts,编辑 Playwright 代码时会自动应用)。核心规则逐条拆解:

1. 导入自研 fixtures,而非 @playwright/test 统一写 import { test, expect } from "../../fixtures"。查看 fixtures/index.ts 可以看到自研层扩展了什么:

  • flagOverridesauto: true):把 BASE_FLAG_OVERRIDESPW_FLAG_OVERRIDES 环境变量合并后,用 page.route() 拦截 **/api/v1/users/features**/api/v1/consolidated-api/*,先 route.fetch() 拿到服务端真实响应,再把覆盖项合并json.data 后 fulfill——即覆盖是"叠加"而非"替换",这是与 Cypress 时代"整包替换 flag"模式的关键区别;
  • api:带 storageState: "playwright/auth/user.json"APIRequestContext,供 setup 类测试做纯 API 操作;
  • workspace / app:通过 helpers/api.tscreateWorkspace / createApp 经 API 创建带随机后缀的 workspace 和 app,测试结束后自动删除 workspace(app 随 workspace 级联),体现"能用 API 完成的 setup 绝不用 UI"的原则;
  • expect 再导出自 matchers,附加了 toShowToast 等自定义 matcher。

2. 选择器优先级。 getByRole() > getByLabel() / getByPlaceholder() / getByText() > getByTestId() > 来自 constants/selectors.tsSELECTORSdata-* 属性)> 裸 CSS(最后手段且必须注释原因)。规范里特别指出 Appsmith 的实际情况:不少表单组件(如 ads-oldFormGroup)渲染了视觉标签但没有正确的 <label for> 关联,此时应优先 getByPlaceholder()。规则还强调:如果 getByLabel() 失败,很可能暴露的是应用真实的可访问性缺陷,应沿优先级列表降级,而不是退到 page.locator("input[name='...']")——裸 CSS 会把测试耦合到 DOM 实现上,这正是规范中提到的 Cypress 测试曾经踩过的脆弱性陷阱。

3. 禁止硬等待。waitForTimeout、无 networkidle(规范解释其不可靠:等"500ms 无请求",既 flaky 又慢)。正确姿势是等待"证明页面就绪的有意义元素",或在变更后等待对应 API 响应:

const response = page.waitForResponse(r => r.url().includes(API.actionsExecute));
await page.getByRole("button", { name: "Update" }).click();
await response;

4. 断言必须走自动重试。 await expect(locator).toBeVisible() 这类 Playwright 内建 expect 会重试到超时;expect(await locator.isVisible()).toBe(true) 只解析一次,是明确禁止的 BAD 写法。

5. 常量集中管理。 API 路径来自 constants/api-routes.ts,选择器来自 constants/selectors.ts,路由来自 constants/routes.ts,测试文件中永不硬编码。该文件实际维护了 workspacesdatasourcesgitImportactionsExecute 等 15 个端点常量。规范的判定口诀是:字符串因应用变更而变的是常量,因测试数据变更而变的(如断言值 "Bangladesh")留在行内。

6. POM 纪律。 构造函数只收 Page;POM 永不写断言(断言属于测试文件)、永不 sleep、方法保持 1–5 行、不组合其他 POM(多 POM 编排放 helpers/)。现有示例可参考 page-objects/home.page.tscomponents/table.component.ts

7. API 契约必须先验证。 任何 request.get / request.post / page.request(尤其是 query 参数和请求体)落地前,必须去 app/server/appsmith-server/ 下 grep 对应的 @GetMapping Controller,追到 service 层确认 @RequestParam / @RequestBody精确参数名与必填性。规范记录了一个真实事故反模式:因为测试手上是 appId 就猜 ?applicationId=,而列表 API 实际只接受 workspaceIdGET /api/v1/datasourcesDatasourceServiceCEImpl.getAllWithStorages),参数名猜错往往在 CI 里只给你一个无提示的 400。migration-setup 中对 API.datasourcesAPI.plugins 的调用均以 workspaceId 传参,正是这一契约的落地。

Step 3:确定落点——层级、project 与 spec 拆分

按 tier 选目录

Tier 目录 何时使用
smoke playwright/tests/smoke/ 登录、建应用、存活检查
sanity playwright/tests/sanity/<feature>/ 某功能域的核心流程
regression playwright/tests/regression/<feature>/ 边界场景、复杂交互
ee playwright/tests/ee/{sanity,regression}/<feature>/ EE 专属功能

调用方指定 tier 就用指定的,否则默认 sanitysanity/regression/ 下的功能子目录是强制的。playwright.mdc 进一步给出各 tier 的规模与触发频率定位:smoke 约 10–15 条、每次 push/PR 跑;sanity 约 50–100 条、每个 PR 跑;regression 无上限、夜间/发版跑;升级路径就是"移动文件"(regression → sanity → smoke)。作用域完全由目录结构表达,不使用 { tag: [...] } 元数据。

复用现有 project、新建 project,还是拆多个 spec

决策依据是读取 playwright.config.ts 中各 project 的 testDirdependenciestestIgnore

  • 复用现有 project:新 spec 的目录已落在某 project 的 testDir 内,且该 project 的依赖链已覆盖所需前置。例如 tests/sanity/widgets/chart.spec.ts 直接用 sanity project。
  • 新建 project:当 spec 需要现有 setup 链未覆盖的高成本共享前置(经 API 导入应用、连接特定数据源、灌测试数据),且挂在共享 project 里会拖慢无关测试时。新建须遵循四件套模式(与 fixtures/migration.setup.ts 一一对应):
    1. Setup 文件 playwright/fixtures/<name>.setup.ts——用 API 完成昂贵操作(不启浏览器),把关键状态写入 playwright/.state/<name>.json
    2. Teardown 文件 playwright/fixtures/<name>.teardown.ts——读状态文件并清理资源;
    3. 状态读取器 playwright/helpers/<name>-state.ts——为测试文件提供类型化访问器;
    4. playwright.config.ts 中新增带 dependenciestestDirteardown 的 project 条目。
  • 拆多个 spec 文件:提示词描述了多个独立页面/关注点时一个 spec 对一个页面;共享 setup 的测试应可并行(每个文件独立读取共享状态、直接导航到目标页);单 spec 超过约 10 条测试时按子功能拆分。Git 迁移测试就是实例:tests/regression/git/ 下分列 migration-mysql.spec.tsmigration-postgres.spec.tsmigration-mongo.spec.tsmigration-modal-form.spec.tsmigration-widget-bindings.spec.ts,全部共用 migration-setup project,各自聚焦一个页面。

经验法则:一个 spec 文件一个 test.describe,一个 test() 一个关注点(名字里出现 "and"/"&" 就该拆)。测试文件命名 kebab-case.spec.ts,POM 命名 kebab-case.page.ts / kebab-case.component.ts

Step 4:编写 Spec 与 Lint 检查

Skill 给出两类模板。smoke 级简单 spec

import { test, expect } from "../../fixtures";
import { ROUTES } from "../../constants/routes";

test.describe("Smoke — Feature Name", () => {
  test("behavior description in lowercase", async ({ page }) => {
    await page.goto(ROUTES.applications);
    await expect(page.getByRole("button", { name: /new/i })).toBeVisible();
  });
});

仓库中真实存在的 tests/smoke/login.spec.ts 与模板完全同构:进入 ROUTES.applications 后断言 URL 为 /applications/ 且出现 "new" 按钮——由于测试 project 已复用 storageState,spec 本身不需要任何登录逻辑。

依赖 fixture workspace/app 的 sanity/regression spec

import { test, expect } from "../../fixtures";
import { SELECTORS } from "../../constants/selectors";

test.describe("Feature — Specific Area", () => {
  test("filters table by country", async ({ page, app }) => {
    await page.goto(app.url);
    await expect(page.locator(SELECTORS.widgetInDeployed("textwidget")).first()).toBeVisible();
    // test logic here
  });
});

这里的 app fixture 会在测试前经 API 创建 app-<uuid8> 应用并挂到一次性 workspace 下,测试后自动清理。需要 POM 时在 playwright/page-objects/(可复用组件放 components/)新建,遵循既有模式。

每写完一个文件立即执行 lint,通过后再继续:

cd app/client && npx eslint <spec-file-path> --ext .ts

playwright.mdc 中的"批量生成检查清单"解释了这一步为何放在写作循环内而非最后:agent 批量生成时的默认倾向是抄最省事的写法(networkidle、内联字符串、投机性 import),清单逐条拦截——每个 page.goto() 后必须跟随条件断言、API 路径必须取自 API.*、选择器必须取自 SELECTORS.*、移除未使用导入、ESLint 能捕获 networkidle 与硬等待、每个新 request 调用先核对服务端参数名。

Step 6 核心:认证链与 project 映射

运行前必须理解 playwright.config.ts 的 project 依赖图。顶层配置为 fullyParallel: truetimeout: 60_000expect.timeout: 10_000actionTimeout: 15_000,且 use 中固定了 storageState: "playwright/auth/user.json"(各测试 project)以及 trace: "on"screenshot: "only-on-failure"video: "on"ignoreHTTPSErrors: true;CI 环境下额外开启 1 次重试、4 worker、html+json 双 reporter,并 forbidOnly

认证链:signup-setup → setup → 测试 project

signup-setup  →  setup  →  你的测试 project(smoke / sanity / regression / regression-git)
  • signup-setupfixtures/signup.setup.ts):让配置的用户一定存在,自动处理全新部署。源码显示它 page.goto(ROUTES.signup) 后用 getByTestId("firstName")(空实例超级用户 onboarding,会经 /setup/welcome)与 getByPlaceholder("Enter your email")(非空实例的旧版 signup 流)做 .or() 判别;旧版流中若重定向回 /user/signuperror 参数匹配 /already|sign in instead/i,则视为成功(账号已存在);最后完成 profiling(t--user-proficiencyt--user-use-case 单选 + "Get started")并断言到达 /applications
  • setupfixtures/auth.setup.ts):用 USERNAME / PASSWORD 走登录页,等待离开 /loginpage.context().storageState({ path: "playwright/auth/user.json" }) 持久化登录态——后续所有测试 project 的 use.storageState 都指向这个文件。
  • 测试 project:携带保存的登录态执行 spec。

这条链在使用 --project 时自动按依赖顺序执行。绝不能绕过它直接跑测试文件(不带 project flag),否则全新实例上没有账号、没有 auth state,测试必然失败。

project 映射表与"为什么重要"

Spec 目录 --project Setup 链
tests/smoke/ smoke signup → auth
tests/sanity/ sanity signup → auth
tests/regression/(非 git) regression signup → auth
tests/regression/git/ regression-git signup → auth → migration-setup(+ teardown)

对应 playwright.config.ts 中的 wiring:regression project 显式 testIgnore: ["**/git/**"],与独立的 regression-git project(testDir: "./playwright/tests/regression/git")互不重叠;regression-git 依赖 migration-setup,后者又依赖 setup 并通过 teardown: "migration-teardown" 绑定清理 project。

跑错 project 的直接后果:以 git 迁移为例,migration.setup.ts 会经 API 创建 workspace、调用 API.gitImportKeys?keyType=ECDSA)生成导入密钥、把公钥注册为 Gitea deploy key、经 API.gitImport?workspaceId=... 导入 TED-migration-test-1 仓库、按 DS_CONFIGS(postgres 5433 / mysql 3306 / mongo 28017,host 取 DATASOURCE_HOST)逐个重连数据源、抓取页面 slug 列表,最终把 MigrationState(workspaceId、appId、appSlug、branchName、deployKeyId、pages 映射等)写入 playwright/.state/migration.json。spec 侧经 helpers/migration-state.tsloadMigrationState() / getPage() 读取——该读取器在状态文件缺失时直接抛错并提示"确保 migration-setup project 成功运行"。若把这类 spec 放到普通 regression project 下,setup 根本不执行,所有用例即刻失败。状态文件本身 gitignored,只含 ID/slug/名称、永不含凭据;测试只读不写;需要变更共享数据的测试必须在 afterEach 恢复原状。

若你的新 spec 需要自定义 setup project,流程为:先查现有 setup 是否覆盖 → 不覆盖则按上述四件套新建 → 在 playwright.config.ts 注册 → 更新 Skill 中的映射表。并且:这份表可能落后于 config 的演化,每次动手前都以重读 playwright.config.ts 为准。

运行命令

cd app/client && npx playwright test <spec-file-path> --project=<project>

若提供了 PW_FLAG_OVERRIDES

cd app/client && PW_FLAG_OVERRIDES='{"flag_name": true}' npx playwright test <spec-file-path> --project=<project>

Feature flag 覆盖的分层优先级(高者胜):单测内 page.route() > PW_FLAG_OVERRIDES 环境变量 > config/feature-flags.tsBASE_FLAG_OVERRIDES(默认空)> 服务端真实响应。fixtures/index.ts 中的 flagOverrides fixture 实现了"先 fetch 真实响应、再合并指定键"的语义,杜绝了整包替换导致的 flag 状态失真。

超时设置:执行该命令的工具 block_until_ms 至少设为 120000(2 分钟)——全新部署的首次运行要叠加 signup + auth 两个 setup project,整体可超过 60 秒(注意这与 config 中单条用例 timeout: 60_000 是两个层面的限制)。

Step 7:失败分诊与最多 3 次的重试循环

测试失败时,Skill 要求先分类再动手,分类决定了动作:

测试缺陷信号(修 spec,计为第 N+1 次尝试):

信号 含义
locator.click: Target closed 选择器错误或过早导航
Timeout 且元素在 DOM 中存在但不可见 缺等待或 locator 不对
strict mode violation locator 命中多个元素,需 .first() 或精化
expect(received).toHaveText(expected) 且期望值明显写错 测试数据错误
导入错误、TypeScript 错误 spec 代码问题
waiting for locator 选择器匹配不到任何东西,去看实际页面

动作:读错误输出、修 spec、重跑。

产品缺陷信号(停止重试,转入诊断):

  • toHaveText 期望值正确但实际值显示应用行为明显错误;
  • net::ERR_CONNECTION_REFUSED → 部署挂了;
  • 认证 setup 成功之后仍出现 401 / 403 → 权限回归;
  • 选择器正确但 UI 渲染内容错误;
  • API 调用返回 500。

动作:停止重试,切换 diagnose-pw-failure Skill 做诊断。

模糊失败:默认按"修 spec"处理,给足前 2 次尝试;若同一断言连续 3 次失败且 received 值完全相同,大概率是产品缺陷,转诊断。这套分诊的价值在于把 flaky 测试与真实回归分开——前者消耗自动修复额度,后者立即进入人工诊断路径而不被"修 spec"循环掩盖。

Step 8:结构化结果摘要

测试通过(或重试耗尽)后输出固定格式的摘要,便于 Agent 与 CI 消费:

## Playwright Test Result: PASS

**Spec**: playwright/tests/sanity/widgets/table-filter.spec.ts
**Deployment**: https://my-dp.appsmith.com
**Project**: sanity
**Attempts**: 2 (1 fix applied: wrong selector for filter button)

### What was tested
- Table widget renders with data
- Filtering by country "Ba" shows Bangladesh

产品缺陷场景则输出 FAIL (product bug suspected),包含期望/实际值、截图路径(playwright/results/<test-name>/screenshot.png,来自 config 的 outputDir: "./playwright/results" + screenshot: "only-on-failure")以及指向诊断输出的引用。

关联文件索引

文件 角色
write-and-verify-pw-test/SKILL.md 本文主体:八步工作流定义
write-and-verify-pw-test/scripts/configure-env.js .env 合并写入脚本
playwright.config.ts project 依赖图、超时、reporter、storageState
.cursor/rules/playwright.mdc POM/选择器/断言/等待/分层/并行的完整规范
fixtures/index.ts 自研 test/expect:api/workspace/app/flagOverrides
fixtures/signup.setup.ts / fixtures/auth.setup.ts 认证链两环:确保用户存在、持久化登录态
fixtures/migration.setup.ts / migration.teardown.ts / helpers/migration-state.ts setup/teardown/state-reader 三件套实例
constants/api-routes.ts / routes.ts / selectors.ts API、路由、选择器常量
config/feature-flags.ts 基础 flag 覆盖层(默认空)
tests/smoke/login.spec.ts / tests/regression/git/ 两级真实 spec 样例
app/client/package.json test:pw:smoke / test:pw:sanity / test:pw:regression / test:pw:regression-git / test:pw:flake-check 等脚本入口

需要强调的适用前提:该工作流面向已运行的 Appsmith 部署(DP 或本地 docker 环境),凭据、DATASOURCE_HOSTGIT_CLONE_URL 等须与目标部署配套;migration 相关变量与 setup 仅在有 Gitea 与对应数据库容器时才可用。本地快速调试亦可参考 test:pw:ci 脚本(PLAYWRIGHT_BASE_URL=http://localhost 直连本机实例)。

登录后查看全文
热门项目推荐
相关项目推荐