首页
/ Ghost E2E 测试工作区协作规范:AGENTS.md 中的工作流、校验闭环与 Playwright MCP 定位器发现

Ghost E2E 测试工作区协作规范:AGENTS.md 中的工作流、校验闭环与 Playwright MCP 定位器发现

2026-09-07 12:57:03作者:明树来

Ghost 在仓库根目录维护着多层文档:面向开发者的 docs/contributing/e2e-testing.md 是"写浏览器端到端测试的规范本体",而 e2e/AGENTS.md 则是面向在 e2e/ 工作区里"动手的人与 AI 代理"的操作入口——它回答三个问题:改代码前读什么、改完必须跑什么、以及如何借助 Playwright MCP 高效且稳定地发现 UI 定位器。本文以该文件为骨架,结合其背后引用的三份"权威文档"(写作指南E2E 工作区 README数据工厂 README)及工作区内的真实配置与源码,把这条"进入 E2E 工作区的正确路径"讲透:读完你将掌握 Ghost E2E 工作区的分层文档体系、改动前后必须执行的校验命令链,以及一套可复现的、从"运行测试拿到实例 URL"到"确认稳定定位器"的完整实操流程。

为什么需要一份 AGENTS.md:在进入工作区前先读权威文档

e2e/AGENTS.md 全文极短,但它传达了一个明确工程原则:任何 E2E 改动都应建立在阅读"人工维护的规范文档"之上,而不是靠工具自身的提示或零散记忆。文件开篇就是硬性要求:

Read the canonical human documentation before changing this workspace

也就是说,这份文件是一份"门禁索引",它把散落在仓库中的规范收敛到三个唯一事实来源(Single Source of Truth):

想要了解的内容 权威文档(相对仓库根目录) 覆盖范围
测试结构、Page Object、定位器优先级、等待与断言方式 docs/contributing/e2e-testing.md 写测试本身的约定,被称为 canonical writing guide
基础设施模式、fixtures、隔离策略、命令与排障 e2e/README.md 本地工作区、Docker 基础设施、运行与调试
测试数据工厂与持久化适配器 e2e/data-factory/README.md 测试数据构造 helpers 的用法与扩展方式

这种分层有一个刻意为之的目的:文档的"章节职责"被拆开,且 e2e/README.md 与写作指南之间通过链接互相引用(例如 README 中写明"测试结构与约定见写作指南,本 README 只覆盖本地工作区、基础设施、fixtures 与命令")。AGENTS.md 则充当两者的总入口,避免协作者(尤其是 AI Agent)在错误的位置寻找信息。

此外,e2e/AGENTS.md 所在目录结构恰好对应了这些文档的实体位置,便于对照阅读:

e2e/
├── AGENTS.md                 # 本文件:工作区协作入口与工作流要求
├── README.md                 # 运行模式、隔离、命令、排障
├── data-factory/README.md    # 数据工厂说明
├── docs(仓库根)/contributing/e2e-testing.md   # 写测试的规范本体
├── tests/                    # 用例(public / admin / portal 等)
├── helpers/                  # playwright / pages / environment / services / utils
├── playwright.config.mjs
├── eslint.config.js          # 含文件名 kebab-case 等强约束
├── package.json
└── tsconfig.json             # @/ 路径别名定义处

硬性工作流:pnpm、@/ 别名与改动后的必跑校验

e2e/AGENTS.md 的第二部分 "Required workflow" 规定了三条任何人都不能绕过的规则,它们都在仓库配置中可被验证。

1. 只用 pnpm,绝不使用 npm 或 Yarn

Ghost 是一个 pnpm workspace 仓库(见根目录 pnpm-workspace.yaml),e2e 自身是名为 @tryghost/e2e 的私有包(见 e2e/package.json)。在 E2E 工作区中运行任意命令都应从 e2e/ 目录发起,或通过 pnpm --filter @tryghost/e2e 定位到该包。

2. 通过 @/ 别名导入共享测试 helper

写作指南与 lint 配置共同强制了这一约定。@/ 别名定义在 e2e/tsconfig.json

