+
+
-
+
Published
{blog.formattedDate}
- ·
- {blog.readingTime}
+
+ ·
+
+ {blog.readingTime}
diff --git a/apps/www/package.json b/apps/www/package.json
index b1e2deea0f6e0..404c0d41de581 100644
--- a/apps/www/package.json
+++ b/apps/www/package.json
@@ -6,7 +6,7 @@
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "pnpm run content:build && next --port 3000",
- "prebuild": "pnpm --filter=docs run build:guides-markdown",
+ "prebuild": "pnpm --filter=docs run build:federated-content && pnpm --filter=docs run build:guides-markdown",
"build": "pnpm run content:build && next build",
"export": "next export",
"start": "next start",
diff --git a/e2e/docs/.gitignore b/e2e/docs/.gitignore
new file mode 100644
index 0000000000000..68c5d18f00dcb
--- /dev/null
+++ b/e2e/docs/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/docs/README.md b/e2e/docs/README.md
new file mode 100644
index 0000000000000..deb4ca41c8a5f
--- /dev/null
+++ b/e2e/docs/README.md
@@ -0,0 +1,83 @@
+# Supabase Docs E2E Tests
+
+Playwright end-to-end tests for the docs site under `apps/docs`.
+Add new docs journeys under `features/`. They all run together as one suite.
+
+## Setup
+
+Install the Playwright browser once from this directory:
+
+```bash
+cd e2e/docs
+pnpm exec playwright install chromium
+```
+
+## Choosing a target URL
+
+Tests run against whatever `PLAYWRIGHT_BASE_URL` points to, defaulting to the
+local docs dev server at `http://localhost:3001`.
+
+- **Local docs server** — in a separate terminal, start docs from the repo root:
+
+ ```bash
+ pnpm dev:docs
+ ```
+
+- **A deployed site** — set the base URL inline:
+
+ ```bash
+ PLAYWRIGHT_BASE_URL=https://supabase.com pnpm e2e:docs
+ ```
+
+If the target is a protected Vercel preview, also set
+`VERCEL_AUTOMATION_BYPASS_SECRET` so the tests can bypass deployment protection.
+
+The local docs server requires a full monorepo install and credentials for some
+content, so the quickest way to run the suite is against a deployed site. Reach
+for the local server only when you need to test unpublished content changes.
+
+## Running the tests
+
+From the repo root:
+
+```bash
+pnpm e2e:docs
+```
+
+Or from this directory:
+
+```bash
+pnpm run e2e:docs
+```
+
+### UI mode for debugging
+
+```bash
+pnpm e2e:docs:ui
+```
+
+### Run a single file
+
+```bash
+pnpm run e2e:docs -- features/
.spec.ts
+```
+
+## Debugging
+
+- View the HTML report after a run:
+
+ ```bash
+ pnpm -C e2e/docs exec playwright show-report
+ ```
+
+- Traces and screenshots for failures are saved in `test-results/`.
+
+## CI
+
+`.github/workflows/docs-e2e.yml` runs this suite on pull requests that touch the
+tested docs pages or `e2e/docs`. When the PR changes `apps/docs`, the workflow
+waits for the matching Vercel preview and points `PLAYWRIGHT_BASE_URL` at it.
+When only the harness or workflow changes, Vercel skips the docs preview, so
+the suite falls back to production. Draft PRs are skipped until marked ready
+for review. The workflow can also be triggered manually with an optional
+`base_url` input that defaults to production.
diff --git a/e2e/docs/features/quickstarts.spec.ts b/e2e/docs/features/quickstarts.spec.ts
new file mode 100644
index 0000000000000..afc09c4637067
--- /dev/null
+++ b/e2e/docs/features/quickstarts.spec.ts
@@ -0,0 +1,45 @@
+import { expect, test } from '@playwright/test'
+
+import { collectDocsOwnedLinks } from '../utils/docs-links.js'
+
+const QUICKSTART_PATH = '/docs/guides/getting-started/quickstarts/nextjs'
+const ARTICLE_SELECTOR = '#sb-docs-guide-main-article'
+
+test.describe('Next.js quickstart', () => {
+ test('loads and docs-owned article links resolve', async ({ page }, testInfo) => {
+ const baseURL = testInfo.project.use.baseURL
+ expect(baseURL, 'A Playwright base URL should be configured').toBeTruthy()
+
+ const response = await page.goto(QUICKSTART_PATH)
+ expect(response, `Expected a response for ${QUICKSTART_PATH}`).not.toBeNull()
+ expect(
+ response!.ok(),
+ `Quickstart page should return a successful status, got ${response!.status()}`
+ ).toBeTruthy()
+
+ const article = page.locator(ARTICLE_SELECTOR)
+ await expect(article, 'Guide article should be present').toBeVisible()
+ await expect(
+ article.getByRole('heading', { level: 1 }),
+ 'Guide article should include an h1'
+ ).toBeVisible()
+
+ const links = await collectDocsOwnedLinks(page, baseURL!)
+
+ for (const url of links) {
+ try {
+ const linkResponse = await page.request.get(url)
+ expect
+ .soft(linkResponse.ok(), `${url} should resolve (status ${linkResponse.status()})`)
+ .toBeTruthy()
+ } catch (error) {
+ expect
+ .soft(
+ null,
+ `${url} should be reachable (${error instanceof Error ? error.message : error})`
+ )
+ .toBeTruthy()
+ }
+ }
+ })
+})
diff --git a/e2e/docs/package.json b/e2e/docs/package.json
new file mode 100644
index 0000000000000..3a53ee4976ee4
--- /dev/null
+++ b/e2e/docs/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "e2e-docs",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "e2e:docs": "playwright test",
+ "e2e:ui": "playwright test --ui"
+ },
+ "dependencies": {
+ "@playwright/test": "^1.59.1"
+ }
+}
diff --git a/e2e/docs/playwright.config.ts b/e2e/docs/playwright.config.ts
new file mode 100644
index 0000000000000..27712136db900
--- /dev/null
+++ b/e2e/docs/playwright.config.ts
@@ -0,0 +1,50 @@
+import { defineConfig } from '@playwright/test'
+
+const IS_CI = !!process.env.CI
+
+export default defineConfig({
+ testDir: './features',
+ testMatch: /.*\.spec\.ts/,
+ timeout: 60_000,
+ forbidOnly: IS_CI,
+ retries: IS_CI ? 2 : 0,
+ maxFailures: 3,
+ expect: {
+ timeout: 15_000,
+ },
+ fullyParallel: false,
+ workers: 1,
+ use: {
+ baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3001',
+ browserName: 'chromium',
+ headless: true,
+ navigationTimeout: 30_000,
+ screenshot: 'only-on-failure',
+ trace: 'retain-on-failure',
+ video: 'off',
+ extraHTTPHeaders: process.env.VERCEL_AUTOMATION_BYPASS_SECRET
+ ? {
+ 'x-vercel-protection-bypass': process.env.VERCEL_AUTOMATION_BYPASS_SECRET,
+ 'x-vercel-set-bypass-cookie': 'true',
+ }
+ : undefined,
+ },
+ projects: [
+ {
+ name: 'Features',
+ testDir: './features',
+ testMatch: /.*\.spec\.ts/,
+ use: {
+ browserName: 'chromium',
+ },
+ },
+ ],
+ reporter: IS_CI
+ ? [['list'], ['html', { open: 'never', outputFolder: './playwright-report' }]]
+ : [
+ ['list'],
+ ['html', { open: 'never', outputFolder: './playwright-report' }],
+ ['json', { outputFile: './test-results/test-results.json' }],
+ ],
+ outputDir: './test-results',
+})
diff --git a/e2e/docs/tsconfig.json b/e2e/docs/tsconfig.json
new file mode 100644
index 0000000000000..d59602689dc22
--- /dev/null
+++ b/e2e/docs/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "nodenext",
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "strict": true
+ }
+}
diff --git a/e2e/docs/utils/docs-links.ts b/e2e/docs/utils/docs-links.ts
new file mode 100644
index 0000000000000..5796b0b84ce4d
--- /dev/null
+++ b/e2e/docs/utils/docs-links.ts
@@ -0,0 +1,42 @@
+import type { Page } from '@playwright/test'
+
+const ARTICLE_SELECTOR = '#sb-docs-guide-main-article'
+const DOCS_PATH_PREFIX = '/docs'
+
+/**
+ * Collect unique docs-owned links from the main guide 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): Promise {
+ const origin = new URL(baseURL).origin
+ const hrefs = await page
+ .locator(`${ARTICLE_SELECTOR} a[href]`)
+ .evaluateAll((anchors) =>
+ anchors.map((anchor) => (anchor as HTMLAnchorElement).getAttribute('href') ?? '')
+ )
+ const links = new Set()
+
+ 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()
+}
diff --git a/package.json b/package.json
index e76e4c7dd3cc6..7ccfdf72a2251 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,8 @@
"e2e:setup:platform": "SKIP_ASSET_UPLOAD=1 NODE_OPTIONS=\"--max-old-space-size=4096\" pnpm run build:studio && pnpm --prefix ./apps/studio start",
"e2e": "pnpm --prefix e2e/studio run e2e",
"e2e:ui": "pnpm --prefix e2e/studio run e2e:ui",
+ "e2e:docs": "pnpm --prefix e2e/docs run e2e:docs",
+ "e2e:docs:ui": "pnpm --prefix e2e/docs run e2e:ui",
"perf:kong": "ab -t 5 -c 20 -T application/json http://localhost:8000/",
"perf:meta": "ab -t 5 -c 20 -T application/json http://localhost:5555/tables",
"setup:cli": "supabase start -x studio && supabase status --output json > keys.json && node scripts/generateLocalEnv.js",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c64b8b7953d51..3a8f291d2a4b5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2007,6 +2007,12 @@ importers:
specifier: ^7.3.2
version: 7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)
+ e2e/docs:
+ dependencies:
+ '@playwright/test':
+ specifier: ^1.59.1
+ version: 1.59.1
+
e2e/studio:
dependencies:
'@playwright/test':