Files
supabase/e2e/docs/scripts/run-e2e-docs.ts
Miranda Limonczenko 52cb1c2600 feat(docs) Dynamically E2E test all docs-owned content (#48320)
Closes DOCS-1203

## Problem

The docs E2E workflow only ever tested one hardcoded page: the Next.js
quickstart. All other docs content had no E2E coverage.

## Solution

This PR expands the initial scaffolding to generalize the Next.js
quickstart tests, page runs and checks local links, to all pages
affecting Docs content:


- Add `resolveDocsScope` (`e2e/docs/utils/resolve-docs-scope.ts`) to map
changed guide and troubleshooting `.mdx` files to their `/docs/...` page
paths, and to expand changed `_partials` to every page that includes
them (including transitively, through partials nested inside other
partials). Federated guide sections (`graphql`,
`database/extensions/wrappers`, `ai/python`, `deployment/terraform`,
`deployment/ci`) and reference docs stay out of scope, and resolution is
capped at 20 pages to keep runtime bounded.
- Replace the single `quickstarts.spec.ts` test with a generic
`docs-pages.spec.ts` that loads whatever pages are resolved, asserting
each renders with an `<h1>` and that its docs-owned links resolve.
- Add `run-e2e-docs.ts` so `pnpm e2e:docs` resolves scope locally (from
commits since `origin/master`, plus staged/unstaged changes) and skips
Playwright entirely when nothing in scope changed.
- Update `.github/workflows/docs-e2e.yml` to widen the trigger paths to
all guides/troubleshooting/partials, resolve scope in a dedicated step,
skip the rest of the job when scope is empty, and accept a `page_paths`
input for manual `workflow_dispatch` runs.
- Rewrite `e2e/docs/README.md` to document the new scoping behavior, the
override envs (`DOCS_E2E_PAGE_PATHS`, `DOCS_E2E_BASE_REF`), and how CI
uses the suite.
- `pnpm e2e:docs:all` is also added to run tests on every page locally.
Good for scoping issues but should not be included in CI.

## Manual testing

Walk through the following steps to verify this works:

- [x] `pnpm e2e:docs` from repo root resolves the expected pages for a
local guide edit and can run against local dev
**Note:** Challenges with testing on local in part because of the long
lag for first page load. Recommendation to use a hosted URL is added to
docs.
- [x] Editing a shared `_partials` file resolves to every page that
includes it (including through nested partials)
- [x] `pnpm e2e:docs` exits cleanly with no Playwright run when no
in-scope files changed
- [x] `git diff --name-only ... | pnpm -C e2e/docs resolve-docs-scope`
prints the expected page list for a sample diff
- [x] Workflow run on a PR that only touches `e2e/docs`/workflow files
skips the Playwright steps
- [x] Manual `workflow_dispatch` run with `page_paths` set tests only
those pages
- [x] Run `pnpm e2e:docs:all` to run the suite on all docs content,
which takes awhile

## Next steps

After this PR merges, we have the scaffolding to add more fun tests like
a11y 😁

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added scoped Docs E2E runs that target eligible doc pages based on
changes, plus manual page-targeted runs and an “all eligible pages”
mode.
* Introduced `DOCS_E2E_PAGE_PATHS` (and updated base ref/base URL
behavior) to control which pages are tested.
* **Bug Fixes**
* Automatically skips Playwright setup when no relevant pages are in
scope; Playwright reporting now uploads only on failure.
* **Documentation**
* Updated the Docs E2E README with new run/CI behavior, troubleshooting
notes, and commands to inspect the resolved page list.
* **Tests**
* Added a Docs-owned pages E2E suite; removed the Next.js quickstart E2E
spec.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 00:04:58 +00:00

181 lines
5.6 KiB
JavaScript

#!/usr/bin/env node
/**
* Default entry for `pnpm e2e:docs`.
*
* If DOCS_E2E_PAGE_PATHS is already set (CI, or an explicit local override),
* runs Playwright with that list. Otherwise resolves pages from files changed
* vs DOCS_E2E_BASE_REF (default origin/master), including the working tree.
*
* Pass `--all` to test every in-scope guide and troubleshooting page instead
* (hundreds of pages — expect a long run against a deployed site).
*
* Extra CLI args are forwarded to Playwright (e.g. --ui, a spec file path).
*/
import { spawn, spawnSync } from 'node:child_process'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import {
parseChangedFilesList,
resolveAllDocsPages,
resolveDocsScope,
} from '../utils/resolve-docs-scope.ts'
const __dirname = dirname(fileURLToPath(import.meta.url))
const E2E_DOCS_ROOT = join(__dirname, '..')
// Mirrors the default in playwright.config.ts.
const DEFAULT_BASE_URL = 'http://localhost:3001'
const PREFLIGHT_TIMEOUT_MS = 3_000
async function isBaseUrlReachable(baseUrl: string): Promise<boolean> {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), PREFLIGHT_TIMEOUT_MS)
try {
await fetch(baseUrl, { signal: controller.signal })
return true
} catch {
return false
} finally {
clearTimeout(timeout)
}
}
function git(args: string[], cwd: string): string {
const result = spawnSync('git', args, {
cwd,
encoding: 'utf8',
env: process.env,
})
if (result.status !== 0) {
const detail = (result.stderr || result.stdout || '').trim()
throw new Error(`git ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`)
}
return result.stdout
}
function repoRootFromCwd(): string {
return git(['rev-parse', '--show-toplevel'], E2E_DOCS_ROOT).trim()
}
function collectChangedFiles(repoRoot: string, baseRef: string): string[] {
const ranges: string[][] = [
// Commits on this branch since diverging from the base
['diff', '--name-only', '--diff-filter=ACMR', `${baseRef}...HEAD`],
// Unstaged working tree
['diff', '--name-only', '--diff-filter=ACMR'],
// Staged working tree
['diff', '--name-only', '--diff-filter=ACMR', '--cached'],
]
const files = new Set<string>()
for (const args of ranges) {
try {
for (const file of parseChangedFilesList(git([...args], repoRoot))) {
files.add(file)
}
} catch (error) {
if (args.includes(`${baseRef}...HEAD`)) {
throw error
}
// Working-tree diffs can be empty / fail in odd git states; ignore those.
}
}
return [...files].sort()
}
async function resolveAllPagePaths(): Promise<string[]> {
const repoRoot = repoRootFromCwd()
const pages = await resolveAllDocsPages(repoRoot)
console.error(`Resolved all ${pages.length} in-scope docs page(s) (guides + troubleshooting).`)
return pages
}
async function resolvePagePaths(): Promise<string[] | null> {
const existing = process.env.DOCS_E2E_PAGE_PATHS?.trim()
if (existing) {
return existing
.split(/[\n,]/)
.map((p) => p.trim())
.filter(Boolean)
}
const baseRef = process.env.DOCS_E2E_BASE_REF?.trim() || 'origin/master'
const repoRoot = repoRootFromCwd()
const changedFiles = collectChangedFiles(repoRoot, baseRef)
const result = await resolveDocsScope({ changedFiles, repoRoot })
if (result.skip) {
console.error(
`No in-scope docs pages changed vs ${baseRef} (including working tree). Skipping Playwright.`
)
return null
}
console.error(`Resolved ${result.pages.length} docs page(s) from changes vs ${baseRef}:`)
for (const page of result.pages) {
console.error(` ${page}`)
}
return result.pages
}
async function main() {
const rawArgs = process.argv.slice(2)
const runAll = rawArgs.includes('--all')
const withoutAll = rawArgs.filter((arg) => arg !== '--all')
// pnpm's `--` separator (from `pnpm run ... -- --list`) can land at index 0
// or, once `--all` is stripped, wherever `--all` used to precede it.
const playwrightArgs = withoutAll[0] === '--' ? withoutAll.slice(1) : withoutAll
const pages = runAll ? await resolveAllPagePaths() : await resolvePagePaths()
if (pages === null) {
process.exit(0)
}
// playwright.config.ts sets a global maxFailures: 3, which would otherwise
// abort an exhaustive --all run after just 3 failing pages out of hundreds.
// -x is Playwright's shorthand for --max-failures=1.
const hasMaxFailuresArg = playwrightArgs.some(
(arg) => arg === '-x' || arg.startsWith('--max-failures')
)
const finalPlaywrightArgs =
runAll && !hasMaxFailuresArg ? [...playwrightArgs, '--max-failures=0'] : playwrightArgs
const baseUrl = process.env.PLAYWRIGHT_BASE_URL?.trim() || DEFAULT_BASE_URL
if (!(await isBaseUrlReachable(baseUrl))) {
console.error(`No docs server responding at ${baseUrl}.`)
if (!process.env.PLAYWRIGHT_BASE_URL) {
console.error('Start it with `pnpm dev:docs`, or point at a deployed site:')
console.error(' PLAYWRIGHT_BASE_URL=https://supabase.com pnpm e2e:docs')
} else {
console.error('Check that the URL is correct and reachable.')
}
process.exit(1)
}
const env = {
...process.env,
DOCS_E2E_PAGE_PATHS: pages.join(','),
}
const child = spawn('pnpm', ['exec', 'playwright', 'test', ...finalPlaywrightArgs], {
cwd: E2E_DOCS_ROOT,
env,
stdio: 'inherit',
shell: process.platform === 'win32',
})
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal)
return
}
process.exit(code ?? 1)
})
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : error)
process.exit(1)
})