diff --git a/docs/marketing/landing-20260305.jpg b/docs/marketing/landing-20260305.jpg new file mode 100644 index 0000000..50e52b7 Binary files /dev/null and b/docs/marketing/landing-20260305.jpg differ diff --git a/docs/marketing/landing-20260305.png b/docs/marketing/landing-20260305.png new file mode 100644 index 0000000..9b0e978 Binary files /dev/null and b/docs/marketing/landing-20260305.png differ diff --git a/package.json b/package.json index 18b7211..e1a92a2 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "cmdk": "1.0.0", "date-fns": "^4.1.0", "dotenv": "^17.3.1", + "geist": "^1.7.0", "jose": "6.1.3", "lucide-react": "0.475.0", "nanoid": "5.1.6", @@ -67,7 +68,6 @@ "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", - "jsdom": "24.1.3", "@types/node": "^20", "@types/pg": "^8.16.0", "@types/react": "^19", @@ -77,6 +77,7 @@ "dotenv-cli": "11.0.0", "eslint": "^9", "eslint-config-next": "16.1.6", + "jsdom": "24.1.3", "openid-client": "6.8.2", "playwright": "1.58.2", "postcss": "^8", diff --git a/playwright.config.ts b/playwright.config.ts index 831fe47..d25e668 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,3 +1,4 @@ +import fs from "node:fs"; import path from "node:path"; import { defineConfig, devices } from "@playwright/test"; @@ -11,23 +12,47 @@ const ldLibrarySegments = (process.env.PLAYWRIGHT_LD_LIBRARY_PATH ?? "") path.isAbsolute(segment) ? segment : path.resolve(projectRoot, segment), ); +const headlessShellPath = path.resolve( + projectRoot, + ".playwright-browsers", + "chromium_headless_shell-1208", + "chrome-linux", + "headless_shell", +); + const chromiumLdLibraryPath = [ "/usr/lib/x86_64-linux-gnu", "/lib/x86_64-linux-gnu", ...ldLibrarySegments, - path.resolve( - projectRoot, - ".playwright-browsers", - "chromium_headless_shell-1208", - "chrome-headless-shell-linux64", - ), + path.dirname(headlessShellPath), ] .filter(Boolean) .join(":"); +const chromiumExecutablePath = (() => { + if (process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE) { + return process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE; + } + + if (process.arch === "x64" && fs.existsSync(headlessShellPath)) { + return headlessShellPath; + } + + const fullChromiumBinary = path.resolve( + projectRoot, + ".playwright-browsers", + "chromium-1208", + "chrome-linux", + "chrome", + ); + + return fullChromiumBinary; +})(); + export default defineConfig({ testDir: "tests/e2e", timeout: 120_000, + retries: process.env.CI ? 1 : 0, expect: { timeout: 10_000, }, @@ -35,6 +60,14 @@ export default defineConfig({ baseURL: process.env.PLAYWRIGHT_BASE_URL ?? "http://127.0.0.1:3000", trace: "on-first-retry", launchOptions: { + executablePath: chromiumExecutablePath, + args: [ + "--disable-dev-shm-usage", + "--no-sandbox", + "--disable-gpu", + "--use-gl=swiftshader", + "--no-zygote", + ], env: { ...process.env, LD_LIBRARY_PATH: chromiumLdLibraryPath, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5d1c15..a9d3256 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: dotenv: specifier: ^17.3.1 version: 17.3.1 + geist: + specifier: ^1.7.0 + version: 1.7.0(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) jose: specifier: 6.1.3 version: 6.1.3 @@ -2665,6 +2668,11 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + geist@1.7.0: + resolution: {integrity: sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ==} + peerDependencies: + next: '>=13.2.0' + generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} @@ -6768,6 +6776,10 @@ snapshots: functions-have-names@1.2.3: {} + geist@1.7.0(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): + dependencies: + next: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + generate-function@2.3.1: dependencies: is-property: 1.0.2 diff --git a/scripts/capture-landing-screenshot.sh b/scripts/capture-landing-screenshot.sh new file mode 100755 index 0000000..a9d4550 --- /dev/null +++ b/scripts/capture-landing-screenshot.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +paths=( + "$(nix eval --raw nixpkgs#glib.out)/lib" + "$(nix eval --raw nixpkgs#libXfixes.out)/lib" + "$(nix eval --raw nixpkgs#nspr.out)/lib" + "$(nix eval --raw nixpkgs#nss.out)/lib" + "$(nix eval --raw nixpkgs#atk.out)/lib" + "$(nix eval --raw nixpkgs#cairo.out)/lib" + "$(nix eval --raw nixpkgs#pango.out)/lib" + "$(nix eval --raw nixpkgs#gdk-pixbuf.out)/lib" + "$(nix eval --raw nixpkgs#gtk3.out)/lib" + "$(nix eval --raw nixpkgs#dbus.lib)/lib" + "$(nix eval --raw nixpkgs#cups.lib)/lib" + "$(nix eval --raw nixpkgs#expat.out)/lib" + "$(nix eval --raw nixpkgs#libxcb.out)/lib" + "$(nix eval --raw nixpkgs#libxkbcommon.out)/lib" + "$(nix eval --raw nixpkgs#libX11.out)/lib" + "$(nix eval --raw nixpkgs#libXcomposite.out)/lib" + "$(nix eval --raw nixpkgs#libXdamage.out)/lib" + "$(nix eval --raw nixpkgs#libXext.out)/lib" + "$(nix eval --raw nixpkgs#libXrandr.out)/lib" + "$(nix eval --raw nixpkgs#libgbm.out)/lib" + "$(nix eval --raw nixpkgs#alsa-lib.out)/lib" +) + +LD_JOINED=$(printf "%s:" "${paths[@]}") +LD_JOINED=${LD_JOINED%:} + +export PLAYWRIGHT_BROWSERS_PATH=".playwright-browsers" +export LD_LIBRARY_PATH="${LD_JOINED}:${LD_LIBRARY_PATH:-}" + +start_dev_server() { + pnpm dev --hostname 127.0.0.1 --port 3000 >/tmp/mockauth-dev.log 2>&1 & + DEV_PID=$! + trap 'kill ${DEV_PID} >/dev/null 2>&1 || true' EXIT + + for _ in $(seq 1 120); do + if curl -fsS http://127.0.0.1:3000 >/dev/null; then + echo "Dev server ready" + return 0 + fi + sleep 1 + done + + echo "Dev server failed to become ready" >&2 + return 1 +} + +main() { + start_dev_server + pnpm exec node -e "const path = require('node:path'); const { chromium } = require('playwright'); (async () => { const executablePath = path.resolve('.playwright-browsers', 'chromium-1208', 'chrome-linux', 'chrome'); const browser = await chromium.launch({ headless: true, executablePath, args: ['--no-sandbox'] }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); await page.goto('http://127.0.0.1:3000', { waitUntil: 'networkidle' }); await page.screenshot({ path: 'docs/marketing/landing-20260305.png', fullPage: true }); await browser.close(); })().catch(err => { console.error(err); process.exit(1); });" +} + +main diff --git a/scripts/run-playwright-suite.mjs b/scripts/run-playwright-suite.mjs index 5b8ed80..127a105 100755 --- a/scripts/run-playwright-suite.mjs +++ b/scripts/run-playwright-suite.mjs @@ -47,6 +47,13 @@ const env = { .join(':'), }; +env.PLAYWRIGHT_LD_LIBRARY_PATH = [ + process.env.PLAYWRIGHT_LD_LIBRARY_PATH ?? '', + process.env.LD_LIBRARY_PATH ?? '', +] + .filter(Boolean) + .join(':'); + const runSpec = (spec) => new Promise((resolve, reject) => { process.stdout.write(`\n=== Running ${spec} ===\n`); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4205c45..eeae91f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { GeistMono } from "geist/font/mono"; +import { GeistSans } from "geist/font/sans"; import "./globals.css"; @@ -7,16 +8,6 @@ import { ThemeProvider } from "@/components/theme-provider"; import { Toaster } from "@/components/ui/toaster"; import { cn } from "@/lib/utils"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - export const metadata: Metadata = { title: "Mockauth", description: "Multi-tenant mock OIDC provider for QA environments", @@ -24,8 +15,8 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - - + + {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index aed1743..d90f581 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,4 @@ import type { ReactNode } from "react"; -import Link from "next/link"; const frictionPoints = [ { @@ -95,24 +94,28 @@ const excellenceItems: { title: string; description: ReactNode }[] = [ ]; const primaryHeroButtonClasses = - "inline-flex items-center justify-center rounded-full bg-white px-8 py-4 text-base font-semibold text-indigo-700 shadow-lg shadow-indigo-950/40 transition hover:bg-indigo-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"; + "inline-flex items-center justify-center rounded-full bg-white px-8 py-4 text-base font-semibold text-indigo-700 shadow-lg shadow-indigo-900/40 transition hover:bg-indigo-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"; const secondaryHeroLinkClasses = - "inline-flex items-center justify-center rounded-full border border-white/70 bg-white/10 px-8 py-4 text-base font-semibold text-white/90 shadow-lg shadow-indigo-950/20 transition hover:bg-white/20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"; + "inline-flex items-center justify-center rounded-full border border-white/70 bg-white/10 px-8 py-4 text-base font-semibold text-white/90 shadow-lg shadow-indigo-900/20 transition hover:bg-white/20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"; export default function Home() { const currentYear = new Date().getFullYear(); + const productionFeatureSplitIndex = Math.ceil(productionFeatures.length / 2); + const productionFeatureColumns = [ + productionFeatures.slice(0, productionFeatureSplitIndex), + productionFeatures.slice(productionFeatureSplitIndex), + ]; + const developerFeatureSplitIndex = Math.ceil(developerFeatures.length / 2); + const developerFeatureColumns = [ + developerFeatures.slice(0, developerFeatureSplitIndex), + developerFeatures.slice(developerFeatureSplitIndex), + ]; return ( -
+
-
- - MockAuth - +
-
-
-
-
-
-

Eliminate Auth Friction

-
- {frictionPoints.map((point) => ( -
-

{point.title}

-

{point.description}

-
- ))} +
+
+

The Auth Testing Standard

+
+ + A purpose-built, standards-compliant OIDC identity provider designed for testing. It simulates the behavior of a + production authentication server, allowing you to validate sign-ins, token handling, and redirect logic in isolated + environments without relying on real user accounts or external services. It is optimized for QA, local development, + and ephemeral CI pipelines where you need reliable, repeatable, and clean auth states. + +
-
-
+ -
-
-
-

Key Features — Production-Grade Standards

-
- {productionFeatures.map((feature) => ( +
+
+

Eliminate Auth Friction

+
+ {frictionPoints.map((point) => (
-

{feature.title}

-

{feature.description}

+

{point.title}

+

{point.description}

))}
-
-

Key Features — Developer Experience

-
- {developerFeatures.map((feature) => ( +
+ +
+
+
+

+ Key Features — Production-Grade Standards +

+
+ {productionFeatureColumns + .filter((column) => column.length > 0) + .map((column, columnIndex) => ( +
    + {column.map((feature) => ( +
  • + + ✓ + +
    +
    {feature.title}
    +

    {feature.description}

    +
    +
  • + ))} +
+ ))} +
+
+
+

+ Key Features — Developer Experience +

+
+ {developerFeatureColumns + .filter((column) => column.length > 0) + .map((column, columnIndex) => ( +
    + {column.map((feature) => ( +
  • + + ✓ + +
    +
    {feature.title}
    +

    {feature.description}

    +
    +
  • + ))} +
+ ))} +
+
+
+
+ +
+
+

Deployable Anywhere

+
+ {deployableHighlights.map((item) => (
-

{feature.title}

-

{feature.description}

+

{item.title}

+

{item.description}

))}
-
-
+ -
-
-

Deployable Anywhere

-
- {deployableHighlights.map((item) => ( -
-

{item.title}

-

{item.description}

-
- ))} +
+
+

Where MockAuth Excels

+
    + {excellenceItems.map((item, index) => ( +
  1. + + {index + 1} + +

    + {item.title}: {item.description} +

    +
  2. + ))} +
-
-
+ +
-
+
-

Where MockAuth Excels

-
    - {excellenceItems.map((item, index) => ( -
  1. - - {index + 1} - -

    - {item.title}: {item.description} -

    -
  2. - ))} -
-
-
- -
-
-
-