diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..267bb06 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,54 @@ +name: ๐ŸŽญ Playwright E2E & Regression Tests + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: ๐Ÿงช Run Playwright E2E Tests + timeout-minutes: 15 + runs-on: ubuntu-latest + + steps: + - name: โฌ‡๏ธ Checkout Repository + uses: actions/checkout@v4 + + - name: ๐ŸŸข Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: "package.json" + cache: npm + cache-dependency-path: "package-lock.json" + + - name: ๐Ÿ“ฆ Install Dependencies + run: npm ci + + - name: ๐ŸŽญ Install Playwright Browsers (Chromium) + run: npx playwright install --with-deps chromium + + - name: ๐Ÿ”Ž Type Check & Lint + run: | + npm run type-check + npm run lint + + - name: ๐Ÿ› ๏ธ Build Static Site Export + run: npm run build + + - name: ๐Ÿš€ Run Playwright End-to-End Tests + run: npm run test:e2e + + - name: ๐Ÿ“Š Upload Playwright Test Report Artifact + uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 14 diff --git a/.gitignore b/.gitignore index 00bfcec..e7eae05 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ # testing /coverage +/test-results/ +/playwright-report/ # next.js /.next/ diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..18209d7 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,102 @@ +# ๐ŸŽญ End-to-End & Regression Testing with Playwright + +This repository maintains an automated end-to-end (E2E) and regression test suite powered by [Playwright](https://playwright.dev/). The test suite guards user-facing flows and critical business logic against unintended regressions. + +--- + +## ๐Ÿ—๏ธ Architecture & Design Principles + +1. **Production-Faithful Static Export Testing** + Next.js static site generation (`next build` / `npm run build`) generates static HTML/JS output in the `out/` directory. Tests run against this production export served locally via `npx serve out -l 3000 -L` to ensure tests evaluate exact client-side runtime behavior without development-mode artifacts. + +2. **Deterministic Offline Testing (Zero Rate Limits)** + The website client-side queries the GitHub API for releases (`https://api.github.com/**`). In the custom test fixture (`tests/fixtures/base-test.ts`), all GitHub API requests are routed and answered with local snapshots (`src/data/raw-releases.json`). This ensures: + - Zero dependency on live internet access during test runs. + - Zero risk of hitting GitHub's unauthenticated IP rate limit (60 requests/hour). + - High speed and 100% deterministic test outcomes. + +3. **No Flaky Arbitrary Timeouts** + Tests use Playwright web assertions (`expect(...).toBeVisible()`, `expect(...).toHaveClass(...)`) and locator auto-waiting. Explicit calls to `page.waitForTimeout()` are avoided. + +4. **Cross-Platform Device Coverage** + Configured for both Desktop Chrome (`1280x720`) and Mobile Chrome (`Pixel 5` viewport) to validate responsive layouts, desktop navigation, and mobile hamburger drawer interactions. + +5. **Visual Regression Policy** + Pixel-by-pixel screenshot comparisons (`toMatchSnapshot`) are intentionally **deferred** to prevent brittle test failures caused by font rendering variations across operating systems (e.g., Linux CI runners vs. Windows/macOS local machines). Instead, tests focus on structural DOM, role-based semantics, ARIA states, and CSS class assertions. + +--- + +## ๐Ÿš€ Running Tests Locally + +### Prerequisites + +Ensure project dependencies and Playwright Chromium binaries are installed: + +```bash +npm install +npx playwright install chromium +``` + +Before running tests, ensure the static production export is built: + +```bash +npm run build +``` + +### Test Commands + +| Command | Purpose | +| :--- | :--- | +| `npm run test:e2e` | Runs all end-to-end tests headlessly across Desktop and Mobile projects. | +| `npm run test:e2e:ui` | Launches Playwright's interactive UI mode with time-travel debugging and locator inspection. | +| `npm run test:e2e:report` | Opens the generated HTML test results report in your browser. | + +You can also run specific test files or projects directly: + +```bash +# Run only smoke tests +npx playwright test tests/smoke/ + +# Run only on Desktop Chrome +npx playwright test --project=chromium + +# Run a specific test in debug mode +npx playwright test tests/smoke/homepage.spec.ts --debug +``` + +--- + +## ๐Ÿ“ Test Suite Structure + +```text +tests/ +โ”œโ”€โ”€ fixtures/ +โ”‚ โ””โ”€โ”€ base-test.ts # Custom Playwright fixture intercepting GitHub API requests & checking console errors +โ”œโ”€โ”€ functional/ +โ”‚ โ”œโ”€โ”€ changelog-search.spec.ts # Changelog search filter, debouncing, & clear interactions +โ”‚ โ”œโ”€โ”€ release-filters.spec.ts # Downloads channel tabs (Stable/Beta/Alpha) & release search +โ”‚ โ””โ”€โ”€ theme.spec.ts # Dark/light theme toggle, html class mutation, & localStorage persistence +โ””โ”€โ”€ smoke/ + โ”œโ”€โ”€ changelog.spec.ts # Changelog timeline, v1/v2 deprecation badges, yanked notices, & detail pages + โ”œโ”€โ”€ community.spec.ts # Community page, contributor cards, "A Guy" Community Manager card integrity + โ”œโ”€โ”€ downloads.spec.ts # Downloads page, recommended build card, split ABIs, & deprecated/yanked releases + โ”œโ”€โ”€ footer.spec.ts # Footer design credit (@harsha-vardhan-burra) & navigation links + โ”œโ”€โ”€ homepage.spec.ts # Homepage hero, tagline, primary CTAs, & section landmarks + โ””โ”€โ”€ mobile.spec.ts # Mobile viewport hamburger button, drawer menu, and mobile navigation +``` + +--- + +## ๐Ÿค– Continuous Integration (CI) + +The GitHub Actions workflow (`.github/workflows/playwright.yml`) executes automatically on: +- Every push to `main` +- Every pull request targeting `main` +- Manual workflow dispatch + +The CI pipeline runs: +1. `npm run type-check` (TypeScript type integrity) +2. `npm run lint` (ESLint code standards) +3. `npm run build` (Static export generation) +4. `npm run test:e2e` (Playwright test suite execution) +5. Test report upload on failure for artifact inspection diff --git a/next.config.mjs b/next.config.mjs index 8a90a44..07adf8d 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -22,6 +22,8 @@ const nextConfig = { { protocol: 'https', hostname: 'raw.githubusercontent.com' }, ], }, + + productionBrowserSourceMaps: true, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 8264eb8..13d449c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "tailwind-merge": "^2.4.0" }, "devDependencies": { + "@playwright/test": "^1.63.0", "@types/node": "^20.14.0", "@types/react": "^18.3.31", "@types/react-dom": "^18.3.7", @@ -518,6 +519,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -6354,6 +6371,35 @@ "node": ">= 6" } }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index 5dd0e66..07e838d 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,10 @@ "lint": "next lint", "preview": "npm run build && serve out", "format": "prettier --write \"src/**/*.{ts,tsx,css}\"", - "type-check": "tsc --noEmit" + "type-check": "tsc --noEmit", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:report": "playwright show-report" }, "dependencies": { "@tanstack/react-query": "^5.50.0", @@ -26,6 +29,7 @@ "tailwind-merge": "^2.4.0" }, "devDependencies": { + "@playwright/test": "^1.63.0", "@types/node": "^20.14.0", "@types/react": "^18.3.31", "@types/react-dom": "^18.3.7", @@ -38,5 +42,12 @@ "serve": "^14.2.6", "tailwindcss": "^3.4.0", "typescript": "^5.5.0" - } + }, + "browserslist": [ + "last 2 Chrome versions", + "last 2 Firefox versions", + "last 2 Safari versions", + "last 2 Edge versions", + "not dead" + ] } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..a70bfd3 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,43 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Playwright E2E testing configuration for Passcodes website. + * Tests the locally built static export for deterministic, network-independent execution. + */ +export default defineConfig({ + testDir: "./tests", + timeout: 30 * 1000, + expect: { + timeout: 5000, + }, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 2 : undefined, + reporter: [ + ["html", { open: "never" }], + ["list"], + ], + use: { + baseURL: "http://localhost:3000", + trace: "retain-on-failure", + screenshot: "only-on-failure", + video: "retain-on-failure", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "mobile-chrome", + use: { ...devices["Pixel 5"] }, + }, + ], + webServer: { + command: "npx serve out -l 3000 -L", + url: "http://localhost:3000", + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, +}); diff --git a/public/komi-store-badge.png b/public/komi-store-badge.png index d0b5c07..503640a 100644 Binary files a/public/komi-store-badge.png and b/public/komi-store-badge.png differ diff --git a/public/komi-store-badge.webp b/public/komi-store-badge.webp new file mode 100644 index 0000000..f16c891 Binary files /dev/null and b/public/komi-store-badge.webp differ diff --git a/public/logo.png b/public/logo.png index 48a946a..5427a80 100644 Binary files a/public/logo.png and b/public/logo.png differ diff --git a/public/logo.webp b/public/logo.webp new file mode 100644 index 0000000..3c65c8e Binary files /dev/null and b/public/logo.webp differ diff --git a/scripts/sync-releases.mjs b/scripts/sync-releases.mjs index 93ad41a..a95aa2d 100644 --- a/scripts/sync-releases.mjs +++ b/scripts/sync-releases.mjs @@ -433,6 +433,14 @@ async function main() { const category = editorial.category || inferCategory(parsedSections); const isMajor = editorial.isMajor ?? (version.endsWith(".0.0") || releaseType === "Stable"); + // Deprecation rule: v1.x and v2.x releases are deprecated (superseded by newer database architecture in later releases). + const majorMatch = version.match(/^v?(\d+)\./i); + const majorVersion = majorMatch ? parseInt(majorMatch[1], 10) : undefined; + const isDeprecated = majorVersion === 1 || majorVersion === 2; + const deprecatedReason = isDeprecated + ? "Superseded by the newer database architecture introduced in later releases." + : undefined; + const entry = { slug, version, @@ -450,6 +458,8 @@ async function main() { isMajor, isYanked: isYanked || false, yankedReason: editorial.yankedReason || undefined, + isDeprecated, + deprecatedReason, assetsCount: assetsCount > 0 ? assetsCount : undefined, downloadCount: downloadCount > 0 ? downloadCount : undefined, }; diff --git a/src/app/changelog/[slug]/page.tsx b/src/app/changelog/[slug]/page.tsx index dfd861b..f0c0335 100644 --- a/src/app/changelog/[slug]/page.tsx +++ b/src/app/changelog/[slug]/page.tsx @@ -15,6 +15,7 @@ import { CHANGELOG_ENTRIES, getChangelogEntryBySlug, getAdjacentEntries, + isDeprecatedEntry, } from "@/lib/changelog"; import { SITE_META } from "@/lib/constants"; import { ReleaseVisual } from "@/components/visuals/ReleaseVisual"; @@ -59,6 +60,7 @@ export default function ChangelogEntryPage({ if (!entry) notFound(); const { prev, next } = getAdjacentEntries(params.slug); + const isDeprecated = isDeprecatedEntry(entry); return (
@@ -76,6 +78,11 @@ export default function ChangelogEntryPage({
+ {isDeprecated && ( + + Deprecated Release + + )} {entry.isYanked ? ( Yanked Release @@ -111,7 +118,14 @@ export default function ChangelogEntryPage({ - + {entry.version} @@ -162,6 +176,20 @@ export default function ChangelogEntryPage({
+ {/* Deprecated Notice */} + {isDeprecated && ( +
+
+
+

+ {entry.deprecatedReason || + "This release is no longer supported. Superseded by the newer database architecture introduced in later releases. Please use a current release instead."} +

+
+ )} + {/* Yanked Notice */} {entry.isYanked && (
diff --git a/src/app/changelog/page.tsx b/src/app/changelog/page.tsx index 40dc3f1..cc38ac0 100644 --- a/src/app/changelog/page.tsx +++ b/src/app/changelog/page.tsx @@ -9,10 +9,12 @@ import { ArrowRight, Calendar, GitCommit, + AlertTriangle, } from "lucide-react"; import { CHANGELOG_ENTRIES, CHANGELOG_CATEGORIES, + isDeprecatedEntry, type CategoryFilter, } from "@/lib/changelog"; import { GITHUB_RELEASES_URL, DOCS_RELEASE_NOTES_URL } from "@/lib/constants"; @@ -248,17 +250,29 @@ export default function ChangelogPage() { ); } + const isDeprecated = isDeprecatedEntry(entry); + return ( -
+
{/* Left Column: Metadata & Category Node */}
+ {isDeprecated && ( + + Deprecated + + )} {entry.isYanked ? ( Yanked @@ -294,7 +308,14 @@ export default function ChangelogPage() {
- + {entry.version}
@@ -319,7 +340,7 @@ export default function ChangelogPage() { @@ -369,6 +390,19 @@ export default function ChangelogPage() { + {isDeprecated && ( +
+
+
+

+ {entry.deprecatedReason || + "This release is no longer supported. Superseded by the newer database architecture introduced in later releases. Please use a current release instead."} +

+
+ )} + {entry.isYanked && entry.yankedReason && (
Release Notice: diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9a596fc..ad9cef7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,17 +1,43 @@ import type { Metadata } from "next"; +import dynamic from "next/dynamic"; import { Space_Grotesk } from "next/font/google"; import { QueryProvider } from "@/providers/QueryProvider"; import { ThemeProvider, THEME_INIT_SCRIPT } from "@/providers/ThemeProvider"; import { Navbar } from "@/components/layout/Navbar"; import { Footer } from "@/components/layout/Footer"; import { PageTransition } from "@/components/layout/PageTransition"; -import { GithubStarDialog } from "@/components/shared/GithubStarDialog"; import { SITE_META } from "@/lib/constants"; -import { PowerShortcuts } from "@/components/layout/PowerShortcuts"; import { AmbientBackground } from "@/components/visuals/AmbientBackground"; import "@/styles/globals.css"; -const spaceGrotesk = Space_Grotesk({ subsets: ["latin"], display: "swap" }); +const GithubStarDialog = dynamic( + () => + import("@/components/shared/GithubStarDialog").then( + (m) => m.GithubStarDialog + ), + { ssr: false } +); + +const PowerShortcuts = dynamic( + () => + import("@/components/layout/PowerShortcuts").then( + (m) => m.PowerShortcuts + ), + { ssr: false } +); + +const spaceGrotesk = Space_Grotesk({ + subsets: ["latin"], + display: "swap", + fallback: [ + "system-ui", + "-apple-system", + "BlinkMacSystemFont", + "Segoe UI", + "Roboto", + "sans-serif", + ], +}); export const metadata: Metadata = { title: { default: SITE_META.title, template: "%s | Passcodes" }, diff --git a/src/components/downloads/ArchDownload.tsx b/src/components/downloads/ArchDownload.tsx index 894ae67..e0190ba 100644 --- a/src/components/downloads/ArchDownload.tsx +++ b/src/components/downloads/ArchDownload.tsx @@ -59,7 +59,6 @@ export function ArchDownload({ assets, variant = "full", className }: Props) {