Appsmith Playwright E2E 测试编写与自动验证工作流:从环境配置、认证链到失败分诊的完整实践
本文围绕 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 可以看到自研层扩展了什么:
flagOverrides(auto: true):把BASE_FLAG_OVERRIDES与PW_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.ts 的createWorkspace/createApp经 API 创建带随机后缀的 workspace 和 app,测试结束后自动删除 workspace(app随 workspace 级联),体现"能用 API 完成的 setup 绝不用 UI"的原则;expect再导出自 matchers,附加了toShowToast等自定义 matcher。
2. 选择器优先级。 getByRole() > getByLabel() / getByPlaceholder() / getByText() > getByTestId() > 来自 constants/selectors.ts 的 SELECTORS(data-* 属性)> 裸 CSS(最后手段且必须注释原因)。规范里特别指出 Appsmith 的实际情况:不少表单组件(如 ads-old 的 FormGroup)渲染了视觉标签但没有正确的 <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,测试文件中永不硬编码。该文件实际维护了 workspaces、datasources、gitImport、actionsExecute 等 15 个端点常量。规范的判定口诀是:字符串因应用变更而变的是常量,因测试数据变更而变的(如断言值 "Bangladesh")留在行内。
6. POM 纪律。 构造函数只收 Page;POM 永不写断言(断言属于测试文件)、永不 sleep、方法保持 1–5 行、不组合其他 POM(多 POM 编排放 helpers/)。现有示例可参考 page-objects/home.page.ts 与 components/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 实际只接受 workspaceId(GET /api/v1/datasources → DatasourceServiceCEImpl.getAllWithStorages),参数名猜错往往在 CI 里只给你一个无提示的 400。migration-setup 中对 API.datasources 与 API.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 就用指定的,否则默认 sanity。sanity/ 和 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 的 testDir、dependencies 与 testIgnore:
- 复用现有 project:新 spec 的目录已落在某 project 的
testDir内,且该 project 的依赖链已覆盖所需前置。例如tests/sanity/widgets/chart.spec.ts直接用sanityproject。 - 新建 project:当 spec 需要现有 setup 链未覆盖的高成本共享前置(经 API 导入应用、连接特定数据源、灌测试数据),且挂在共享 project 里会拖慢无关测试时。新建须遵循四件套模式(与 fixtures/migration.setup.ts 一一对应):
- Setup 文件
playwright/fixtures/<name>.setup.ts——用 API 完成昂贵操作(不启浏览器),把关键状态写入playwright/.state/<name>.json; - Teardown 文件
playwright/fixtures/<name>.teardown.ts——读状态文件并清理资源; - 状态读取器
playwright/helpers/<name>-state.ts——为测试文件提供类型化访问器; playwright.config.ts中新增带dependencies、testDir、teardown的 project 条目。
- Setup 文件
- 拆多个 spec 文件:提示词描述了多个独立页面/关注点时一个 spec 对一个页面;共享 setup 的测试应可并行(每个文件独立读取共享状态、直接导航到目标页);单 spec 超过约 10 条测试时按子功能拆分。Git 迁移测试就是实例:tests/regression/git/ 下分列
migration-mysql.spec.ts、migration-postgres.spec.ts、migration-mongo.spec.ts、migration-modal-form.spec.ts、migration-widget-bindings.spec.ts,全部共用migration-setupproject,各自聚焦一个页面。
经验法则:一个 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: true、timeout: 60_000、expect.timeout: 10_000、actionTimeout: 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-setup(fixtures/signup.setup.ts):让配置的用户一定存在,自动处理全新部署。源码显示它
page.goto(ROUTES.signup)后用getByTestId("firstName")(空实例超级用户 onboarding,会经/setup/welcome)与getByPlaceholder("Enter your email")(非空实例的旧版 signup 流)做.or()判别;旧版流中若重定向回/user/signup且error参数匹配/already|sign in instead/i,则视为成功(账号已存在);最后完成 profiling(t--user-proficiency、t--user-use-case单选 + "Get started")并断言到达/applications。 - setup(fixtures/auth.setup.ts):用
USERNAME/PASSWORD走登录页,等待离开/login后page.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.ts 的 loadMigrationState() / 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.ts 的 BASE_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_HOST、GIT_CLONE_URL 等须与目标部署配套;migration 相关变量与 setup 仅在有 Gitea 与对应数据库容器时才可用。本地快速调试亦可参考 test:pw:ci 脚本(PLAYWRIGHT_BASE_URL=http://localhost 直连本机实例)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00