"paths": {
  "@/admin-pages": ["./helpers/pages/admin/index"],
  "@/public-pages": ["./helpers/pages/public/index"],
  "@/portal-pages": ["./helpers/pages/portal/index"],
  "@/helpers/*": ["./helpers/*"],
  "@/data-factory": ["./data-factory/index.ts"],
  "@/data-factory/*": ["./data-factory/*"]
}

对应的导入形态(写作指南原文示例):

import {expect, test} from '@/helpers/playwright';
import {LoginPage, PostsPage} from '@/admin-pages';
import {createPostFactory} from '@/data-factory';
import {usePerTestIsolation} from '@/helpers/playwright/isolation';

这些别名有双重保障:一方面 e2e/eslint.config.js 通过 no-relative-import-paths/no-relative-import-paths 规则(允许同目录相对导入、其余统一使用 @ 前缀)在风格上阻止散乱相对路径;另一方面 no-restricted-imports 又限制 @/helpers/pages/* 这类深层导入只出现在页面对象内部,从而保证 Page Object 的封装不被破坏。

3. 改动后必跑的校验闭环

e2e/AGENTS.md 明确给出改动后的动作组合,这也是被 package.json scripts 支撑的最小回归集合:

# 从 e2e/ 工作区运行
pnpm test tests/admin/signin.test.ts   # 运行被改动影响的聚焦用例
pnpm lint                             # eslint . --cache
pnpm test:types                       # tsc --noEmit && tsc -p tsconfig.scripts.json
  • 改完测试:跑 focused test + pnpm lint + pnpm test:types
  • 改完数据工厂:除了上面三项,还必须追加 pnpm build(在 package.json 中它被定义为 pnpm test:types,见 e2e/package.json)。

为什么 lint 与类型检查如此重要?因为写作指南里的很多约定——而不是全部——是"可执行"的约定。例如文件名 kebab-case 规则 ghost/filenames/match-regex 的正则为 ^[a-z0-9.-]+$ 且为 error 级别(见 e2e/eslint.config.js),FeaturePage.ts 这类 PascalCase 文件名会被直接判错,从而保证"测试文件按行为命名、页面对象按 <feature>-page.ts 命名"的规范有机器兜底。测试目录(tests/**/*.ts)还被额外约束:禁止使用 page.locator()(必须用 Page Object 或更高层方法)、禁止 test.describe.parallel()/serial()(前者被要求改用 usePerTestIsolation())、并启用本地自研规则 local/no-unsafe-reset-environment 来保护隔离逃生舱的正确用法(详见 e2e/eslint.config.js)。

4. 规范变更要回流到文档,而不是制造"工具专属副本"

最后一条工作流规则颇具 Agent 时代的前瞻性:

Update the canonical human guide when a shared E2E convention changes. Do not create or rely on tool-specific copies of the guidance.

它要求:一旦共享 E2E 约定发生变化,改动应回到唯一规范文档(canonical human guide)里,而不允许创建"某个 AI 工具专属的规则副本"并在后续依赖它。这正是 AGENTS.md 自身保持极短、只做索引与指针的原因——规范本体始终只有一份,避免多个副本漂移失配。

发现定位器的标准姿势:Playwright MCP 工作流

e2e/AGENTS.md 用近半篇幅描述了"何时以及如何用 Playwright MCP 发现定位器、构建 Page Object"。这是对 AI 代理最实用的一段,完整流程可拆成四个步骤。

前置条件与适用场景

When discovering selectors or building a Page Object, use Playwright MCP when it is available.

MCP(Model Context Protocol)服务器在这里扮演"浏览器控制接口":当代理需要为一段陌生 UI 挑选稳定定位器时,与其凭空猜测 DOM,不如让 MCP 直接连上真实运行中的测试实例。

步骤一:用 PRESERVE_ENV=true 保留环境并拿到实例 URL

e2e/ 目录下以保留环境的方式运行一个聚焦测试:

cd e2e
PRESERVE_ENV=true pnpm test

