Files
supabase/e2e/docs/utils/docs-links.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

90 lines
2.7 KiB
TypeScript

import type { Page } from '@playwright/test'
export const GUIDE_ARTICLE_SELECTOR = '#sb-docs-guide-main-article'
export const TROUBLESHOOTING_ARTICLE_SELECTOR = 'article.prose'
const DOCS_PATH_PREFIX = '/docs'
const TROUBLESHOOTING_PATH_PREFIX = '/docs/guides/troubleshooting/'
/**
* Pick the main article selector for a docs page path.
* Guides use a stable id; troubleshooting entries use a plain prose article.
*/
export function articleSelectorForPagePath(pagePath: string): string {
const pathname = pagePath.startsWith('http') ? new URL(pagePath).pathname : pagePath
if (
pathname === TROUBLESHOOTING_PATH_PREFIX.slice(0, -1) ||
pathname.startsWith(TROUBLESHOOTING_PATH_PREFIX)
) {
return TROUBLESHOOTING_ARTICLE_SELECTOR
}
return GUIDE_ARTICLE_SELECTOR
}
/**
* Collect unique docs-owned links from the main article.
*
* Cross-app paths such as `/ui` and `/dashboard` are excluded because the
* docs preview does not own those routes.
*/
export async function collectDocsOwnedLinks(
page: Page,
baseURL: string,
articleSelector: string = GUIDE_ARTICLE_SELECTOR
): Promise<string[]> {
const origin = new URL(baseURL).origin
const hrefs = await page
.locator(`${articleSelector} a[href]`)
.evaluateAll((anchors) =>
anchors.map((anchor) => (anchor as HTMLAnchorElement).getAttribute('href') ?? '')
)
const links = new Set<string>()
for (const href of hrefs) {
if (!href || href.startsWith('#')) continue
let url: URL
try {
url = new URL(href, baseURL)
} catch {
continue
}
if (!['http:', 'https:'].includes(url.protocol)) continue
if (url.origin !== origin) continue
if (url.pathname !== DOCS_PATH_PREFIX && !url.pathname.startsWith(`${DOCS_PATH_PREFIX}/`)) {
continue
}
url.hash = ''
links.add(url.toString())
}
return [...links].sort()
}
/**
* Playwright's headless Chromium reports a `HeadlessChrome` UA string, which
* Vercel's bot protection blocks on some routes (notably /docs/reference/*)
* even though the same page loads fine for a real browser. Stripping
* `Headless` avoids that false positive when checking links out-of-band via
* page.request rather than an actual navigation.
*/
export async function browserLikeUserAgent(page: Page): Promise<string> {
const userAgent = await page.evaluate(() => navigator.userAgent)
return userAgent.replace('HeadlessChrome', 'Chrome')
}
/**
* Parse DOCS_E2E_PAGE_PATHS (comma- or newline-separated /docs/... paths).
*/
export function parseDocsE2EPagePaths(raw: string | undefined): string[] {
if (!raw?.trim()) return []
return raw
.split(/[\n,]/)
.map((path) => path.trim())
.filter(Boolean)
.map((path) => (path.startsWith('/') ? path : `/${path}`))
}