mirror of
https://github.com/supabase/supabase.git
synced 2026-09-08 19:08:44 +08:00
Closes DOCS-1278 ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature. Adds E2E test scaffolding and a CI check for the marketing site. ## What is the current behavior? Closes [FE-4047](https://linear.app/supabase/issue/FE-4047). The marketing site has no E2E coverage. Docs has a suite in `e2e/docs`, but its runner, git helpers and axe reporting are private to that package, so a second site cannot reuse them. ## What is the new behavior? * **A www suite scoped to changed content.** Changed `.mdx` files in `_blog`, `_events`, `_customers` and `_alternatives` map to the URLs they render. Pages with `disable_page_build: true` are skipped because they 404 by design. Capped at 20 pages. Enforces `heading-order` and `page-has-heading-one`, matching docs. * **`e2e/shared` The docs site is also static with similar needs. This folder shares the docs logic with www. * **A CI check that is safe to mark required.** Path scoping lives in a `Detect changed paths` step rather than a `paths:` trigger, so the check reports on every pull request instead of being skipped. `waitForVercelDocsPreview.js` becomes `waitForVercelPreview.js`, shared by both workflows. ## How the check behaves The job always reports a check run, so it is safe to mark required. Path scoping happens in a step rather than a `paths:` trigger, which would leave non-www pull requests waiting on a check that never reports. | Case | Behavior | | --- | --- | | Fork pull request adds new pages | Passes without testing. The Vercel wait is gated on `head.repo.full_name == github.repository`, so forks resolve no preview URL. The job emits a `::warning` and a job summary containing a ready-to-run `gh workflow run www-e2e.yml` command with the resolved page paths, so a maintainer can run it against the preview. | | Vercel preview times out or fails | Passes without testing. The wait step is `continue-on-error: true`, so a 900s timeout or a failed deployment leaves the URL unset and the suite skips. Vercel's own `Vercel – zone-www-dot-com` check already reports the failure. | | Draft pull request | Job does not run at all, gated at the job level on `pull_request.draft == false`. `ready_for_review` is in the trigger's `types`, so marking it ready runs the check. | | Another app changed, www untouched | Job runs and every step skips. The `www` filter matches only the four content directories, `e2e/www`, `e2e/shared`, the lockfile, and this workflow. | | Only the harness changed | Passes without testing. Scope resolves to zero pages, and the Vercel wait is additionally gated on `www_app`, so it does not wait for a preview Vercel skipped. | | No preview resolves, any reason | Skips rather than falling back to production. Production does not serve pages the pull request adds, so testing it would fail a valid change. | ### Not covered Changes to `apps/www` components and routes do not trigger this check — only the four content directories do. A follow-up can check global components such as the navigation and the footer. ## Manual testing 1. Start the site: `pnpm dev:www` 2. Run `pnpm e2e:www` with no www content changed. It should resolve zero pages and skip Playwright, not fail. 3. Touch a post, then run `pnpm e2e:www` again: `echo "" >> apps/www/_blog/2024-01-01-some-post.mdx`. The resolved `/blog/...` path should be listed before Playwright starts. 4. Run against production with no local server: `PLAYWRIGHT_BASE_URL=https://supabase.com WWW_E2E_PAGE_PATHS=/blog/postgres-language-server pnpm e2e:www` 5. Point step 4 at a page with a known heading problem. The failure should name the rule, the CSS selector and the markup. 6. Confirm docs still passes on the shared runner: `pnpm dev:docs`, then `pnpm e2e:docs` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added WWW end-to-end testing for affected content pages, including accessibility checks. * Added standard and full-site test commands, configurable preview testing, and failure reports. * Added shared utilities for page discovery, accessibility scanning, and test execution. * **Documentation** * Documented WWW test setup, coverage, debugging, CI behavior, and running checks against production or preview environments. * **Improvements** * Updated documentation test workflows to better identify affected changes and handle preview environments. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
104 lines
2.5 KiB
TypeScript
104 lines
2.5 KiB
TypeScript
import type { Page, TestInfo } from '@playwright/test'
|
|
import type { Result } from 'axe-core'
|
|
|
|
import { scan } from '../../shared/axe.ts'
|
|
|
|
export const WCAG_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']
|
|
|
|
export const ENFORCED_RULES = ['heading-order', 'page-has-heading-one']
|
|
|
|
export const EXCLUDED_RULES = [
|
|
'color-contrast',
|
|
'html-has-lang',
|
|
'html-lang-valid',
|
|
'html-xml-lang-mismatch',
|
|
'document-title',
|
|
'aria-hidden-body',
|
|
'meta-viewport',
|
|
'meta-refresh',
|
|
'css-orientation-lock',
|
|
]
|
|
|
|
export interface A11yScanResult {
|
|
surface: string
|
|
url: string
|
|
include: string
|
|
excludedRules: string[]
|
|
loaded: boolean
|
|
status: number | null
|
|
elementCount: number
|
|
violations: Result[]
|
|
}
|
|
|
|
export function shouldEnforceAll(): boolean {
|
|
return !!process.env.A11Y_ENFORCE_ALL
|
|
}
|
|
|
|
export async function scanArticle(
|
|
page: Page,
|
|
surface: string,
|
|
include: string
|
|
): Promise<A11yScanResult> {
|
|
const reported = await scan(page, { tags: WCAG_TAGS, excludeRules: EXCLUDED_RULES, include })
|
|
const enforced = await scan(page, { rules: ENFORCED_RULES, include })
|
|
|
|
const byRule = new Map([...reported, ...enforced].map((violation) => [violation.id, violation]))
|
|
|
|
const elementCount = await page.evaluate(
|
|
(selector) => document.querySelector(selector)?.querySelectorAll('*').length ?? 0,
|
|
include
|
|
)
|
|
|
|
return {
|
|
surface,
|
|
url: page.url(),
|
|
include,
|
|
excludedRules: EXCLUDED_RULES,
|
|
loaded: true,
|
|
status: null,
|
|
elementCount,
|
|
violations: [...byRule.values()],
|
|
}
|
|
}
|
|
|
|
export function unloadedResult(
|
|
surface: string,
|
|
url: string,
|
|
status: number | null,
|
|
include: string
|
|
): A11yScanResult {
|
|
return {
|
|
surface,
|
|
url,
|
|
include,
|
|
excludedRules: EXCLUDED_RULES,
|
|
loaded: false,
|
|
status,
|
|
elementCount: 0,
|
|
violations: [],
|
|
}
|
|
}
|
|
|
|
export const MIN_MEANINGFUL_ELEMENTS = 20
|
|
|
|
export function scanLooksEmpty(
|
|
result: A11yScanResult,
|
|
minElements: number = MIN_MEANINGFUL_ELEMENTS
|
|
): boolean {
|
|
return result.elementCount < minElements
|
|
}
|
|
|
|
export async function attachScanReport(testInfo: TestInfo, result: A11yScanResult): Promise<void> {
|
|
await testInfo.attach('axe-results.json', {
|
|
body: JSON.stringify(result, null, 2),
|
|
contentType: 'application/json',
|
|
})
|
|
}
|
|
|
|
export function blockingViolations(result: A11yScanResult): Result[] {
|
|
if (shouldEnforceAll()) return result.violations
|
|
return result.violations.filter((violation) => ENFORCED_RULES.includes(violation.id))
|
|
}
|
|
|
|
export { formatViolations, settleForAxe, violationIds } from '../../shared/axe.ts'
|