正常情况下每次测试结束后环境会被回收(global teardown 会清理 e2e 容器与测试数据库),而 PRESERVE_ENV=true 会保留容器与数据库,测试运行器随后会打印出该 Ghost 实例的地址——写作指南给出的典型值是 http://localhost:2369(见 docs/contributing/e2e-testing.md "Preserve the test environment")。基础设施(MySQL、Redis、Mailpit、Tinybird)必须已在运行,可使用 pnpm devpnpm --filter @tryghost/e2e infra:up 先行启动(见 e2e/README.md)。

步骤二:导航到实例并拍摄"无障碍快照"

打开打印出的实例 URL,在交互前先取 accessibility snapshot。这一步非常关键:它迫使你基于可访问性语义而非视觉像素来选择定位器,与项目"优先语义定位器"的定位器优先级天然对齐(详见下文)。

步骤三:实际演练交互,验证定位器并截图

对目标 UI 执行真实交互(点击、输入等)以验证候选定位器确实命中了期望元素;当渲染状态对后续判断有参考价值时再截图留存。换句话说,定位器必须以"真实交互通过"为证据,而不是只在静态 DOM 里存在。

步骤四:按定位器优先级重写,不照抄生成结果

这是 MCP 工作流中最容易被忽略、却写在 AGENTS.md 里的一句话:

Follow the locator priority in the E2E writing guide; do not copy generated selectors without checking that they are stable.

生成的选择器可能是脆弱的结构表达式,必须经过稳定性审查并按写作指南的优先级重写,自高向低为:

  1. ARIA 角色 + 可访问名称page.getByRole('button', {name: 'Save'})
  2. 表单标签page.getByLabel('Name')
  3. 唯一可见文本page.getByText('Saved')
  4. 稳定测试 ID:仅在无语义定位器可用时使用
    • Ember Admin 常用 data-test-*,React Admin apps 用 data-testid
  5. 稳定的结构选择器:仅在万不得已时使用

这条优先级的完整示例代码与解释在 docs/contributing/e2e-testing.md,页面对象中推荐按固定次序为元素声明只读 Locator,例如:

// e2e/helpers/pages/admin/feature-page.ts(示意结构)
export class FeaturePage extends AdminPage {
    readonly saveButton = page.getByRole('button', {name: 'Save'});
    readonly nameInput = page.getByLabel('Name');
    readonly statusMessage = page.getByText('Saved');
}

如果 UI 上确实没有可靠的语义锚点,正确做法是回到产品代码里加一个稳定的测试 ID,而不是让测试去耦合样式或 DOM 位置——这正是 Ghost 工作区中测试与产品代码相互演进的典型形态。

备选方案:Playwright Inspector 与浏览器开发者工具

AGENTS.md 明确留了退路:

If Playwright MCP is unavailable, use Playwright Inspector or browser developer tools as described in the writing guide.

两种途径的目标一致:打开被保留的实例、观察可访问性树与相关属性、在优先级指导下选定定位器并验证交互。区别仅在于"控制浏览器的接口"不同,规范本身不随之分叉。

工作流背后的规范底座:写作指南核心约定

e2e/AGENTS.md 反复指向的 docs/contributing/e2e-testing.md 是所有这些工作流的"语法层"。其中与定位器发现最直接相关的约定包括:

  • 等待靠状态而非计时:用 await element.waitFor({state: 'visible'}) 与 web assertion(await expect(...).toContainText(...));禁止 page.waitForTimeout(5000)page.waitForLoadState('networkidle')
  • 异步操作等"用户会看到的 UI 信号":例如保存后等待状态消息出现,而不是固定延时。
  • iframe 用 frameLocator():它像其他定位器一样自动重试,例如 page.frameLocator('[data-testid="portal-popup-frame"]')
  • 页面对象内方法返回 locator 或值、但不在其中断言:断言留在测试用例里;Modal 被建模为普通类而非页面子类,把定位器作用域收敛到 getByRole('dialog') 上。
  • Arrange–Act–Assert 作为可读性启发:搭建场景 → 执行被测行为 → 验证结果,用命名与结构让三阶段自明。

这些约定直接支撑了 MCP 工作流中"取无障碍快照、按优先级选 locator、做交互验证"三步的产出质量。

