首页
/ Supabase 文档站 E2E 测试指南:Playwright 驱动的范围解析、链接校验与 WCAG 无障碍扫描

Supabase 文档站 E2E 测试指南:Playwright 驱动的范围解析、链接校验与 WCAG 无障碍扫描

2026-09-06 17:13:26作者:董灵辛Dennis

本篇基于仓库中的 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 套件。它的职责有三:

  1. 加载每一个进入范围的页面,确认文章主体(article)能正常渲染;
  2. 校验文章内所有「docs-owned」链接(即指向站点自身 /docs/... 的链接)都能解析成功;
  3. 对每个页面做 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.tspaths.tsaxe.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:docspnpm e2e:docs:allpnpm e2e:docs:a11ypnpm e2e:docs:uipnpm 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 可以看到范围解析的优先级:

  1. 若设置了 DOCS_E2E_PAGE_PATHS 环境变量,则直接按其中的页面列表运行(忽略 git diff);
  2. 否则读取 DOCS_E2E_BASE_REF(默认 origin/master)并收集变更文件,交给 resolveDocsScope 映射成页面路径;
  3. 若结果为空,打印「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.tsuse.baseURL)。官方建议日常检查优先用已部署站点,本地 server 仅用于验证「生产还没有的未发布内容」。

4.1 已部署站点

PLAYWRIGHT_BASE_URL=https://supabase.com pnpm e2e:docs

若目标是受保护的 Vercel 预览站,还需额外设置 VERCEL_AUTOMATION_BYPASS_SECRET。这个变量并非随意透传:playwright.config.ts 会据此给所有请求附加 x-vercel-protection-bypassx-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.tstimeout: 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_PREFIXTROUBLESHOOTING_PREFIXPARTIALS_PREFIX。几个细节值得注意:

  • _ 开头的 .mdx 文件(隐藏内容)会被跳过(isHiddenMdx);
  • troubleshooting 的页面路径用的是文件名而非完整目录结构(slug = basename(filePath, '.mdx')),而 guides 保留完整目录层级;
  • 联邦化章节(federated sections)一律排除,即 graphqldatabase/extensions/wrappersai/pythondeployment/terraformdeployment/ci 这五类(FEDERATED_SECTIONS)。有意思的是,这套清单有「防漂移」机制:assertFederatedSectionsInSync 会读取 apps/docs/scripts/federated-content/sources/*.ts 中声明的 section: 值,与常量做双向比对,多一个或少一个都会直接抛错——从源码结构看,这是为了让「联邦章节清单」在单处修改时立刻暴露,避免范围解析悄悄漏测或多测。

5.2 partial 闭包展开

修改一个被广泛引用的 _partials 片段时,如何找到所有受影响页面?buildPartialIndex 会遍历 _partialsguidestroubleshooting 三棵树,用正则 <$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 列出 guidestroubleshooting 全树的每一页(按指南说法是「数百页」量级);
  • 自动追加 --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.tsparsePagePaths 中:按 [\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 渲染 + 链接解析测试

对每个页面,断言链是:

  1. page.goto(pagePath) 且响应状态成功(response.ok());
  2. 文章主体可见——选择器由页面路径决定(docs-links.ts):
    • guides 页:[data-testid="sb-docs-guide-main-article"]
    • troubleshooting 页:[data-testid="sb-docs-troubleshooting-main-article"]
  3. collectDocsOwnedLinks 收集文章内链接并逐个校验。该函数的过滤规则(docs-links.ts#L22-L56):只取 article a[href];跳过空 href 和锚点;解析后要求协议为 http/https、origin 与站点一致路径以 /docs/docs/ 开头——即只校验「docs 自己拥有」的链接,/dashboard/ui 等非文档路由被跳过;去 hash 去重后按字典序返回。
  4. 每个链接用 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 秒)。流程:

  1. 页面加载失败或状态非 2xx 时,先 attachScanReport(testInfo, unloadedResult(...)) 附上一个 loaded: false 的扫描报告再断言失败——即使页面挂了,报告里也能看到挂在哪;
  2. settleForAxe 等待页面稳定后,scanArticle 只扫描文章主体(include 选择器);
  3. 每次运行都把完整 axe-results.json 作为附件写入报告;
  4. 若文章内元素数低于 20(MIN_MEANINGFUL_ELEMENTS),scanLooksEmpty 判定「扫描可能没等到渲染完成」,会以 warning 注解提示「clean result 证明不了什么」;
  5. 非阻断性问题只注解、不失败;只有阻断性违规才 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_RULESaxe-helpers.ts#L10-L20):color-contrasthtml-has-langhtml-lang-validhtml-xml-lang-mismatchdocument-titlearia-hidden-bodymeta-viewportmeta-refreshcss-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. 调试失败

  1. 运行结束后打开 HTML 报告:

    pnpm -C e2e/docs exec playwright show-report
    
  2. 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 上运行。要点:

  1. 为什么不用 paths 触发过滤:由于该检查是 required,workflow 必须对每个 PR 都产出一个 check run(否则非文档 PR 会永远等不到这个 check)。因此路径过滤被移到「Detect changed paths」步骤里,用 dorny/paths-filter 判断 docs(内容目录、e2e/docse2e/shared、lockfile、workflow 本身)与 docs_app(整个 apps/docs/**)两组过滤器;范围外时后续步骤全部跳过、check 报绿。
  2. 范围解析:checkout 时对 PR 使用全量历史(fetch-depth: 0)+ sparse-checkout(只检出 e2e/docse2e/sharedscriptspatchesapps/docs/content/{guides,troubleshooting,_partials}apps/docs/scripts/federated-content/sources),然后对 base 分支 diff 跑 node --experimental-strip-types e2e/docs/scripts/resolve-docs-scope.ts,输出 skippaths
  3. 等待 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 命令。
  4. 执行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 天)。
  5. 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 的两个已知不可靠点。

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