Supabase 文档站 E2E 测试指南:Playwright 驱动的范围解析、链接校验与 WCAG 无障碍扫描
本篇基于仓库中的 Docs E2E 指南,讲解 Supabase monorepo 中针对文档站(apps/docs)的端到端测试套件:如何用 Playwright 只对「本次改动影响的页面」做渲染与链接校验、如何通过环境变量选择测试目标(生产、Vercel 预览、本地 dev server)、如何覆盖默认的 git 变更范围,以及 @a11y 无障碍扫描的 WCAG 覆盖与跳过规则。读完本文,你可以独立在本地运行、调试该套件,并理解 CI 中「Docs E2E」这一必需状态检查的完整工作流。
1. 这套 E2E 测试是干什么的
当你在 apps/docs/content 下修改了指南(guides)、故障排查条目(troubleshooting entries)或共享片段(partials)时,就应运行这套 Playwright 套件。它的职责有三:
- 加载每一个进入范围的页面,确认文章主体(article)能正常渲染;
- 校验文章内所有「docs-owned」链接(即指向站点自身
/docs/...的链接)都能解析成功; - 对每个页面做 WCAG 2.1 A/AA 无障碍扫描(限定在文章主体范围内)。
套件本身位于 e2e/docs 目录,核心文件包括测试用例 features/docs-pages.spec.ts、范围解析 utils/resolve-docs-scope.ts、无障碍工具 utils/axe-helpers.ts、链接收集工具 utils/docs-links.ts,以及入口脚本 scripts/run-e2e-docs.ts。
2. 环境准备
只需一次性安装 Playwright 的 Chromium 浏览器:
cd e2e/docs
pnpm exec playwright install chromium
e2e/docs/package.json 声明的依赖很精简:@playwright/test(^1.59.1)、@axe-core/playwright(^4.12.1)和 axe-core(^4.12.1),外加 workspace 内的共享包 e2e-shared(即 e2e/shared,提供 run-suite.ts、paths.ts、axe.ts 等公共逻辑)。
注意 package.json 中脚本的启动方式:
"e2e:docs": "node --experimental-strip-types scripts/run-e2e-docs.ts",
"e2e:docs:all": "node --experimental-strip-types scripts/run-e2e-docs.ts --all",
"e2e:docs:a11y": "node --experimental-strip-types scripts/run-e2e-docs.ts --grep @a11y",
"e2e:ui": "node --experimental-strip-types scripts/run-e2e-docs.ts --ui",
"e2e:docs:local-smoke": "playwright test --config=playwright.local-smoke.config.ts",
"resolve-docs-scope": "node --experimental-strip-types scripts/resolve-docs-scope.ts"
入口并非直接跑 playwright test,而是先经过 Node 原生类型剥离(--experimental-strip-types)执行 scripts/run-e2e-docs.ts,由它完成「页面范围解析 → 目标站可达性预检 → 再拉起 Playwright」的编排(实现在 e2e/shared/run-suite.ts)。根目录 package.json 也暴露了转发脚本:pnpm e2e:docs、pnpm e2e:docs:all、pnpm e2e:docs:a11y、pnpm e2e:docs:ui、pnpm e2e:docs:local-smoke,等价于 pnpm --prefix e2e/docs run ...。
3. 运行测试:默认按 git 变更自动选页
默认情况下,pnpm e2e:docs 只测「你当前改动影响的页面」:
- 变更来源 = 相对
origin/master的提交(origin/master...HEAD),加上已暂存与未暂存的工作区文件; - 如果解析出的范围内没有任何页面,命令会直接成功退出,不会启动 Playwright。
标准跑法(从仓库根目录出发,指向已部署的文档站):
PLAYWRIGHT_BASE_URL=https://supabase.com pnpm e2e:docs
也可以打开 Playwright 的 UI 模式观察同一范围的运行:
PLAYWRIGHT_BASE_URL=https://supabase.com pnpm e2e:docs:ui
在 e2e/docs 目录内执行 pnpm run e2e:docs 效果相同。
从 e2e/shared/run-suite.ts 可以看到范围解析的优先级:
- 若设置了
DOCS_E2E_PAGE_PATHS环境变量,则直接按其中的页面列表运行(忽略 git diff); - 否则读取
DOCS_E2E_BASE_REF(默认origin/master)并收集变更文件,交给resolveDocsScope映射成页面路径; - 若结果为空,打印「No in-scope docs pages changed ... Skipping Playwright.」并
process.exit(0)。
解析完成后,脚本还会对 PLAYWRIGHT_BASE_URL 做一次 3 秒超时的 fetch 可达性预检;不可达时若未设置该变量,会提示你用 pnpm dev:docs(仓库根脚本,即 turbo run dev --filter=docs --parallel)启动本地服务,或指向已部署站点。
4. 选择目标 URL
测试通过 PLAYWRIGHT_BASE_URL 决定测哪个站;未设置时默认指向本地文档 dev server http://localhost:3001(见 playwright.config.ts 中 use.baseURL)。官方建议日常检查优先用已部署站点,本地 server 仅用于验证「生产还没有的未发布内容」。
4.1 已部署站点
PLAYWRIGHT_BASE_URL=https://supabase.com pnpm e2e:docs
若目标是受保护的 Vercel 预览站,还需额外设置 VERCEL_AUTOMATION_BYPASS_SECRET。这个变量并非随意透传:playwright.config.ts 会据此给所有请求附加 x-vercel-protection-bypass 与 x-vercel-set-bypass-cookie 两个请求头,从而绕过 Vercel 的部署保护。
4.2 本地文档 server
# 终端 1:启动本地 docs(需要完整 monorepo 安装,部分内容还需要凭据)
pnpm dev:docs
# 终端 2:不设置 PLAYWRIGHT_BASE_URL,或显式设为 http://localhost:3001
pnpm e2e:docs
本地运行有两个已知的可靠性问题,指南明确建议这两类页面改用已部署站点:
- Reference 页面编译慢:本地 dev 模式下
/docs/reference/*页面首次请求编译可能超过 1 分钟,会超过套件的单测试超时(60 秒,见 playwright.config.ts 的timeout: 60_000); - server-side 认证指南本地 404:
/docs/guides/auth/server-side/*存在已知的「仅本地」路由问题,页面在生产环境正常,但本地会 404。
因此,凡是文章链接指向 /docs/reference/* 或 /docs/guides/auth/server-side/* 的页面,请优先对已部署站点运行。
5. 页面范围如何从变更文件解析出来
这是整套套件最核心、也最容易被忽视的机制,实现全部在 e2e/docs/utils/resolve-docs-scope.ts。
5.1 在范围内的路径
| 变更路径 | 行为 |
|---|---|
apps/docs/content/guides/**/*.mdx |
测试 /docs/guides/<slug>(排除联邦化章节) |
apps/docs/content/troubleshooting/**/*.mdx |
测试 /docs/guides/troubleshooting/<slug> |
apps/docs/content/_partials/** |
测试所有引用了该 partial 的自有页面 |
对应源码中的三个前缀常量(resolve-docs-scope.ts#L16-L20):GUIDES_PREFIX、TROUBLESHOOTING_PREFIX、PARTIALS_PREFIX。几个细节值得注意:
- 以
_开头的.mdx文件(隐藏内容)会被跳过(isHiddenMdx); - troubleshooting 的页面路径用的是文件名而非完整目录结构(
slug = basename(filePath, '.mdx')),而 guides 保留完整目录层级; - 联邦化章节(federated sections)一律排除,即
graphql、database/extensions/wrappers、ai/python、deployment/terraform、deployment/ci这五类(FEDERATED_SECTIONS)。有意思的是,这套清单有「防漂移」机制:assertFederatedSectionsInSync会读取apps/docs/scripts/federated-content/sources/*.ts中声明的section:值,与常量做双向比对,多一个或少一个都会直接抛错——从源码结构看,这是为了让「联邦章节清单」在单处修改时立刻暴露,避免范围解析悄悄漏测或多测。
5.2 partial 闭包展开
修改一个被广泛引用的 _partials 片段时,如何找到所有受影响页面?buildPartialIndex 会遍历 _partials、guides、troubleshooting 三棵树,用正则 <$Partial ... path="..."(PARTIAL_PATH_RE)提取每个 .mdx 文件内的 partial 引用,建立两个索引:
includedByPartials:某个 partial 又被哪些上层 partial 引用(用于闭包展开);pagePartials:每个页面直接引用了哪些 partial。
随后 expandPartialClosure 用 DFS 把「改动的 partial → 所有引用它的上层 partial」一路展开,pagesUsingPartials 再找出引用闭包中任一 partial 的页面。换句话说:改了最底层的公共片段,最终测的是「所有直接或间接包含它的所有页面」。
5.3 20 页上限与全量运行
解析出的范围默认截断为 20 页(MAX_SCOPED_PAGES = 20),避免一个被极广泛共享的 partial 把运行时间炸掉;超过上限时按排序只取前 20 页,其余静默丢弃。要测更多页面,不要调高上限,而是用全量命令:
PLAYWRIGHT_BASE_URL=https://supabase.com pnpm e2e:docs:all
e2e:docs:all(即入口脚本带 --all 参数)会:
- 忽略
DOCS_E2E_PAGE_PATHS与 20 页上限,调用resolveAllDocsPages列出guides与troubleshooting全树的每一页(按指南说法是「数百页」量级); - 自动追加
--max-failures=0,避免被 playwright.config.ts 中全局的maxFailures: 3提前截断(若你已显式传了-x/--max-failures则不覆盖,见 run-suite.ts#L77-L81); - 由于默认单 worker(
workers: 1),全量跑非常慢,可并行化,例如:
PLAYWRIGHT_BASE_URL=https://supabase.com pnpm e2e:docs:all -- --workers=4
指南同样强调:全量检查请对已部署站点跑,不要对本地 dev server 跑(原因见 4.2 节)。
想在不启动 Playwright 的情况下预览解析结果,可以用同一套默认范围(origin/master 以来的提交 + 工作区变更):
{
git diff --name-only --diff-filter=ACMR origin/master...HEAD
git diff --name-only --diff-filter=ACMR
git diff --name-only --diff-filter=ACMR --cached
} | pnpm -C e2e/docs resolve-docs-scope
6. 覆盖要测哪些页面
保持 DOCS_E2E_PAGE_PATHS 不设置即可沿用默认的「变更文件」范围。要指定具体页面:
DOCS_E2E_PAGE_PATHS=/docs/guides/getting-started/quickstarts/nextjs \
PLAYWRIGHT_BASE_URL=https://supabase.com pnpm e2e:docs
要与别的基准分支比较:
DOCS_E2E_BASE_REF=origin/develop \
PLAYWRIGHT_BASE_URL=https://supabase.com pnpm e2e:docs
DOCS_E2E_PAGE_PATHS 接受逗号或换行分隔的 /docs/... 路径列表。解析逻辑在 e2e/shared/paths.ts 的 parsePagePaths 中:按 [\n,] 切分、去空白,且不以 / 开头的项会自动补前缀。
7. 每个页面测试做了什么
features/docs-pages.spec.ts 对解析出的每个 pagePath 动态生成两类测试(并通过 test.describe.configure({ mode: 'parallel' }) 保证文件内测试可以分发给多 worker)。文件开头还有一条「护栏」测试:
test('resolved page list must not be empty', () => {
expect(pagePaths.length, 'No pages to test. ...').toBeGreaterThan(0)
})
防止「范围解析为空却误以为通过」的情况。
7.1 渲染 + 链接解析测试
对每个页面,断言链是:
page.goto(pagePath)且响应状态成功(response.ok());- 文章主体可见——选择器由页面路径决定(docs-links.ts):
- guides 页:
[data-testid="sb-docs-guide-main-article"] - troubleshooting 页:
[data-testid="sb-docs-troubleshooting-main-article"]
- guides 页:
- 用
collectDocsOwnedLinks收集文章内链接并逐个校验。该函数的过滤规则(docs-links.ts#L22-L56):只取article a[href];跳过空 href 和锚点;解析后要求协议为 http/https、origin 与站点一致、路径以/docs或/docs/开头——即只校验「docs 自己拥有」的链接,/dashboard、/ui等非文档路由被跳过;去 hash 去重后按字典序返回。 - 每个链接用
page.request.get发 GET 并断言soft成功(soft assertion 让所有链接都跑完,报告全部失败项)。一个实战细节:请求会附带一个「去掉HeadlessChrome字样」的 User-Agent(browserLikeUserAgent),因为 Vercel 的 bot 防护会在部分路由上拦截 HeadlessChrome UA。
7.2 无障碍测试(@a11y)
同文件后半段对每个页面生成 @a11y 标签测试,test.setTimeout(120_000) 把单条上限提到 120 秒(默认是 60 秒)。流程:
- 页面加载失败或状态非 2xx 时,先
attachScanReport(testInfo, unloadedResult(...))附上一个loaded: false的扫描报告再断言失败——即使页面挂了,报告里也能看到挂在哪; settleForAxe等待页面稳定后,scanArticle只扫描文章主体(include选择器);- 每次运行都把完整
axe-results.json作为附件写入报告; - 若文章内元素数低于 20(
MIN_MEANINGFUL_ELEMENTS),scanLooksEmpty判定「扫描可能没等到渲染完成」,会以 warning 注解提示「clean result 证明不了什么」; - 非阻断性问题只注解、不失败;只有阻断性违规才
expect(...).toEqual([])。
8. 无障碍扫描的覆盖与跳过规则
单独跑无障碍子集:
PLAYWRIGHT_BASE_URL=https://supabase.com pnpm e2e:docs:a11y
(即入口脚本加 --grep @a11y。)
几个关键事实,均来自 utils/axe-helpers.ts:
- 扫描标签为 WCAG 2.1 A/AA:
WCAG_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']; - 默认阻断规则只有两条:
ENFORCED_RULES = ['heading-order', 'page-has-heading-one'](axe-helpers.ts#L8)。scanArticle会做两次scan——一次按 WCAG 标签全量收集(用于注解/报告),一次只跑强制规则——所以测试失败信息里报的「blocking」默认只有这两条; - 设置环境变量
A11Y_ENFORCE_ALL=1后(shouldEnforceAll),所有扫出的违规都会变成阻断项; - 被跳过的规则在
EXCLUDED_RULES(axe-helpers.ts#L10-L20):color-contrast、html-has-lang、html-lang-valid、html-xml-lang-mismatch、document-title、aria-hidden-body、meta-viewport、meta-refresh、css-orientation-lock。跳过理由:color-contrast占扫描耗时大头,且文章内的对比度来自共享 token 与页面外壳(chrome),文章级扫描扫不到;其余规则作用于<html>/<head>/<body>,同样超出文章作用域; - 跨域 iframe 会被跳过,第三方嵌入不会被算作本站违规。
明确不在覆盖范围的:/docs/reference/*、共享外壳(chrome)、以及大部分 WCAG 要求——键盘导航、焦点管理、读屏器行为都需要人工测试。
另外注意一个指南强调的「范围 vs 内容」差异:哪些页面被扫描 由你分支的变更决定,但页面内容 来自 PLAYWRIGHT_BASE_URL 指向的站点。生产环境不会有你的新改动,新建页面会 404——所以扫描自己的内容时,应把 PLAYWRIGHT_BASE_URL 指向该 PR 的预览站,而不是生产。
9. Playwright 关键配置速览
e2e/docs/playwright.config.ts 中值得了解的参数:
| 参数 | 值 | 说明 |
|---|---|---|
timeout |
60s | 单测试超时;本地 dev 下 reference 页编译会超过它 |
expect.timeout |
15s | 断言等待上限 |
retries |
CI 下 2 次,本地 0 | retries: IS_CI ? 2 : 0 |
maxFailures |
3 | 全局失败上限;--all 时由脚本改写成 0 |
fullyParallel / workers |
false / 1 |
默认串行单 worker,全量跑时可用 --workers 提速 |
browserName |
chromium(headless) | 与安装命令对应 |
navigationTimeout |
30s | 导航超时 |
screenshot / trace |
only-on-failure / retain-on-failure | 失败时留证 |
reporter |
CI:list + html;本地额外 json | HTML 报告在 ./playwright-report,JSON 落 ./test-results/test-results.json |
outputDir |
./test-results |
失败产物(trace、截图)落这里 |
10. 调试失败
-
运行结束后打开 HTML 报告:
pnpm -C e2e/docs exec playwright show-report -
到
test-results/下查看失败运行的 trace 与截图(配置为retain-on-failure,只有失败才保留)。对@a11y测试,还可以在报告附件中下载该页的axe-results.json,里面有完整的违规明细、元素计数与排除规则列表。
11. CI 如何调用这套套件
工作流 .github/workflows/docs-e2e.yml 定义了「Docs E2E」这一 master 上的必需状态检查,在触及自有文档内容、partials 或 e2e/docs 的 pull request 上运行。要点:
- 为什么不用
paths触发过滤:由于该检查是 required,workflow 必须对每个 PR 都产出一个 check run(否则非文档 PR 会永远等不到这个 check)。因此路径过滤被移到「Detect changed paths」步骤里,用dorny/paths-filter判断docs(内容目录、e2e/docs、e2e/shared、lockfile、workflow 本身)与docs_app(整个apps/docs/**)两组过滤器;范围外时后续步骤全部跳过、check 报绿。 - 范围解析:checkout 时对 PR 使用全量历史(
fetch-depth: 0)+ sparse-checkout(只检出e2e/docs、e2e/shared、scripts、patches、apps/docs/content/{guides,troubleshooting,_partials}、apps/docs/scripts/federated-content/sources),然后对 base 分支 diff 跑node --experimental-strip-types e2e/docs/scripts/resolve-docs-scope.ts,输出skip与paths。 - 等待 Vercel 预览:仅当
apps/docs有变更时等待预览。由于 Vercel GitHub App 自 2026-02-17 起不再写 Deployment 对象,workflow 改为轮询「Vercel – docs」commit status 再用 Vercel API 反查预览 URL(scripts/waitForVercelPreview.js)。解析不到预览就跳过,而不会退回测生产——因为生产没有本 PR 新增的页面,测生产会让合法变更挂在必需检查上;步骤摘要中还会给出维护者手动补跑的gh workflow run命令。 - 执行:
pnpm install --frozen-lockfile --filter=e2e-docs...→pnpm -C e2e/docs exec playwright install chromium --with-deps --only-shell→ 在e2e/docs下以PLAYWRIGHT_BASE_URL(预览 URL)、DOCS_E2E_PAGE_PATHS(解析出的页面列表)、VERCEL_AUTOMATION_BYPASS_SECRET(预览需要时)运行pnpm run e2e:docs;失败时上传playwright-report/与test-results/工件(保留 7 天)。 - Draft 与手动触发:Draft PR 在被标记 ready for review 前保持跳过(job 级
if判断draft == false)。workflow_dispatch手动运行必须提供page_paths输入,可选base_url输入默认指向生产。
12. 命令与环境变量速查
| 命令 / 变量 | 作用 |
|---|---|
pnpm e2e:docs |
按默认 git 范围运行(生产/预览/本地) |
pnpm e2e:docs:all |
全量运行,忽略 20 页上限,--max-failures=0 |
pnpm e2e:docs:a11y |
只跑 @a11y 无障碍测试 |
pnpm e2e:docs:ui |
Playwright UI 模式 |
pnpm -C e2e/docs resolve-docs-scope |
只打印解析出的页面列表 |
PLAYWRIGHT_BASE_URL |
目标站点,默认 http://localhost:3001 |
DOCS_E2E_PAGE_PATHS |
显式页面列表(逗号或换行分隔),覆盖 git 范围 |
DOCS_E2E_BASE_REF |
变更比较的基准 ref,默认 origin/master |
VERCEL_AUTOMATION_BYPASS_SECRET |
绕过受保护 Vercel 预览的部署保护 |
A11Y_ENFORCE_ALL |
设为真值后所有 WCAG 违规都变成阻断项 |
CI |
为真时启用 2 次重试、forbidOnly 与 CI 报告器 |
最后重申适用前提:该套件与 e2e/docs/README.md 描述的命令、范围规则、上限行为均以当前仓库状态为准;本地运行前需完整 monorepo 安装(pnpm install)与 pnpm exec playwright install chromium,而面向 /docs/reference/* 与 server-side 认证指南的页面,请以已部署站点为测试目标,避开本地 dev server 的两个已知不可靠点。
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 StartedRust0624
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