从用例到数据工厂:改动边界的两种情形

e2e/AGENTS.md 把"改测试"与"改数据工厂"作为两条不同的校验路径对待,是因为它们处于不同的抽象层:

  • 测试用例通过 Page Object 与 @/ 别名使用 helpers,通常只需要"聚焦用例 + lint + 类型检查"即可完成回归。
  • 数据工厂(e2e/data-factory)是测试数据的构造与持久化层,其代码会以 @tryghost/e2e 的构建产物被引用,因此改动后必须追加 pnpm build

数据工厂本身采用"工厂类 + 持久化适配器"的分层:实体形状与随机默认值归属 @tryghost/test-data 包,工厂在 build() 中把"响应形状"转换为"写入载荷"(扁平化关联、丢弃仅响应字段),entityType 属性驱动持久化(见 e2e/data-factory/README.md)。在测试中的典型用法:

import {createPostFactory} from '@/data-factory';

test('...', async ({page}) => {
    const postFactory = createPostFactory(page.request);
    const publishedPost = await postFactory.create({
        title: 'My Published Post',
        status: 'published'
    });
});

隔离模型与运行模式:理解 PRESERVE_ENV 之前必知的环境语义

MCP 工作流第一步 PRESERVE_ENV=true 之所以能"保留一个可检查的实例",背后是一套精心设计的隔离模型。理解它有助于避免在错误的环境身份上做定位器验证:

  • 默认按文件隔离(per-file):每个文件一次 Ghost 环境周期。global setup 先创建基础数据库、启动 Ghost、等待健康并快照数据库;文件边界处从快照克隆新库并重启 Ghost 复用。
  • 按测试隔离(per-test):usePerTestIsolation()(定义于 e2e/helpers/playwright/isolation.ts)用两个标准 Playwright 调用完成——test.describe.configure({mode: 'parallel'})test.use({isolation: 'per-test'}),为每个测试分配独立 Ghost 实例。fullyParallel: true 会强制按测试隔离。
  • 环境身份(identity)参与因素:fixture option 中 configlabs 参与 per-file 复用身份,二者任一变化都会在文件内触发环境回收重建;而 stripeEnabled 不参与复用,总是强制 per-test 隔离(因为 Ghost 必须针对每个测试的 fake Stripe 服务器启动)。
  • 逃生舱 resetEnvironment() 只能在 beforeEach 钩子中、且必须在解析 baseURLpagepageWithAuthenticatedUserghostAccountOwner 等有状态 fixture 之前调用;ESLint 自研规则会拦截明显误用,fixture 内的运行时守卫是最终硬校验。

运行模式方面,若未显式设置 GHOST_E2E_MODE,脚本会自动选择:本机 admin dev server 在 http://127.0.0.1:5174 可达则走 dev 模式(Ghost 挂载源码并把资源代理给宿主 dev server),否则走 build 模式(使用预构建镜像,资源从 /content/files 提供);也可用 GHOST_E2E_MODE=dev / GHOST_E2E_MODE=build 强制指定。这些语义在 e2e/README.md 中有完整展开——PRESERVE_ENV=true 之后你要检查的,正是某个处于确定隔离身份与运行模式下的真实 Ghost 实例。

结语:一份让"人与 AI"共用同一套共识的入口文档

e2e/AGENTS.md 的价值不在篇幅,而在刻意保持精简的指针式设计:它把所有规范收敛到三份人工权威文档,把工作流压缩为"先读文档 → 只用 pnpm/@/ 别名 → 按边界跑全校验 → 约定变更回流文档",再把最容易让 AI 代理出错的选择器发现环节固化成"保留环境 → 无障碍快照 → 交互验证 → 按优先级重写"四步。配合 eslint.config.js 中可执行的命名、导入与隔离规则,Ghost 得以让编辑器、CI 与 AI 代理在同一套约定下协作:规范只有一份,谁来执行都一样。若你的改动触及任何共享约定,请记住最后那条回写规则——把变化送回 canonical guide,而不要在别处另起炉灶。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388