E2E 环设计
E2E 用例 = 页面分片 + 总索引:唯一入口、机器可校验、一例一片、分片键只认页面;testid 应然源 = 用例分片,实然 = 前端源码,按页由机器双向集合校验。
契约正文位于
.atlas/rings/e2e/reference.md+.atlas/skills/atlas-e2e/SKILL.md,本页是设计依据。
一、它解决什么问题
E2E 在真实项目里有三种典型死法:
| 死法 | 表现 | 对策 |
|---|---|---|
| 写不起 | 脚本手写,改一版界面全报废,写与维护都贵到没人做 | 用例即真相 + 脚本派生 |
| 跑不稳 | 选择器脆、环境凑不齐,红的是环境不是产品 | 单段真栈执行 |
| 假绿 | 全绿但什么都没验证——断言初始即恒真、拿 toast 当验证、断言落在被操作控件自己身上 | 零步基线 + 假绿三查(头号敌人) |
前两者靠「用例即真相 + 脚本派生 + 单段真栈」解决;「假绿」是头号敌人——可靠性不能托付给任何人的自觉,只能把「该做的判断」变成流程里必然执行的机器动作。
二、它是一台什么机器
测试不是手写的,是「AC → 分片 → 派生脚本」流水线的产物。工件只有四件:
- 总索引
e2e-index.md(唯一入口 + 状态账本); - 页面分片
cases/<page>.md(每例一片,唯一要人维护的东西); - 派生脚本
scripts/<page>.ts(机器生成、可重生成); - 前端源码里的
data-testid(执行锚点)。
一条需求流过五步:
- 写:域级 PRD 的 AC 给业务预期;按页写分片,每条用例一块
atlas-case——intent(证明什么)+testid:(锚哪些元素,应然清单)+seed:(前置数据)+step/expected(动作行 / 断言行)。 - 审:文本期校验器查形态(结构 / 词表 / 自相矛盾),设计期独立审查查语义(可证伪 / 异源 / AC 可达);两道都过才许生成,缺审查 = 未审,生成被拒。
- 生:生成器按页派生脚本(
goto+ testid 齐全守卫 + 逐步执行),并每页附一条零步基线测试,专抓「初始即真」的恒真断言。 - 跑:真实前端 + 真实后端单段执行,登录态用真实登录预置;结果回写索引状态列。
- 收:首跑红先三分类(mock 语义残留 / 命名漂移 / 真缺陷)再回写;域收口 = 全
green+ 审查无Critical+ 用户验收走查。
九条设计原则(只写为什么)
- 应然先行——预期唯一来自 AC、锚点应然来自分片、实现只当参考基线。从 live app 出发是用实现验证实现:产品错了测试照样绿。
- 名字绑死、机器可校验——用例 ↔ testid ↔ 前端源码按页双向集合相等。凡依赖人肉对齐的必漂;名字是机器能核对的最低成本不变量。
- 判据分层——形态项进文本期门,能执行化的语义进运行期门,执行化不了的进独立审查。把语义塞进文本期 = 没判(regex 判句意的代价实测是 680 次重试 / $11 / 零用例);近似门必须自我声明「不构成证明」。
- 自检流程化,不提示词化——「写用例时注意别空转」类提示被否:提示词层补救 = 把质量托付给自觉。落地 = 生成器每页必产零步基线测试;恒真断言读不出来,只能跑。
- 假绿三查——恒真(初始即成立)/ 瞬时(toast 当验证)/ 同源(断言落在被操作控件自己身上)三族失效,对应刷新复断言、断言真可见、编辑前后两端等整族规则。
- 门也要被审计——判据句、探针、统计都可能单独错,「门报出来的数」不等于事实,须拿与被测对象不同来源的事实对拍。
- 脚本派生,用例是唯一要人维护的真相——手写脚本 + 索引对照会漂,且失去「改用例即重生成」。
- 单段真栈,桩只桩化外部依赖——验证的证明力来自「被测对象 = 交付对象」;mock 层的自证绿无证明力。后端可先返假数据,但不在前端塞 mock 层;登录态用真实登录预置。
- 无门等于无约束,人侧门兜底——机器门管形态与存在性、管不了业务对不对,故域收口配用户验收走查(走查载体 = 评审控制台,见 E2E 用例评审控制台)。
三、两个真相源
| 内容 | 真相源 |
|---|---|
| 用例 | 索引(product/e2e/e2e-index.md)+ 分片(product/e2e/cases/<page>.md) |
| testid | 应然源 = 用例分片的 testid: 应然清单,实然 = 前端源码 data-testid 扫描,执行即对账 |
两者用名字绑死,按页由机器双向集合校验。
依据来源(硬优先级)
| 层 | 来源 | 提供 | 不提供 |
|---|---|---|---|
| ① 业务预期(唯一来源) | 域级 PRD 的 AC | 成功 / 失败 / 边界 / 异常的结果与文案 | — |
| ② 交互与锚点 | product/e2e/cases/*.md 分片 | 页面路由、可交互元素、data-testid 应然 | 业务规则 |
| ③ 参考基线(仅参照) | .trellis/spec/structure/{current,legacy}/ | 现状功能 / 字段 / 接口存在性 | 不得当结论,更不得反推预期 |
三禁令:禁用真实 / 旧实现行为反推预期;锚点应然只来自用例分片;禁无 AC 来源的用例(prd_ref 必填)。
与多数开源方案的根差别:它们从 **URL 或 live app(现状)**出发;Atlas 从 **PRD + 用例分片(应然)**出发——这是「应然先行」的根。
四、组织形态
text
product/e2e/
├── e2e-index.md 总索引(唯一入口;主表 + 反向视图)
├── cases/<page>.md 页面分片(`atlas-case` 块)
└── scripts/<page>.ts E2E 脚本(由分片派生,Node-Playwright 单轨)三条硬规则:① 总索引唯一入口且机器可校验;② 一例一片(归断言落点页,跨页记 pages[] 不复制);③ 分片键只认页面。
- 页面 = 分片文件名(去扩展名即
slug;静态路由 =/+ slug、带参路由取业务段、根页home);「页 ↔ 真实路由」映射的唯一真相源 = 索引页面表。 - 索引主表列:
用例ID | 页面 | 中文标题 | 类型 | 关联AC | 需求 | 状态 | 状态原因 | 分片(末列带锚点链接);另有反向视图页面 ↔ 用例。 - 分片块格式:
## <ID> 标题+```atlas-case行式键值块(stdlib 可解析,非合法 YAML)。 - 字段不两处出现:状态类字段只在索引;
testid只在分片。
五、testid 规范与双向校验
- 唯一硬锚:必要元素(所有可交互元素 + 需断言的展示元素)必须带
data-testid;脚本一律以 testid 定位,不用语义 / 样式 / 位置选择器。 - 命名
<页面slug>-<语义名>[-<变体>];元素段必须语义名,禁样式 / 位置 / 序号名;全站唯一、不复用。 - 校验语义:抽取源 = 前端源码;按页比对「前端
data-testid扫描集合」⇔「分片引用集合」;有分片的页必须相等(多 / 少即FAIL);未实现的页 →SKIP(记「未实现页」清单,不算失败)。 - 面板容器标记:前端里 Tab 级面板的容器必须标
data-atlas-panel,由动作展开的内容面板不得标——漏标 ⇒ 面板内hidden类断言在未展开时恒真 ⇒ 误报。 - 实现期薄桩:
.trellis/spec/conventions/testid.md(paths:= 前端 globs;生产者scripts/gen_testid_stub.py),内含祈使句「实现按分片应然清单命名data-testid,不得自造」+ 两块清单(分片应然 / 前端实然,差集 = 待实现面)。 - 存量开户豁免:存量项目里已存在的不符合 Atlas 命名规范的
data-testid降级为WARN容忍(避免开户即全面阻断);而新分片中新声明的 testid 违规仍严格报FAIL。
双向集合对账门的实现细节见 门禁系统。
六、用例词表与 ID
| 字段 | 取值 |
|---|---|
| 类型 | smoke / acceptance / exception / permission |
| 状态 | red / green / blocked / skipped(E1 起);prototype-pass = E0 历史终态(存量合法、不再产生) |
| 状态原因 | <枚举>:<中文说明>;枚举 ENV_ISSUE / NOT_IMPLEMENTED / SPEC_AMBIGUOUS / BLOCKED_DEP / DEFERRED / OUT_OF_SCOPE / OTHER |
| ID | E2E-<PAGE>-NNN,PAGE 大写,每页独立、三位定长、删除不复用 |
- 分片另有
intent字段:可证伪的一句话声明(这条用例证明什么),不得是「页面显示正确」类模糊表述。 step/expected行式语法:<动词> <testid> [<值>] # 中文说明,词表封闭:- 动作 9:
clickfillselectcheckuncheckpresshoverrefreshgoto - 断言 11:
textcontainscountvisiblehiddenenableddisabledvalue/countOptions/attr/delta(相对值增量断言,应对未读数 / 购物车类累加业务,无需硬编码初始基数) expected行可加initial:/unchanged:豁免前缀;中文降为#注释。
- 动作 9:
七、质量门与校验清单
- 结构门(FAIL):字段齐 / ID 唯一 / 链接(文件 + 锚点)可达 / 无孤儿无死链 / 词表合法 /
prd_ref格式。 - 有意义门:
expected空、占位字样、断言数 <1、skipped无原因 → FAIL;intent缺失 / 模糊、不可判定词、纯跳转步骤、瞬时断言(toast)、持久化未刷新复断言、「或 / 等 / e.g.」式步骤、i18n key / 枚举 / 组件名当文案 → WARN。 - 结果断言硬规则(源真实事故):① 禁瞬时反馈当验证;② 持久化写必须刷新后复断言;③ 断言须与被操作控件异源;④ 编辑 / 切换类断言前后两端;⑤ 文案须为渲染后可见文案。
- 用例结构硬规则:R7 同一用例内对同一实体不得既断言存在又断言消失(一个可判定终态 = 一条用例);R8 断言前必须让目标元素可见(先切页签 / 展开);R10 跨页用例的终态断言必须落在断言落点页上求值;precondition 不得说谎。
- R9 空转禁止 + 零步基线(运行期门):
expected的每条断言在未执行任何step的初始态必须不成立。生成器按页产test_<page>__zero_step_baseline——只goto,逐条求值该页全部用例断言并集,命中即 FAIL 并指名。两条显式豁免:initial:(本就该在初始态成立 ⇒ 排除出基线);unchanged:(「动作不改变它」才是被测属性 ⇒ 双向判定)。 - seed 数据模型:
precondition升为结构化 seed 块(实体 × 字段,幂等 upsert,session 实体统一「已登录」);播种通道 =stack-profilee2e 段声明式;声明 / 发射解耦开关(e2e.seed.mode: enforce | declare)——enforce每条用例独立发射运行期播种,declare只参与静态 models 对账与依赖标注(专为状态累加链项目设计,避免破坏性动作被前置播种强行撤销)。 - 追溯门(跨环,WARN):域内页面全「已实现」的域,其域级 PRD 定义的每个 AC 必须被索引主表 ≥1 条
关联AC指向,零挂靠 ⇒ WARN + 逐域清单。覆盖关系是派生量(真相源 = 主表关联AC+ 域级 AC 定义行),不设手工对账表。 - 网络打桩门(
validate_e2e_mocks):用例分片若声明mock:,必须有对应的 fixture 代码文件存在——杜绝只声明不打桩的「虚假打桩」。 - 判据 / 探针独立对拍:门的三部分(判据句、探针、统计)都可能单独错,须拿与被测对象不同来源的事实对拍。
八、脚本与执行
- 运行器由
product/stack-profile.yaml的e2e.runner决定;一页一脚本,派生可重生成(定制放scripts/_support/)。 baseURL不硬编码:唯一地址 =e2e.app_base_url(真实应用单段);为null⇒ 生成器报「未就绪」(退出码 3 +--json的not_ready),调用方判SKIP,不猜默认。- 就绪探针(区分真红与假红):执行前用标准库 TCP 探针探测靶场服务;未声明地址或端口不通 ⇒ 显式判
SKIP (ENV_ISSUE),防止「服务没起」被误当成整批用例断言FAIL。 - 运行器单轨化:整体退役 python-playwright,单轨定型为 Node-Playwright(
@playwright/test);生成脚本一律为一页一脚本scripts/<page>.ts,通过testMatch: "scripts/*.ts"确定性收集。 - 测试环境运行隔离:全栈 E2E / 后端真实测试与本地 dev server 隔离,后端测试统一采用独立库(如
app_test),杜绝测试过程清空 / 污染前端日常联调数据。 - 运行态排查留底与证据链:E2E 运行失败时保留排查现场,生成结构化证据快照(含终端错误留底与 failure 截屏),杜绝偶发红灯被静默吞掉。
- 断言须真可见:
text/contains/value先断可见再比值(隐藏元素同样有文本,直比 = 假绿)。 - 应用侧登录态(硬):以真实登录换取令牌,按真实前端读取的存储键写进运行器原生 storage;凭据只来自运行期环境变量(不读项目
.env、不硬编码);未设置 ⇒ 跳过预置并明确警告;换取失败 ⇒ 报错中止(静默续跑 = 未登录态跑用例的假绿)。 - 元素齐全守卫 = 自动等待:testid 清单守卫用运行器原生轮询断言等元素出现(真实应用是 SPA,
goto后元素异步渲染,即时存在性判定必红);超时即失败并点名缺失清单。
九、应然 UI 变更的前置与影响面
判据不是需求大小,而是是否改变应然 UI。应然 UI 的载体 = 域级 PRD 的文案 / 流程六件套 + 用例分片的 testid: 应然清单。
- 改 UI → 更新分片
testid:应然清单(新增页在索引页面表加行)为前置; - 不改 UI(规则 / 数据)→ 复用既有 testid;
- 纯后端不可见 → 本环不产用例。
影响面识别归 apply(需求文本 + structure 路由表 + 索引页面表;失败问用户)。
前置人侧门:改变应然 UI 的需求进入本环前,域级 PRD「用户旅程走查」须已获用户确认,未确认不得落笔用例。
十、状态机与完成门
- 状态机:
red → green(E1 起);prototype-pass= E0 历史终态;blocked/skipped必带原因。 - 首跑红三分类协议:首跑红逐条分类——mock 语义残留 → 改用例 / 命名漂移 → 改实现对齐 / 真缺陷 → 走需求级;不分类不得批量回写状态。
- 完成门 = 硬门 + 显式例外:需求相关用例必须在真实应用上全
green且实例 ⑤ 实现代码审查无Critical;做不到的标blocked+ 原因与去向,方可例外收口。 - 域收口人侧门:该域 E2E 真实应用全 green 后,用户在评审控制台逐用例执行 + 人工 review(控制台盖不住的项 =
manual.md手工面)确认后才算域收口;不符 ⇒ 差异登记不收口;真栈不可用 =blocked带原因。走查门不阻塞开发:机器面全绿即开下一域,走查攒批补、过门后归档。
十一、独立审查
- 骨架同构:
.atlas/shared/independent-review.md(五实例同构,各实例只声明自己的四项)。 - 本环四项实例值:fan-out = 页面分片(复用 testid 双向校验的同一分区键)+ 阈值(20 条或 500 行)按用例 ID 区间再切 + 一个全局审查者;回退环 =
prd|e2e;权威输入 = 域级 PRD 的 AC + 该页用例分片 testid 清单(缺则不得给「通过」);门禁 = 阻断脚本生成(判据「报告存在 + 无Critical」,缺失 = 未审)。 - 落点 =
product/e2e/reviews/<日期>-<范围>/(与人工评审账本console/分离,不得混用)。 - 设计依据:子代理审查属设计期语义门(生成后、执行前),不能替代运行期门——零步基线仍独立必需,两者互补。
十二、与其它环的接口
| 环 | 接口 |
|---|---|
atlas prd | AC → 用例上游(prd_ref 回指 AC ID);AC 文案规范 = 文案类断言来源;「用户旅程走查」用户确认 = 前置人侧门 |
atlas structure | structure/ 事实 = 参考基线仅参照;data-models 事实 = seed × models 静态对账对象;路由表 = 影响面识别输入 |
atlas apply | 影响面识别(需求文本 + 路由表 + 索引页面表);apply 在实现前跑,新用例置 red、不回写执行状态 |
